ejabberd-21.12/0000755000232200023220000000000014154362354013632 5ustar debalancedebalanceejabberd-21.12/inetrc0000644000232200023220000000015714154362354015044 0ustar debalancedebalance{lookup,["file","native"]}. {host,{127,0,0,1}, ["localhost","hostalias"]}. {file, resolv, "/etc/resolv.conf"}. ejabberd-21.12/lib/0000755000232200023220000000000014154362354014400 5ustar debalancedebalanceejabberd-21.12/lib/mix/0000755000232200023220000000000014154362354015175 5ustar debalancedebalanceejabberd-21.12/lib/mix/tasks/0000755000232200023220000000000014154362354016322 5ustar debalancedebalanceejabberd-21.12/lib/mix/tasks/deps.tree.ex0000644000232200023220000000572514154362354020562 0ustar debalancedebalancedefmodule 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.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 ejabberd-21.12/lib/ejabberd/0000755000232200023220000000000014154362354016136 5ustar debalancedebalanceejabberd-21.12/lib/ejabberd/module.ex0000644000232200023220000000057214154362354017765 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config_util.ex0000644000232200023220000000065214154362354021001 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/0000755000232200023220000000000014154362354017403 5ustar debalancedebalanceejabberd-21.12/lib/ejabberd/config/validator/0000755000232200023220000000000014154362354021370 5ustar debalancedebalanceejabberd-21.12/lib/ejabberd/config/validator/validation.ex0000644000232200023220000000233114154362354024057 0ustar debalancedebalancedefmodule 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.Validator @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 ejabberd-21.12/lib/ejabberd/config/validator/validator_dependencies.ex0000644000232200023220000000170414154362354026423 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/validator/validator_attrs.ex0000644000232200023220000000150214154362354025126 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/validator/validator_utility.ex0000644000232200023220000000155514154362354025504 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/ejabberd_hook.ex0000644000232200023220000000104314154362354022515 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/logger/0000755000232200023220000000000014154362354020662 5ustar debalancedebalanceejabberd-21.12/lib/ejabberd/config/logger/ejabberd_logger.ex0000644000232200023220000000247614154362354024326 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/attr.ex0000644000232200023220000001005614154362354020715 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/config.ex0000644000232200023220000000744714154362354021222 0ustar debalancedebalancedefmodule 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() |> Map.put(:modules, get_modules_parsed_in_order()) |> Map.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 ejabberd-21.12/lib/ejabberd/config/store.ex0000644000232200023220000000240614154362354021077 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/opts_formatter.ex0000644000232200023220000000247514154362354023021 0ustar debalancedebalancedefmodule 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 ejabberd-21.12/lib/ejabberd/config/ejabberd_module.ex0000644000232200023220000000403214154362354023043 0ustar debalancedebalancedefmodule 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.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 ejabberd-21.12/lib/ejabberd/logger.ex0000644000232200023220000000060614154362354017755 0ustar debalancedebalancedefmodule Ejabberd.Logger do def critical(message, args \\ []), do: :logger.critical(message, args) def error(message, args \\ []), do: :logger.error(message, args) def warning(message, args \\ []), do: :logger.warning(message, args) def info(message, args \\ []), do: :logger.info(message, args) def debug(message, args \\ []), do: :logger.debug( message, args) end ejabberd-21.12/lib/ejabberd/hooks.ex0000644000232200023220000000056314154362354017623 0ustar debalancedebalancedefmodule Ejabberd.Hooks do # Generic hook setting features def add(hook_name, host, module, function, priority) do :ejabberd_hooks.add(hook_name, host, module, function, priority) end # Should be named 'removed' def delete(hook_name, host, module, function, priority) do :ejabberd_hooks.delete(hook_name, host, module, function, priority) end end ejabberd-21.12/lib/mod_presence_demo.ex0000644000232200023220000000124014154362354020402 0ustar debalancedebalancedefmodule ModPresenceDemo do use Ejabberd.Module def start(host, _opts) do info('Starting ejabberd module Presence Demo') 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, __MODULE__, :on_presence, 50) :ok end def on_presence(user, _server, _resource, _packet) do info('Receive presence for #{user}') :none end def depends(_host, _opts) do [] end def mod_options(_host) do [] end def mod_doc() do %{:desc => 'This is just a demonstration.'} end end ejabberd-21.12/_checkouts/0000755000232200023220000000000014154362354015761 5ustar debalancedebalanceejabberd-21.12/_checkouts/configure_deps/0000755000232200023220000000000014154362354020755 5ustar debalancedebalanceejabberd-21.12/_checkouts/configure_deps/src/0000755000232200023220000000000014154362354021544 5ustar debalancedebalanceejabberd-21.12/_checkouts/configure_deps/src/configure_deps_prv.erl0000644000232200023220000000427114154362354026137 0ustar debalancedebalance-module(configure_deps_prv). -export([init/1, do/1, format_error/1]). -define(PROVIDER, 'configure-deps'). -define(DEPS, [install_deps]). %% =================================================================== %% Public API %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([ {namespace, default}, {name, ?PROVIDER}, % The 'user friendly' name of the task {module, ?MODULE}, % The module implementation of the task {bare, true}, % The task can be run by the user, always true {deps, ?DEPS}, % The list of dependencies {example, "rebar3 configure-deps"}, % How to use the plugin {opts, []}, % list of options understood by the plugin {short_desc, "Explicitly run ./configure for dependencies"}, {desc, "A rebar plugin to allow explicitly running ./configure on depdendencies. Useful if dependencies might change prior to compilation when configure is run."} ]), {ok, rebar_state:add_provider(State, Provider)}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> Apps = rebar_state:project_apps(State) ++ lists:usort(rebar_state:all_deps(State)), lists:foreach(fun do_app/1, Apps), {ok, State}. exec_configure({'configure-deps', Cmd}, Dir) -> rebar_utils:sh(Cmd, [{cd, Dir}, {use_stdout, true}]); exec_configure(_, Acc) -> Acc. parse_pre_hooks({pre_hooks, PreHooks}, Acc) -> lists:foldl(fun exec_configure/2, Acc, PreHooks); parse_pre_hooks(_, Acc) -> Acc. parse_additions({add, App, Additions}, {MyApp, Dir}) when App == MyApp -> lists:foldl(fun parse_pre_hooks/2, Dir, Additions), {MyApp, Dir}; parse_additions(_, Acc) -> Acc. do_app(App) -> Dir = rebar_app_info:dir(App), Opts = rebar_app_info:opts(App), Overrides = rebar_opts:get(Opts, overrides), lists:foldl(fun parse_additions/2, {binary_to_atom(rebar_app_info:name(App), utf8), Dir}, Overrides). -spec format_error(any()) -> iolist(). format_error(Reason) -> io_lib:format("~p", [Reason]). ejabberd-21.12/_checkouts/configure_deps/src/configure_deps.app.src0000644000232200023220000000035014154362354026026 0ustar debalancedebalance{application, configure_deps, [{description, "A rebar3 plugin to explicitly run configure on dependencies"}, {vsn, "0.0.1"}, {registered, []}, {applications, [kernel, stdlib]}, {env,[]}, {modules, []}, {links, []} ]}. ejabberd-21.12/_checkouts/configure_deps/src/configure_deps.erl0000644000232200023220000000027014154362354025243 0ustar debalancedebalance-module(configure_deps). -export([init/1]). -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> {ok, State1} = configure_deps_prv:init(State), {ok, State1}. ejabberd-21.12/_checkouts/configure_deps/rebar.config0000644000232200023220000000004514154362354023236 0ustar debalancedebalance{erl_opts, [debug_info]}. {deps, []}.ejabberd-21.12/rebar30000755000232200023220000350406514154362354014753 0ustar debalancedebalance#!/usr/bin/env escript %% Rebar3 3.15.2 %%! +sbtu +A1 PKiR'bbmustache/ebin/bbmustache.appmPMk0 WM^w,uz =ȉڲsGe@ctG1~΂Dz->KS[˘!%OL C! -|!`i6f]A64tw?Ⱥ;,hYR*d+mF{3$`PU$ŝ8Ӎ?|.ެfDb~i4ju_=}fw?}PKiR+P3bbmustache/ebin/bbmustache.beam|XsUWH'i$\:i'm$I8 0L,Yj;"$v.CB3L@3LN>l-U[_@־>;ݲ6lRv9.ORSn{1CХRϭN8]Uwf;ծpz\U힍FU]t.=#`:apש@suZ=s՞:>Ͷ|{Utjk!tPhqܡsʁp& gvugtgOUKl6Nwc: NHȆ[ωmAzw݆.T}[t]שϺNwq{_O%Knv5h>X4KZÝn;y :Z.CaÙon[69.6z=Nfd9ɤfEzsx,AΠoeYnu}kzFo6l5maDq,A`ȭ.{N3 ~Vb>T#xВXTݡV[jHTOv/*;׺l,"Hpo:47=-6Zlz& |f M|׍uכgN'~b5,d3|>Hu5 ȝ Ŀ߆ܭ6CKNo?+ ~NT dKg;`YZ)KĖ@ypsam0=JȬW*S[f[RnpZv^vzaZ"':%ʲ|{#1_4[|9WK?0f9ĕcغ8SSd#χPY]9cCbq<njխ5*|5%Osp*`yl}!i~ &o10Egqu'y'} i|H=}`tr Xۚmi,anz<<, Y[2Τd|O-ElJ}i40zL::L{ڊW]Mi%3d1ļN;:qѢU˰ svMoe`tN>SKܻ}AwBwmHo/^-bQ/Mp!̻91Gٓ_!ڀQI@Sq| χt<ϓ͊6fŐ)iӀp0+ FoAk;v`u oR13XGu*$X!Qy2K7+P+O ,U4;>3/sn7G^ Vd&Eё+0ͦdM1Rͤ兼de'̿%>OF,\;drM2!^11*b x4&V4Kiʱw|w|v#Ƽ䢄@oND%GJWH^+5K=侴q{8nȕ񵽼n'Ztȕ &~1]i*1>J6MrٴϦK)4B9eɴ!B^ ZOf,9GBf(e7i+!hQX6Y1*<H)Hz<Ȣe(6293!@a¨dP\&Ph%2s@fOR>JWo/T>u:Wk$.=t(961i`3* EIařRqdR8Mk|t_$eq&%J DP3 U.1:{3P 3P <_ ,'oԑẌa8#";d6* ִ("?KTƏgD+q?lJΤWEDUy(nHt0*Ci ST1(^WY(w5#Ovc‘4? 3L-BUm^_f"4WЫ@AɌ}>LI*V[U^VE7W֎%+TkU_KxgU"Ƚބp)+F$By52b kI f T^- aO} ԧ HWpZGj@K482sfCD !p>WUn FwT~GYrҾjeTe>ӹ\>keY 7|_.*ѦEF|y,c#F5~Y4_61|!xRaՙf\3_Dg08%6F^%^L̨ٟVVc+[^G2CpU:RYaH#k~€U>#Eԃ/k"*KO̺5 q8 RRDեrKFQu /bBrP_gWuaF7+\H1 D+xby^C# xcz45#'7 Y 'c &bʰxYv רK/9곸=Ma43@G%_׆뛲9V/˲V'[oBhΈķЂjuuC W&R1-AJkUojFQE01u?UY7}T<9э0x%zݚ` (j/A3CR N,0uP!dH^W{4Wu#m+K J1lt-pgi_)sC|WH9vq )&6>/(w=~ZQ8(O (c>Eg|}ww| Ưx>h M4:y Sr@Chx%wq+X!̌BYe$P;S|jρh'**&(@bwϩsߗ:62Q! j%0Դ{Ew?Lќ[#4k~\!@F#yK7pbpX$ASߠ.SS+ ]V0E[R`>(>QcWF%+cP/!~d*iR %TE< ./ϱ3YЅRhZ2Ek ' `j)s &-qsȦ\4"lR*"EE'MK7'1gאOϣDz?fU _r[+t-"_7K=6i RLKW"ަO2_j~,3@;8e:E(w P~]@v*@";TU-T|P tkD4ɷGX.G%Kx]\(5Fvx*SOmd=`c'{fpo gZ]Z[s4al>r|CH-DE)[ > ^Y@ EGBOIiiL,#XN cFI\FTa*;r0C˓dAE/1M!i/OOGIܢ,r4#Gd{A;"9S%R Aؘ4wV4=#Xҵdr\ǴXJl ;>>]@CKw,]) @S l/ưCKȉRDy.͍R7"-T D,'aOɾX]l&!X~0P0|tz)9~a=">s 9ҋEZ@~|~B*@& gv c,8"f,C e+G#sy󊶜Y_J.#ž p]ҜF˸/l *=p+SU#FRt& ;E}冤5/A'vg#w]q|"f<.ܾ(}`:hl^;JQ!.~dՠXA6DYQ9f0$D$3eу"^Ik)/=6N6d^:uM`Р  s@ $$  )3}_@|L:p{h,3]@Z,zpɹ8,K ^%?q ,o ,-Y1¿cТiaq<O g)vfWqzKXl,M4.KMfx:a!6Qn w ;NI6 1asq*"sq|.1gהaMa:!6w Tж;뛹.g\/85f'scb'cgvNyn6Y-LHtΘ+\24S6۬ zϮu{x ?5.EOz{Fbsym>k@|N:Rh?H$ 7qO@n@&rr kAnk@&pj)L{&n9^ 2, AF\1cV`uoYmrb1y\_$ƙ1$*)NNư,_2G?(i$X;i#fT72E"S$7E+"5BaPN)ErGjr"`$7(+o̩ M 6FsjsnN*\)tח56*jCH}E++9 IE,:![\HEM8۠b6>5iI׬|c}S"T#\v>HmF~YA?~,NuMM@j6JϤ-ViS+K'gt&PDmNNP@i'ڗ|Z@v]i7ڝ4z`Gt8ERz#@6ZF+h% ZKiB#΢\LEһ!j2 }>G_/%/ ZQz'#=K/ :T]ѻWn=CT}>X/҇#Qh}>A^aAow-zP@_/џЗO Co]PKiR(-certifi/ebin/certifi.appm1k0 q@KnibJЌ&l#$$Ɔ(TH}V9* '7{`cUoF&tk֒U /dacq(SFRQ_xyEoP*F3d)UXrf,4d(^ 8۷mb^K>,#L^Cs$N,iH-byO@PƯbw8SpOY >HϓPKiR<*acertifi/ebin/certifi.beamLPO>8#%hAAn))nN>?k=ٙ]E-@VAFC 󵕻k nejirwwq{ڽ+;;Ya-A.vΘ; ([_BhD &BςcB"pYt]ƄBu,鿝s,3rE#*Jpt, FЄJǁak޿E8 ? h߿M2ye4]MsOT:]1CAO[8%&20F.-%k`xLCI!d)st t1L pz4\`7X]5@t߷+eE/*zmn"X @GF"o jh#ח)ΦZ!yf'.?A9&>jAjvȅZ.vŵ6*,,,C,7 |Cjagb?DZNx(q)3OHA``$ N1<2 "ԖQ,Mi_3mQ18ãgGܾL(מylf&ђPpT[ĮD[nG* &|)O0zhA.=@rX֓6.x%x@6&_|%1UՈ U}Qy0[տGHhMK̋u6s.^U1'g!BIH§U*,uT^j鷘F7E4Rv;_tT5537^ahtK*)MbUq`vjuQЇF+aBpil⧶OE [CJ~b? 溡&|5EF60%|'xNĈc݀~bT+`+z`55`>ӕCל)t=K0 CMf̜* X3ߒ]|nNW-ѓi0$|7zL𩸗{ApG'\WZMNYՇ[?ٮ.0a>wvݕ'Z*0(95P„B)yU`Zit|3N>%^ogdg8ϬcH:#HR $ -cL KkP0b7]AW4C6<ԡGD[!< ^fNmfuBl ۟ˣ玅@'=waۻ^ u&Cf;mb^ʢvwx!97kzԷa><{ΎdyqXl#)w[}хX3}uu?T T(qc2/?tJGd*tWŌx?uAy>mOC<|r+f66pZ ݓ/LЀ`U s>]4cTQ6% ; ģY4'PM(ӏ團|['C @ \*te\6 ;h *54߮piHhU>\-X'!@ m@\-Lr iC>IwS>wz jm& Z4MW^7El_mTG~d$TؼR 6_2Fܳ"'-&{p!*y𨔲ҥ,~yu~i*z^*S-< r/„iֿX_`:?VvIc*{C{3ڕ[W鯏W? "K$lwhDoQ{_坑 =jΜЩ3grcbHrԃ%JjSQ_µUby~Sa A3 }/H@]aHBs-U&-qDQ$/>+ "1/j:QRarW{ObXd{e))ŸӝNZcx ^)bGzf.$A$LkBF$מאFg^Ⱝ\gJ%ް2bFf=#C%mJɂ\k@еm~<|f1ӕ5' De$`ݘN5nnBz ҿfEG=!$}_.sY7=u d4IY$^䌵uVP:dEXr6`aoѵɿR>nܒOFb,Nj-#^Kq>oLց}F sS=-d1euJ՛;[;:訷d1㍚fn%o`wlPੋiQ҆eXCc16SJ) fδ?RhG_=~E ^)wbu֔],O4 cdq _ sub4))\+n{t#a$rOlJoƾn*y( 'o5Fŝ4gj+Fz4Z?KObSMBvFV6p6*Z{HԁZNZ1QH9#yöY{sFv|BXFmL&%-)g qг.w꒼4q!3ᮤx  Vt(bk ܡ$hEt,OQ`#NT3uם _A_x@\HpFKzPj#S/A,$@ "27 ѵ͍^ Jr6|R)lVg.;&_!^ .먆Db7oPNX"Vӱ f*J?X>#c%ΞJczIz 2p68(BT79k;NIjNj)l`^SL niobԩ"45OPg+L14S`-iC[)V Bo\ t޾\z/zvyۤf2A<bfOR}N08ia6'c~[^j8,ٵ HXd`KEI~r/QtgV-1 qVGQqIR T>kx~uq,G鳫*P}|!916:zr0ȸa? (3Ilgy8TcA%&=xgwN4Bϛ/&o#v~i wRF5J7Rƨ4nAJ1~u |{uΜߐ(^8!5sHjȷ|#f`dX1 ]l)H)Q P]}a}BtƅTE$^#!~Pm'vXϝ+vUH,Z}lϭZ,K7̟E%D `++gG_q?6]msN!QyOYrʀ6 84?yDbGA](>G=Пkʙ *UcuHKAs'¶Ps(pTiBŽ)zuuIH!x3 |௮Q`k)spnsG8) YT&|njt? zN 'NϬDOz93 #LCqHY*r1>htqH#&pJ<>>bH]Oiϒ@hW ᵺloM\ug9Q̻DeCqɾpg,U{i'cN3a@\g l$҆Bgc^?ID %Ur-oTLr/-(XUn'ጱZTX{^w~)AbUZhJy<'/ضd|kxnW3!p6iݔߝD[Kb $5d{}`¦Wfrv'WҪ:T5*\mSzb;S5.o>ڏs5kڪU@|ˣq-\t_I\3J.o݇F38(nΪ)SGsiR-w%ɍS(úlֱic+;wi|Xa<; v^g_\ (KY.(5Vr^f"[vڔ*VЇGZjȆC&*6Lu#={Nbwm>kU5P[3.ꨡewuہuWÒ~͏~.gtBr2e3SpQ1k5W>Lhg>.X2r0+t9ݪ)n>ȋcp;~\@rK}Њ-jYӭWcCeЌ1)e41/V&[!%Os~( 'LW݄D t"w5k.գϹX(&hp%+Q͛]~GIStɁ:hI܏]ٿ%3f{@4NѫS(E!bkxGM qK h{R{xAEI=_7q6IX ){7)Oj1K!"RP'Ql<4U8#&TӀ cxJ $XW:-3oifҭVK3t_Yc9Z"^^eUcn+jSaҙt=a[ 3Fr_L&o[2X)sO5n_;Wp %K}C5Hf}-,6| B-3mT͹A=yZ {"^!6,{[ Su,nzRmqhˉ,Ntvo>Y/k~K3xKWIXޤN8؇sOk>nvgN9 X%#WA\ rEqg zN:(Akݹ ASyZ"I}ڗStL^HEٖf^+R?U>1pCx?׉B[pZ TE ϱ:1b{֛Zj.CoaOnz 4z}`p1&Tk?kk] \ɶjyp]%3W$1tQ}:7 TJ5%do㰥QGzɍXFUb?0ڗ_hĴ JW9X\ Q޾*LZa^YO琈ZA+{&(unN§F, }0Ej L RקQ|~cb[I- 5 T>=x%7}TccGӟƍ/5?u__5OѠ7SI3 gҠlܠ]b;r|ռ\U$V[xWy>ZQ"@Aj;Ν~id/ԯ:LtR%μÜ3#] < ycQ+EVK@Ğߩ |",piL9x>R$gBfen0aW7wm=$TDQ2=g(_W.3CsCJʴP#򑐣ufM!>g CACeLVt_PA'r»H"B-U'`<LgLAA\A|xxXXjc9/ĀЄXlՐ Kl, +=Pw."yYvehBN+Br]稛0`{o%zę=K eeSyށPOumB2্"J%C~֧#ODmPYN%hG{SVNxVAjde^ NQnS?x}Ylj^[q_@]_ȞP *v⬪:w&t)A N]n9RZDﶦyRhx {H`_pc=/ :u"dsP3ƈ=@QC|F1q( 5v7gوeN=H* VU6-zU 2PrfNW a\F]W#E=Qh򞐞Giy>nZѿF#8ɖ=У qFC&/WsiҔMuC/RUX4ΜZGRT$ !r)?b0;ʜ%ʔ=CWu~]RV2"xwp +~Tcʹ0&lA,2Eo^&/%-/NnC|v:Of7JV134.r*[,|4+~+y#bfJ@aNfD2PhU es}b1|`fGkH35A_ehN,^M[/c8dn;.5y'#oXlp' 3zt<^S*\ q.,8w ܑgm(%9/@lG3f@EtW=_+~ps+ѡ\hm\#Iܻ5뵄a!a揰fs[`Q~oq* }S>\ZK#7xɗ>_ ^n&x|!4`pBNA7 6:Ǔ ,pogaC)?`,3)7f~H"1UZ 2e'kmySEq:c,D4%~C| N] QLj$61?a^P=0pPfߞjU~] U, q>a-ÓScbg`|2)ߊ(@LY5,pv-`r;1'D LcU hMl;2Kl,zĂS7:&LVD͗ <:{pc+@r8\m0{ʨ󺞔PrY\EYI/=?r~PEdh^< @ ٰ҉`2*+4>3E䝅:'eO`SЫu{k_akH= &*Scܓa=w!'86NC zW3%_ҚΜb1UbWb2Wq><_D*A+ )a4yL&&g+]a2#_M \U%z Ta^v;&WTҬF{Hb|# ! C1B/AOUū3 2|)u6+-\ ,ܞ xL 3 6O)\,jg7kA~.8pnx/Q<5S/ʮ I膔lid4f4ؗ:אRʇܖ+i۹*lͳHe)m7>ix,tScH24,~ 8A* 1X `L˗1Jlr~u0{z3ɝajƬ%z+3 ꗏ%Mm+[إi9;,ַf#fi޿iBtkxY%mL.:+sDzםMdsh-d nm&bC25sʛ7bfg'B@cF[6`h0* `r/_Q=-!NlKPFeƥWzùbԾ̻V nq&O ޷@D~#k8"-@|Qn&B}Ёy1-';tv̒[k2*; 1bX}҅g@NӴ`Hl v8[ý lixNÞ P -N:Ne^FNXNY*UOxD#b~ VkjQ#PMÈ4ψK}'~ ui΃)gqƼr;pˎ")Gj?s+B@Ny ҍڜ:7Hra=<0 e\=ׄJ0=6gW/(иB@O|&"hm\y#1%*S6*_.v z4L;[z1AZMߣP{-↑ަA,JH& D~彪,g4\A уcne\r/%%oǎ'{ZE56KRéyC}֦ )КI4` {ޅUjz7ӂ֍=2.=hTKƺ#ޫ5vEa9 ͨͭUMj*G*S⋁6DU#% 8\.72dg I1#D &URI8ƸC\QE{v̾:ԭoӿ Ow&J'݄xeW8E5">.zh2s!w;d]Qbc7/ga}U6Z<2~U?̦d+RlsSOTM~DWP'\r湭⑏N@#87 j/nsfX^:oO:sٸ~_oWo%GD SCeLOFK Z|}qt;;[%|MW7+E=zMz%}8[wr%fxNQ$ fj?`_ ~1M|~9Rn$npHR{1;6)@/*[E\@#**JԼfq3 -2@Cl(Q6>LM/n)B.F4f %U9Lj\BU !`=d!-Lr3!XJ[iDD%|tS:&5E5g hmk@[$.oTBo!;}ʄ#<1ЋSk.ꎔ?ED |&jccbiD xFvNfOA)ߖ@QaE PwwF'}`4L\.)qۡ7,j1R2$}U(:OcnWeǑE<ho16gnNv/_Ry3_x{?r PݶKrDnD],z7PlGϬ>Td>}FSǼ`C}U՜ x e`^gWJ?FjRz>?1wIH$, *Jwi@)24yH?Jd@/;/2y;ټaa4c*OWX`ԟȼZ7$5o*% CWXF{1^8!kΦ-:Bim1{ o|F@ 1vZMu{:X1I;ZOdu ?~`\ЀynF*eI@^0Ȯ֯"GQjR_*p1t3.1sCBhX_>cxّ\25•gw scq9[3GL!ሢxi 2/yFb콬]66s.ASζYS֐COև^wLJDgǭth[k~yW #֓f\9X yKtK]Oc픹NqԌMaP}NPјBɫ* Fg\d#ޭli=DyIk9p{9^-NG2ñVYXT伙[U*A;pHR"ݹ-Z^1TI:p`{O.IPz KXUbw0L=CUߛKzU0l Ykje$ED?V sh/ܚh"g04k )*\d ֻo:¸*Z-^_9PZː~^۪ zZh88tVôRNs꠩+0I%2aݴW4՛}.t<)[k/f} V2tU:"h+ߊ5bF$vi nB\!8׬ɨCRq3r'T (b~@p}Ӓ_98vrlU Md;w~rp"H QWτ ޥIV =,dNQ4dݒ`8CV8 &gnF@N.j |9d#$0"GDz. H"Uۣ=>!rH|wI¹ȅG0rn1 ٬̥櫝ߦ`$䔥AZ<)Wi4 >FUHmb05>˭_Z6\&ҡJ\i/Wiϗ7=-;3׌ w8!pϣS=|'sX`y4ӵm0'Hmwwit*2Ahd}?};?`i?Xb a9i\Kyq^f}uqu9]RR^5E?^uOqi;7% oHȇ >̥x~7-ҫ! /Ĵ!S׏e)y:z%Ð2vNwul:eDtg[phKيJLߎB+gud <e^d[EPܦG~իntoT eMy:þla,Œ_"czRXGYCJU+vm6hx|nSJkjm+,>LK$l]OO۩O1IeNAFKFk?eyU}c:Fa&kTrn&Rx̩N |Uh.2Cٰ'wٕbyƯHhvZqLǜd+sF.h@@^EDU Mȋ}b|#*IMC/&QWξ<|s~?_̆Bf5+lj1zc!xXD#D|tX,H~Sm6t =B,  ДEǒsYq;p;cH6:)//^>>v:d ~4RŜ`CmD@~9B~DƽȼgG Y_4HLаopr3e;f7mu NaGrT8bv,~ag}LwU8F`Q=c^8Go@N9|Z-eڅ.$ÔA2IXHXBf 뚩+q4V N(mWzuפ>k :9yIXNvx9m1EۋWݱ|5K՘eBM 4v@<׋%a`sbo/B,wrZ0k=efk0X 90Vy> 0Ԗ3,Ѡϓ9r/'k*6@;>6V1{ m J|-{\ZOm~N2THUqE#I#@LV\QIPZo|o(*ۖ6 oEv&&twN$'ދhn&dU"㍅@?%2'l`t$]w80+I zc*E+Q ѼwiU:?2mU!O/Vѕ97r4U^+gO=EJg4S5`f@!^FWۿ]JJRuSl1nL*RVe*Ej.k ./]g{ ߥ"ǷGh-+Axԏ+HlAX&]귭y߇OXۢ {*A(fovne wݹ/Njai|kz5cm{+-zC&zFш$>hIsCz -˜ u1G0nrVnxPWt;x&:m݊i$}gMDOTt;Z<‘ Q|'fbfZͱYq=.j\rz@}n#KjkR艝5]pzQ^{b3+"(ǯz/gba,A8nNr*a֬ajͫ 8? ,RD`#:ere~%^$Wrt]38@c;2DqSI06@p9 T+KvFzRʨ}xM#ݒ@r?Z(3[?טillhR?/LrK8ܝ9پ^&R~w #`1f/rewE]f5<@B'0uڕg:F֑cHx> %&hG`p9/ٕVAt 2HLLnC29)S5Iٚ.n)~{9q 'd|:*mL QIWCJ16.BcU{%Rw6k+#8/_x�wVdZ|BB _sv6ۏCA]dYm>ް}OvfZ*ut>Yq,}l¬~ZL=hMͰL8O"mxʚRA>TR* }`BFm*np zb3 8fw*,K `.E-G>[:a9ojx%iTZ_~vLov&8T T_;7uuQ$({6$!-$?_҃}1+.nԸ:wWrd\c '?i{.wCPkb'MQ^ vpO(8p%#_U*fX3<^ `Gxi'?~J]XPG4 o/fF'wAʜ|s!&NcN/L> ).5\`\&H"ÀyRq=|e$k^aqho [˓>%#E viI۲KQ3WLx(t/mouapY%OAՊͭJEnU,剉+ڲn] ]-ݝHq̿?}}{޵WlH`ZsRb.XJ&Ʃ Qk"η/վ"6 ,_M S O}~HZAf27tSZo%WeFH" xW$>NW ͙1_|:w^ ȟmT2>T&#]}9 FӚ?m}S+Ƃ˭j$*`כ']GF S"VA|i[AFQ@sk$Cwc#sZlKUs+duӍbsR4}Nh_-û*H‚[  9o Ґ05-8V qfaO֤ViJLv%Xa &m4E/JW-qXQxFFfB Uo O0R}Gyc?02EN4lX9ǩQbLt#Ѱ>}ipCsbΌ &ZAd =<3|OZ I[Rkjz9Aőo3.ZT#CI .v %b\ >$_ÊPb1fOI<9ǭyWÑ[ׇ /W$ jC(ܹޯYeK:sf]T[w_dW.Fӽ}ec[R`|'j]Y8^חߖv=Gh eZD;_zET̨F 5̓+<7 :iM&<{="mc6ʢqg[>OGE2BwAQwq>V-v.x{YqzehzvB$QV]YIzry qPƢİ%,cbet҂gugڜc}ltL zKȤݢ͚VbuO4 ' zO.eMg=dm3$u[CL}||d@"$dpdcawY9suCEV/' 18 ZKm aqr{*k[" {(.gӆD&+^+8#q0lo0]fӬ_,v\ㆰ <.l7pKb(iNosl8&ňΆMA1Lފ{ Fꃗ+ xKP7["i﷝*kP sQ@}Loθg(ψ{PU0]\kPkhWd%GrU* )ܐEY(4<<(!(!~,EC;7gOH*GIXX t{Or.vv9j@Z4 _zo5A`QP75=-#==Ag{ Цcg}2Ew?ˆ\A,tt_?#)5;b`CͲ(xUm~we;ûSV SKln5b"d=غ\7cd/yYFR WZ{d=`뛺`PT=qػ,^jz !Oøӽۛ_u=Bٙu!M|r0d'/ٖ 'Iev/QW8!xCs ѕuV(DcrۯUYb*4)_vNϹ_ZvXRLGhu5!i;863w -xL-Qӳx8T[)A}HN I$^lc_IzUޱTaWOy Gmԯ}zׄ:SsXg QœԚ>r$(;gHyZ.^*|$orxLlr&΀G#^|^}r} '5`,st {X=޵<;mB.9̌)'SpX [I=031SX$zC8/t*_.OEGs2K>}dTOJp2i(n>qS*LP=cR]>b &阽;Oq=TaJzGSKERz\ 5P;M㰕JzHRERRчQ±S&'"WŢWNGŚfܡ4J1}g;Ubt}H.|_T gMRw2c/KZVW ^ 6DNCOu޺=?p2-%FAwa!SC;AՉ7IQ7 eK ,x$]N) ^B<*rv4a( o^oyZ:񜂆мq[G9^,@xTI+Z\tC1_V:"Z2+ܜ㵌hNJsOoV7Ì=Zey3 5VS&f:Bh_@y 1:29.aݙn378xnL|q{:&QPПYe<2.6D`D`Rq,-B\@j! ^}AQި䑦v@Ҿ $j|a:-ʦ$Ve_yz{355?|!ݻG }O5XHp70dZ=3(:n0g89xd%cTކwCSZV XJ/s"-ʓEN$]:>hTS@hyq2b "vS g *#䪵<(k;&I-[?=8*uϿQ,؏gsYhxFP4EUU4`ٚ4BzO*{VPJ#)e!y9jjc!2M'f:ËI-'`Q- W-?xQWRӄYrpVEoXvpX$B }ڔ?Z^ ? #\];)K'u"<g6`d< ay 4em&ɫNVHSL>ި7s|1zH92ЏA%_[fT niq+R,o} OEK˯J.fJ d1$ 2EܨJr3x rC=iK\Ȼ}bza1#oɻ¢x9rhRAUͣ\zTȽo߽yc~&jH?a 1ƞTuMo_(J~QV kEF$ObRLk'aF݀La9΀D'&gY; ?{7 n'͕ /ljY=JpR6y[39 o`TU gcgobȒl,!/"'FGEK=hPLwdcbe 053p5y巃?-b2//PZpt#P!̻>܋}U;9X+ +y@ WXkŹ)t~( #oUi$YB Q.9lc 9_ԭnـ0IN 򺇩;s|LEcaZé٧Cir!#bmUˀWiq3RP.lwJ jޭ\ߝ65w]6OhJhe*Nv''xx-/;%[|Wrᄎ^qOj&Őxm^ wq7Ta73‡kU_:kḱ~J>v(j'wA3ϜnkB'شqshCݼ?.(d0> W+˿ADF//wSiKw!,Rrm!nbnBbzT6xc@;zRHn,5{sI(w;i+tZ3dX7eg9\zuxϳY\S3K$s)7i_,6*bȑr~GT8Ű'u"k(ac8RIr)& U`:= I8^et̆<); 'LJnd]|aSwa*v7RU"QO_v[ysp$a B5Tr]Ųioi h _.Z~E[dc{=rV Kї.Z{(azi'96G%>|c LPΗjM!,oFϧK2DE]dBʹe%,n!Laf>Zv*RA>͋\F9ΪI1K pDQLkr)UeYH+.'^:*}29VeϣL=^u O@"(~^C yCs;'[ "$\@" Hm8D/p/|V3fǛ9)xl]rKߔ7b .I19_:2f (! A0l,HzFtR٠qP@nI9C u"rF~`7K\Uyhi圯[d#<}W~]]hrgsbg稼qG-z W&dV 7E%!EVtpRZz<(_9JjJ؝ xj/uz8DM_1;V!3bGı'_O2Ӟ>W~}ǰ)^oPWy*Ņ^pι+9[gI{@ pͪaEvwz6*ôȪ>g[^Yϡ7ͺMkm7Ѯ!Ob)5 `@\J}!Sb\:u P!ƚth+ˠՖV/_|P`O\W~NI@ީ:cWL9w%3D"hY"&Fr"t\7_OJl9Ç}!%V A92$vfi//o^KVk^S NQ`V,4e/_uo7.UePS$(M@C=8Lz}UPvTiU)K] FH'r iA"r!b?~7F3 ԧrÜ ?V `G9xsv|?g2R|8C7?*']BS܉Y>:}T SG$y ! I[à֎CHJKGF?&*l'm {Iۻ[L9&_(i_=) pan|>YsA7>Y qt#+NF޽s]5ʵ\Z*u9@uīaHiV@6ɢF 5se>f 6~z0CKNeo4DxѡbdzB/DZ/p]]W}IG%vaiR&j5Cٍ#bH0 xuxssG[H6x G ] +k"E/*QLWET|W:0\';u\z0ARvh`gWh|)5)Yv@Jwu-N;+ԝ2=eKO.c9BTeմ߿;ʽ䍷%(m2l3j+`NM6L$?qC(#@f] 1UV?3}D$#VPct6mY{#BF9fH ~GT˗_dkĆ1actq$r?sMءa|"=SuzosVO}*J'irAI CЄ[QthzQ@8L]'[ J\"6t7F,z A$@c cQDy-z3zds\QdhKGPQxF1IZRXƼ/>^o;nAGqΦ|ڏ~1Q\!#:>yIX05hN+' r.N,gEi: 5 ǀ$ ߉ Ku:AR'6vnv3j0A'7m REjo?+QH}XMN俶1qͷvkN?w%˷Ъ}~Z>= *5Hvkz-16;xYdJ04ZHi/ys;}HN4 Xa рD)qthU2$ -\]7#26\ӇQ@ PC[|9{F5;jRŎ [ hqI`[ҘNr0B~.4c,\pR (IХ),KPԫ)ݽQ /}7uTU֯&,K,/a_sN-2i>8ȓ^\aO.hznk$t]y$K?ݻ,z> /Rnp˴.״}vՔE;DD ~+kP(Ԝ z„ Ntq!Z"^{^Q#Z zdebPv硵UOXlfX,>c*V|BHT{0Ci8u*Gx<J4'8WGf$/sani!{<)"~{%!ζ;Jj)ݒgP3˓phHQǒ !,YJT4=m;ȸU{ Tsyqk e,82m ;r8 y^&%fkoE@}Uv3 5j6ZZu>IoݵCrr|?beO=^{\&C5yЂcc+i>z>W X(҇#$.hA[]$2g RV#j-͍gv^A=绩\\B  DzϦ 乢XC8^ǶMv)[!*i8ޗG~^Q džco8pHݹ{\?zESWE0)>o' bY颣[ç.f+ G33R! , ?5c͌CZb; \|G#h]wS mi~싼^JR`iu(qpM!7,a i5+^d)oJ7%,&+xIsI÷ 2o\̅&_V%Xz$RL@ϕW 訍.p;P;,2sݸi=,谮&ԗhL;L2|LF,Kkuk2"|7JP/m/ZwNֲ2I Ij$C 6/ 8uU`Ym^>G)g7Cn )j>=n?@e &oGO}19@_&I>RF,l:+᷶ҎeWTGĉvFmԖX\LUߘ@{A{ KWC$>KkڀkI&"-hE5DW(#I=ǏEۗreψ|3hi:<[2>$Ǖ=+G%v4C CNbMH[{OE ExG `WwD'Q"o/x0o99A5ˣ]8 U,؞`T {JE# NzP7;w玤\+yK{vv4yASS`/BR(/嚑%a7D&hs#گyODZ; ٳ9ud```>[5Q&.=lQRN"ôi2ҝdxk\oFv]ŀM5&ٖ+O3~vZVʹ'Eh|Yը0v7:4"Da˸萯'KfP=|w`$ɒm‹5rHa5Pú\h%$u59=\/%qz~q"1QY)1'gFu@B`;9"!l9M|5U -p,hگ ) LzSW^\6K(p$ľsϭ YIFP(נ$ ~l ij^9181öW 6*#?=3ؼ4a;ŠfՆPWơB^AYDwYBsҼ'C5 =Xl9xee;}b. 9rE-T()4E9]sX= ~7xxN'Q_TﯩN[{1XYB\r!bH#օ#faMDO%A0(q])D+R523 kBX7)z"XABj!VQ苔ȶNVcvzaG(yBHGaCh  +fWfTUI7;cՁB%_C$SD:QNQci;'L3ᔖʡNk+h3,Ҟ[i(-+vA`xbL$.^/BnmPg$K}j(v{X8fXhsfFOxK\Q;D&x*H>B1aV0W̍ yKGb>oQT"lܘ*v)Pnui%!}i-^FKM9hS=~85"*EKr[>}"~W6= @,^o Ș?:~ 4H;Yٚ}.?bz2̴ OO;1 ?\9_nMz`&8>2j)"L;Im;ک7CL08L\6Z4ipG+m҇Ķ.T`E *P~U+Io͜PJ94u6|uVr s5N> %;}ުdyJ`<$IR~ ,/S3x;|va\՚pϸa[Vqjm/z<@ío[m״;F!H=~Vmxs0eQ 5xQҵ|&%˞ jg˹>=1> LfTB zk˽[.iDŽ7#NF|4_~y_ ņPPw(͖g|=qvo* m6v[yX'U;un;_RJə!u#D-w[YOke]DKGV~a탈"%jRT e7u %I02;wE!̅?uÇ`c 7_9y$r l!&r-k u/* (BI L8{@7 O %"ęoʼn5R1}م?SCӵP zPː^^ ԆVwf7keMy`6Ob\8WdAj va)wbY*1]SpSR2ynrSm3lWCIlWw^&"5PyxZ6 8 Ġfg9p 1.%v ΦTtQX2:t[e#$9lŇj7v"M M&+&t~vcz̃y~x, RSL-pm4嗎%cS-cG0ʷ]kp5 -ONPGX};D@a~nV>_[2n`pluLt"Oບ>ާ]G& *G gHhYyGnWϼu҅b5C: }s05n@Na2.)Sr2r QSȢ0QN"`c**X>5Riy5MDŽfUUS(ܔ,ZPʦơv;1CJ(z % ? Zºahږ#8DV#>m\Tvͩ֜%iPcbbBA@ +Ft.:M}|@=|ϬHJ+֥vqϯ5v *j؎nQS*&7HV,ވY!^qYQ<5ccFp!5e HwA?har(64ݔu ג"A$toMEӎ/^,pй|9?(#E`a[t 0ڪVש垿#/ 'G:JXѷsztVɖ$IR6FP b^2SK=Ū@Tisk(Xjw7A<北w|!ٮJF3ɸ=ܲF9ѽ{틔5sdb{un0+Z89gܩ@`6o2/hIFW5{.8rQ$ G f?  P( SΜZE9n\%( >yZ,;1ĒfЛrE/@&Z嫳eu'f鬰uM^cD%-h9uGp (r,c&x0zwK4!?Dя6Q;|^P4сX?RjXX<3"~sx=-{=-ZZhϬi A*%X*("^b_NJ N[+HDll_5)/{$O.&"'3R Qۮl r^lw_dM~9lK|u'%.;!Zd9j^g)O0AG&WWp= UDS3 %F!ta+ڞ%e[gZʛ}+Of F;8/*&UVBj#]YT9WFt_Sb'D쎟aVdaZ4N.:^xԫKiP5̆`O}N+ HUP"S[z)>$HDz2TLeۀݺ-TBJkZLؓ>m:(v*_TO[,Io)rXUDrmתg(K2ct/!ܪ08ݓpY ?jP*Q;qxڏܬ>nBVFEhh<5s~Hx{RYTw#59`bohN1SEhؿÖud)B\9Xuڅu jָ+ϡK?S_#sѧ!>΋3SОŨbaڕy`{fwg+D) |=٦9H!fq.AeIIA EQ AeaXA!F< 6d sOA$^JQڊ9J\b539UlW4('zY&8EPXD\~JZc'Օ3vgbdNal>2YhƞP7p\o \uGhZx)62e#f|:}Ϛ(b4eNdI R:NblG,+56o"[vϰfjtsphf.!/myEA2F&]* y-`]yj 54 Iݍ{6?x4BQR5 L6/ʴ-a|xrߍwEWwTzBOz$-ޭ C@} Z?MPD,c(ȸrXǩ- i t̩ l-잌=iIȞf `he ; -wD\ (=ⷂL "X7!4Z𹭙+hc׷9:W CC'$κFw%ٳ9J;xs7 sdpu'j@6b.[߼Si>D~_,+2/U&Zkڊ7 $ds|vS

OHl5g,R~ZPG^Bʅ Z2hiLOJZo=z&&oAzl2=5Y;~0fkɻ 0zt$m}\ZtIf%l$OlC/|GLqʻSZHri?i|rƫ͙ `(XwWj9ޱlwdVXNBGVqVlk:Q I$:I%Yh Kt3#G,|a5? z&}JeR"I/W،Z&]Q#Wi1l͉qjT{^W\bED 6?暫&1~jTw%Ө!M]!?QuIY9@OŊOd,,p,Q1mO@Y8z 6o.(1D*5 'uޫm\-*.8X c{:| x5vQ[&l+%;SsӒ .RiVZŸ9!v빎\_KƏpu> ~| MYXt  mzc21|;o@ A?I^ E ,/}QҒ_×v57?%i_:"@LU]L\-oz[]!Mr fr <#vHb|8NBj:ol$?\IYu;+$ *BΩ_o uΰTl+F3_S%0,%~RC5@ͦ\Sm| Fd6}6DbT/`ngb,n#a6sụgTk2U \Uq*sFٚnׄ_`ce G=M )Y1aBtn[8>ZSRp@᪽U|ES7{SaRU A~`%Uxﭸ|Iu͂2vEiᛓ5g &|&w)u:cbQ%*YB4Z%`G[ \^Kۺ=犳ZqZj}_n>l,xlm!'g %-BwPѬe;p >aIU󒪈Lqvuyg F,^e^]TEƅ4b HE,E8xyҙ 1;G`b8'$;>:K gm%9`*t*#ū)JbESg")c[/*;I/H? o;gO6kt^H+){%hɘ VZ $:tMM ۢ1!:;go| ^W^l&f8OV*IOCrcpK&/t ߧeo찢nkY`uh+NLD'ZSaC CLpi#)ke& qAmȅ1$͙ōI[\[1>cUn}ZKyWol"䨼J"h[YNy92NG{5Z50yPk(/kpu8]aSS+!xv$EO$+(!|QO7|BLę3V;!iy;{<]YNe6s&ci9+ Uqa{", d$^@@sBmXY-""3Ԁ.Bu*B lL]EFk]ѫqU VKzne+Co#96.LnŲ7$[=c=UwJ0FϙvDD 1eҌ0ɸ$a+VO j># .{u- Qo;sV>yv'݌i{1};>~d|%.׏ Tz nV ɷHQήށ U͛ R=ntgv\\(y2$ \jh1;gBOrF0;haˣ拹;biʟ~X(7|JC)'V~Ԝ*a{] ewsVjM]c$h,X8we}D55QqrM5GRHGѱ7ŗ4xicB_Xa?hXS6QecVMgU[b1i_&G+d I\!6swhrq m4^i4,v˒sKGQW~ (RKczexK'\O >d! =1qmD5i|TrDQم)7/I8X_/%w/R_V3/fDiՍT/V|ׂ)6e2f虂DTxbJ7/zE,)&$KsH 4Ỵ"Tfg:۝Kߑ+ ZX- ܊?hNx "˄ J`h|6Iڬ`1~n]0Md:?Pu:d_ }RHTGt[ȶ]MS-[^53,!4q- C|߁ϟ-pΤsp_~@znڹp7 RVʧm~L({2 (ӨmWo?D%? 'k젵,3N8TlP"4;8>S_\a2 -WZԥ=ѻamvD2t3?dqa\}N8p/eؒ"K_  Y?EBD*6 P @>/tGPCic^O=W?-6e1'ɟJu;/`!Ra,[ 7ifZ=DfQTC˿@Eh[zh|XPn(^Kd>1UD$s`#Ojlv_Dk[[*1|mvW#򄣇 G  M7-ٱX8os#}^ :QįWobP~4)noYM|^4!XH (<>ukX޾MR@L鯞J dJ⹂]0xR`[>Y.|.eVdQā 1wk<~ QPDwL6tS1jDuc;9 KrȦ|iؕ׮4ą,F|7uMܬ*z镼)N[OXIXo>t@Mx.V_v#!1:Ȭej᫣ +=TyO% +N޶YRey?w 欨/h0gj1ye!p*M;|NeW@F+¢JO?_Mqtv#թOZ3儗tm1Dž2-f0#U Ej/0kȻ*T 4P3{!RSd:b̔JtݬAKJ? Ǒ~z.'@5)H%Bg)͠3C^C (կ6QqC}w* `9e($ĵQL=UAV#H$΢saµ6x)a7.,N{p/'.k&[Ml^} )_tä Fp,3"C# /6W9}Ix^;Ԩ͓5f=WYvY16 T1;" ~UYfՅwMF [7MOEۨzw:L[݊0%zX6sdf˂]Xbe݃I`t{!ڡ exY C8Zo{x.> tVQ9`v\~ b+\5EIS H ,z+Wpx\I]vŹxCm]/>V.1]e3tqnȂv#0"U,J\ZܟI*"4- PN!64*uhd%gzh^[|bPH.C3Mtn .u+ӫф=Y)' @F(0-u5F2@gY,Uu<#/*Ɨև\*::ѵejލJhV+#-G]yU͖~$־FmWؗ (B'@x2F2#(< Ohim2LFU)g+OLxJ \ ʯ!K4'6s {2{rJ ʥ0h_hc㻠;].~40 ӎ]Yߤ/q3 =R|4ǂ !5c3pS튕D.`)8`7gR_]@Z*^Qg8A4Xœ~$~aZ;h63#^scWJA `)QDW L zd"~ˤ=ti<%BG{7Uh^מ܁Β J^kwnx0BRÅכ<@sOBGUt. +{u|\:TD'7?tW[ۿHoϺN\z&fL$ya-f)(ţ|$ @~Ȓ 'o&F }a5So1ig1J0| g -Zu1bDgdKMJh k@(q|E'%Haɚ*<SQHv4}[ĒGgO5o"t@ m#CØ+:`Amwmt>N|1B IEe\oY/Yl2D0XjSB3{Y0DH*?B߷[yZeF%=*Lj@cwç6Qƾ);SET&m8 A8kf WN<+sG\w-) )~oEq {_9Ȅ?5㿍H/ο||4bU),j}K5Mz%Rb[mאY>|,܎!^3mTxA* Z^.-bmd %j̉P`c(9W=e֥w5-cqB?pCRJG:l $jsWo>7.Gz/:% ƠmU7$idtQ 1wP~\{c2^]Lr`e-XZQ/Y_4K)=~ Ag2 z"z*"#|۪r.:mK@}Uo)+0BY\P+nh^e;Jh@wr,` gEv:d "ݻEFdYRluꂑ]&A\*L\6 pwgOcbӹVH1<;?NX2,z`D^TW#6xυY.{ջ~_?a*|a/1 p N<I};ii=ÚkV8P /"!]߰-!亻V/UUI@TVCn_gdv'iնɛDŢj%U}gFC_|ƈg9^ܢQdގՓ I^q©1Y4iYFk{BYD_o,5峅ټ4gr+ه($B\[rZFܑ*lrҮPރvSZyL!٬IΡk 'a)Bma;QOiUh7lMb-Í]Q\Ή  ]tQe.L/f,cVKpHX -yboO;ƈO @f#H.-.1P:9|ȗW</9enKHHKNǒ\Յt:P7awX4QHvгZwcDiF']&c0?mD NXnc.؃/Ts2z%=2{w~DFLGA7:0~7LZ!UaQTEVj`(޴o&i .]qm YZfB:ET”,CFEp^e\kD9(p"yG $'x:9IlR w-1YyC6#B/}n:U*l?!kI3WCQ-{вeE_\R~%tzfqnuhzˀWK/0Zop<ȣ|^TO z~ `Q+e艷%=/X5.U4D\KarKsR^a>zܜߵɶDP, l_pm?I&%}/d$`/5Hv=l"V']!FEwa2)6c;펮{V}G Bf4rje(x=S8oO~|IybNlFFD=ɗbʭk/]_~q,HP,ō).wFֻ &9.5i %q:Copc[f([:lUm8xf/OsN*g"cڂhzitрUCqӸ4򀴪gnbUDy8cw/6 [mJR8o\(lSK`.w)}4!+p@< `Nwf1ώ1a5O Ϋc "-1 N7 x~i6:`J𲝓(@Gջ)OT@b&[pe7.X,irC:ٲhU'N.Sm/$V᪰a{.QyGNZD'%qV[S?#:%*wJ)x85."ۙO^PI U y\NIp)]T7b8P2V:&vF:#zp==-=M6=UԤf(%JX|Ɗ%+o1s(VxE_+u5EzhK4_|}1{44VC{n](;UJ(4εN @6|>,)Xn:r`cFP+v,+@Ǣia6adԲ.T@ѩzKmXPb`!5 OPKמp3 61gl܌ [L.jHꡩ U3ƸU)ƫ fUo]7f@0.5c#D )n%JռFT?*ąO]{yyy{_s'=*&^h07Rq9-+o@@UNR #xx[uHd#|}Cn.Ft7WO4Oo,D^ p K gn\.% 0 xxRUc9b]OF ) `DˇgR6_ˉl#TŴ wl-ղ)Օ0p2ѭX6CxGN|rjCr49D\."U yB|t?rÒCz8&Nr=O)['mp A@YB\6 ߚuoxQCHD?ϩ&@i}}X)SpK1ؚQ3 u_=jD X<`5 ^mu68{z|oI&I,. si<(Oų|8O˺ S'ל~K "!}|jW5x7|>d*Ak A,2P9҂;ϝc+Dq&-uh[ 9tl ɋ*#UB?V528b#jkZx dj*'&ey&C\~^„Leڧ wNI| 8g_ns,0c)O|N3zl8 D όuⲆC5 ?*"Ryၚ|+8 f Ty}_i(#ex&=mrOUqcod-[_qw <4 nɳ^UK 'qΗ §<+Jۓ'蒂'LhT*ąD￙~aZe8s$L|lbguV֢){).~ t- = #?@@.m-H 6b'Bמ2-qmϩZ.u]2@#5.ѹȣR/#.$Yiҥ3!CR %B \O7vʎ _`&Fnwzh=[R;?ql b$N^U>Lls@jPl<-yxNk uͺ@qRb v'חYK.RR2pddhՎo&&HUT-WiI+kJnnI'`|H L3AJNrgJp.vԓl"( $s .DwͯhƙDńCY83DQA W5ʸoh! 4tFSEO E t.UN Khs1.@:ݛx{ ƹeGHJknbQ&.C7x}h a/z9t= ,l4OBo6'aB3aD ]:u@q:40>ՃVctaU0(o7J #ISٻ:栱 ¡j `8R]M:z 4NaؔDfJ ORDNw &x.WZ;Ahߘ`E>q p"-r.Ђ|ΥQ@-&c ߇=0nBIZxЇHP#]4 [ /N0$X.si^^kO_g`f9u֒,%Xc W)M;wTe>G؀С4H+o %9ubq:3OKj;!ԃ̈́/w.?EºOOi%wKlg.[zm~H5GȻ C(T*H 84KFGP=coBؤDݤu_2U'Uyayf3jcU"I-(9jCS]7EǰiwzE^ Pg}UCGr2T^{+_v4| A`@ !N#YwoB}_9(9h8&/('0Jo$2_ 5&?r wG0]<)yZb Ǿ\Tg3/sxga? AFU·>Jl]ʁiLό|럞m">"Y+A)';R'i{Jfٶݪ$3.OOgfAO9_c~$coFM![(?m怶It@!BƳyS"Nc"(k-S9t$D?[Z@@Za>/Ŷin~.0Po~+sa [rRX۽(yb9@P y\^j0u N5ݍwgX /O~{I竔%09+|_#[@btq/ gYyXw9W]̋lF[+.G=,?1DfѪHƩ"b*%~#.k#᫆FSw\ wakn*bQڊѽ]XjKmcszF7&f(Kt3df͝L73_ցƳ߷eH#,|81֒qPZQ*qQw񕿨` @- Ah!ՌY׭ O`k*dDEWNu P";fг '_q{y;+B:d2B4 'C |ܻ)>[Ռo v׵rֳWƀIb;}U+/(5g )FM`~$\NG*V$&M1{o2L)P,o%h}h}@~>KMSeRFɥ-(8kicK@(jy [|wBLM2L, ̾U25175w46q0K#%S8߾ ~l7ӱsė053p5 /32~[\ccV͙[g?ꯦ_[W<2W׳!fyz|lMb=\heVG;ĉzp 'Z߄GX`Jmq,2K9b܏L0L(V. R+9AKv*o<@լF.vl 8T4yV&,fDfxvOj h i _kƾ7m=Z[g"ER -v4e+9TaV!cY{(*T I,$K1AG/[wHx; `.9I^VM -;#BYDd-uṺBQ&d>hiU@~<ʠGTY7xN{`qIyet9--n)0Hdp?a`DV^ 9D2v2x])Rǩܯa<S'e}#LkG˼c{:[}y; 2X2ё4xQ(r.D.Lhi/˲dg9& i} hꏧrޮaztgGg=yӶ, ;.au>+GZ]WT hѢ0;X(nK=|5#"XLOf qs'^5h23ւa>:I\iz>1mKцc@Fitp&`ȵP kv .SglI<+>þ:6gcF$^Yla J4m=;F:~H("u\!CǞBs>im0t@ V F2-XƎ;} 56.1\â€rFMj&FmyE6e%Pu4(N !ݝ'Hp@܂"e7}뭳tWWuWڿ{jn3 M;7J0\Q*-mf *-ٳEVCHXږ ݋!B<Rx 0ٶ뵟-!xypGej6}UBIS֜:FAZTkCId4񆟹} Cf5A4R6޲,N—f~ܝM; EIa+Sz".#nz*ʟvnjʓ=y83L"kY2}_:{冖-NpKܣsIRRavʅ6u%Dq_=GhюteRB] FY%_ ]㴟tHf&1ąF)Ƌ<ݡX dspjq4Z*v1ҟR%-ȶ;%Q P7vuh> ep(s$i~f<Ϟz4̵ 3i >&ԇ{}QG7L'\d.ϯ,Ќn [(UL0.BaMmek%,) 7e5l`G;HV6=¨߭xn:Z lwRDQHKK#T _1T%r%T? J{;++/&5,(܀jƹI/, DVtlSGiXӾCZPĩqKצë\lTAW^X͆3 Q*.&\_h^wa']A=Z9 |W)rkj!Vv>0Bf iŏw?~]ޙ_ ;oa)7K, $kohv(BvAwVOSʻo ~l_ud \,B@!$0YO:Ȁ]@I&>?%B'hVfWԾŜĦu 0f3Sg4 $y)~]j c|v0ɠA#E͙f/xJS( 5\:׃@4X%w7M|N颣9jiVwk( .`~]uhU[so^&~Pn jLV16o;dbIɇy/?)c p=Jşj3 ʳW*"+6t/a51H0oJBwLLNaD:lbq9%\@W=At6iKb 'GN^-΁1Tp'Cj_|ZK>Zz&ɤO:o_&)C ;@M g>!q&**C/;Go'톮'BJqKvE;^G ^ ('U *\Lv  RCĢB#}g]6eIAI+<*pf+"Dj{X$qz 'OsoI{/T`P)tV`o1K5?r^*"ߛH K*鏌dI@O㷥Fy!%UaPF$hMl)=Krh'q]ݩ≥{Ov.9۲6?Mm=:CtIlUfp1} uy\*t>J@08:H'zFrR{f+' ͤP5=Sp}XMϤLAĚƖeպ d{`itnjn6 'P ,B͝ \LM-M m,l-\~jG ǶL F¿ 4쌜w"~c2IR}/`}$@_~{E @PK`BAO<*̟=*>F" bbb b܈ _1nF:# X ,x5L<kl]cv#Y;v<71`뜁-|v8p a؀Gb AT{o1sc֓+ ; 61p603b0w=tBy,@:O2OxR vclr";Ú{_AT7_T kx#G{gG{dKү^W'_lVW/d>4EkɄe0v> ӴҝBqA+U z2Hd/Y s(ce:[G ';ϘW+YPP8O~b?#\я./Yk#GCm=h*N<'ID{ )sWH> q@T1[kiF1 OeD-, IܠC /54B&ɻ#& #A[jZTS>]jmj?z3k\-i٧9%"@ĥ0AZſ7'J>vpAbݤ_L[2 VlV (WF'MoN OXTNK$tON$M/@y*^ZxT/S7#L.\~X=R}VN09܇eLR&5lfRp^1Yߜ ߉WEWҁ)SPtk<먄1r º .Y3%yA1䡈֔p3odL*|Ren )_Mz_Oik>I48#tK%f"~v/a]wfɬ}W PY~5@ebch(tlHbGVZ1O6t;z,/y99f0! $M{LC>#ٻyp+ ڞgP.? yLD A'h6=~\ S2S^4Т(ZkG/$x V~[dg+bw~.a5NdZ-&tk#,N `~^\U I9-[ٌVudQEʑα/'9>]7a⑞BKѻn' "T_\y -Q^a'wݿ;ZV;JXp}Y_2ZYn׷Pׂ „Yjh2sēS!2!Җkqs._;0-yAM|wzER|糆O9*J3I#I- 25 /| RuGH|vI$G@6H:ap5k7T=}D#fRCGN)X-3p:zQDn=|)3xxݛ^P'i=jqtρ,!]ɭߩMq sq00s-31089֦h6Cq#T7ە 3ɵ$A}L:˸=zc.Pqoft;nKfSU hhVv״=ȍc/^Fr6SrĒl*5q"J(|xy5YB EFd5FCx*{W;h/0qfk:[u+&.Jeئ(;َexw`,*{T O+:*vm>%'9'1f.rz0iFY3dBL)&ޮ [+I+َ f^'eG!nQK{ W)~sa\-  t?g'xԐ$ }`Tu =)w>@irh?AqQL]Oufz[Eq{IGdBZ<|iO-I .#>{4 ׵6ظFKz-V/Dh}ز;(:ת1!"b#h&;+PPn7)l\V9]av˫Z7zG#_uZڭI!0 #k_)cB |KJ5OP6\{Q'};O7<.a/]:SH4)v-(L8m+\Pa=~PhU/N/3T5 E(߄ 6{$74d}L 5ޔMP-% m`l7 _N_o:ߜb&M ^:oba`̷HS$)?:t+t&VN'_kK_42cAؠ|P8X9{":Dˀ޶.P) eσr8ˁW= ۔;bxJQWk_걅 uV>7[`Ns=~I2jӊGD$ҼQ/砖MQ۰|;pKQYch@1c̃&s"OY)-6i8&sAtM.s.׫|ĵݻ=24Sr؁F@:+'EV+(ÑӺp O%^s"=џzv- T!g/h\O)u/$Յckk HI5=-5*+ǀzP/'f{i~hbmlǭNmxsտxfj8"{M Q>YILT\Y P`x)gL*rXQzx(ëM9;m t!.24"gF:77fasf`N(+^`=ʣV@t5PD_&٭ D=g`|_D&STԟ5*/ӣO2+fߒбj+WJ_hlAA7{^ bqx1_P>ӦHФ,ss$0~7A^Ok/K_*Ec {?OԖ[ O>=Z5uX&+[j *lqdE_=\Ӊ,$Np2xy?lwbDZUؖD8n6c @c`c~A2F~mx6_hl| o 'y69wxQ(kkr( !rЧQfol|۾ذ}ric$z_C9KjCؼK#anpԃIͻFQJ |G+E Gj4G@]6Pp²j +Bo"s(ݰ8➩ɳbxxDJET/ V㎽H lp>WL=,PӄT"Bw߼ vGBR9B2yz?zIN6BE o1&9t=ZiU,rpƟF~!)U&w>H9 k[&RwG8U,,ll.v_?=:ap13#݇+h mbO.IN7xyX~ a-KO l1XG̮UY RIմc-@L}vx3@ &D4BQr5^$6T5.IzKܽ@:z샪ѭ3d%)W믩1TB"Oɢ dzSDCA)8ahZ/G"py%\P]{ Ya&XdAHTK]n ]%%KO!L,ynz,}̚8dhl?9"G>M6"U}mP5D3:XW%J^ j~PiB>.|*K;_uѐ+;'Ap\wӃ3/N!00 iz9uKɆ?f,Y< Am i0rz0y<׈ԴڬӮKU>+yrIv%BX%/6~UI@f*g@*@n,t CX yE^VՋN]tQdnj/UMwHxZ 템fj+S F%00h eo瑽$<'@)9t;$ql\nХ&$_̹4r*d^K{PݕgIylu"|?(HqY(~97DE^u&-Uv+6"m]`+(m3~SʧæcYZƧQxg݊9-OM8> Q6$=  +7gy{sWP|{?GEG*?lL!/jS%"Fic"2\΃ eI]:2q{VrZ_mS8)uАϮl̯uٜŚ 9'n(`y5Ү3-K}r®/CA3~hTXJK=ׁe9OfQRavn:W&gOa<`3D2J>ȅ9ǡ ,.W[cgِlj֒q3b֎ ۩8A@4I!*9@ⱚx fv&.@{;G3dڀ?j#<Z 1m% R@A$O %љ$.·/J`+G[\"d~*G>ƀnpW>O%;k(+Q.pRbj8[ 5Tũ39 SB#!gT %* V3WL6lf8tݳ+K\OCK8\#ES1L(>w-nq773yA2rd>P6?r@O?CZ)eR >`p}'Hf!3[# " 3e`k@dlG# ;"*y)q"%o9饨ixJg] -|-{K:u3Ǖ`K@n[ǥ`WGGGk"*#j8eҏ.2x7qJL^HDp`p03co@d@a=M8%\"H,ȉ@Edob`<}HYC 6 :kS x~."WFMF T_(, jMPPCM [N\G4CmcɆ+63E!ͥs0.}{G($/Qe>0 @ @11302 WCzD_quk~8qՉ}wpp~Q!`Xf;[MD{Q8⩈(Bi @'*St}Jbbظ*}`cn 73_Bs)(׵}ا\-b/3p=m8TxJLեg_ίy<=LbI|oF*jXGꙙxuΉ" }S, dfNò/\9Oz'zrge8= R;n(M_ }Wo;Uj|ZC>\\H'1ϵlOH%u+kDn5ܒcNi_>Nr \cIwWaCL _x \!q~_,whEX@SX=OZ#,IxA]s t@+IvI;y]Ǒ[[pk=F;,W|IBlؤkQ ͥ~Hu|4s]XچMwy0aǖm c'QY?g$|gr:'OU+{5]MpVuBGa'|Gƅ)414H%E:ѯXtK|(Ab8H@3q#?{G|8oOY-& V:6'o,p&H# x?7Fj@FZnS_!?gsDRZ}S$!BZ kEɌ22;Rv˰wnUÇlRE|z̏;ŨYYPY,  %M[nS<]* O;nՕ=֗߆:@iq=EhedzLf%,TS*}C?]=ho_w?i=(_qdvc"k;R5姁FpBZ$BSnä k󯺈硊JR8_ݯ]ӋGn :1~KmisC+k)dˢrg}`(څwnOwݡL-0]Z:zB=~3gIUN~G>A@ $0x_= }**j+ev=TgO)بkS>}R ib}ϕ1yE1=p HF9 wi]"US%Zͷq/gMl,c_컰ͲS4]\llMܼF ϲ(T fFPvb$+D+9z;jG%q>#˪9Zc p&gRxMٍF鵼kibœ+jл ]tTeg{ˑ=Y@yȔJ$ p_ ,=a hmElu6ځE{wBCUEo)i|,V D "N*'McNq3An%N+,_!8?9DC W^לˢ[WуA!MԄFIdb#^>V,LgE\y/Xߴ/FJK r#t;ٙ:JeXM5\ y9[]:]=݌1|A0oIMy`ӔJ<=],s*:CNJWWfβ͊H).ʋM)*^NCP{b53`R+U+ q &ѝ']XOGߢ܁A -6 0/{#ZxRܿR~Lr>\~t|.fVh /цYpھ &* /cce}l˞iIEeYv%>O$ktn[)S'dMp9=XNFZ{Rڌ' 6#&'L5-)ƒmx[DhN"wgcw[ƭೕ{yd0)P)R/ v?p~"8''3եf/Tu(<@uKzO#aR4oWW{%Y9 :oX昴GoPۼ[{$UΖڎUP[dme!8xkaղfs~JZp:̒>r $w;VC6&djtYtUAPh;ĸU ?]Tҳ!|q9ԫ0xX""JepN ;XG_iak+Q?$P/Ū`2X%[[(y$n%ęCmhsH]2fПgGCrYDRqw<%^ZZD߶qWyhN8eēZe4;_.'>yMO7eYIҚ`c2IfF&#(Z;&{ITe$nCOG<]Jq }^c>.ʼnE !4a{Rܰkh؍2aKcn>wYO}0:*Є W{*"): &ϿFhbxd}Uό&WR򹳤JczfE56yW$7yt{|):*ܢN}&YB1L?='5n6:x} .YhR^e\R!PfD9܇-zkUUt0gK6 y癒a!i8 Z+?4ȃ@cms`X$#I>ǀXkҤK@S)aM< D8ʮ#*77(J?k^߈寈W %0!h톓]9ަr:~!dk-8BPW -zo^y0ӷrhIuL\J"iܿUOkS y|=t,cX0GTE(=mg*b,E`Cy):-eFU'YGzAr5%jɯ &K{Ls=fs $. xkŏWt? GU~;M/6\P`x,Ն{ч+<~i6-s˰",p36.pԫhn(6pZS,y%GH?%٫-;@s jKˏ$|RV>Us0ZVx1O!XRަ6e ,+5V1>3ڇ-!H4Z;U M1R>W5ϽaSs So9f遬r7Hg"'e؋,xsH! /ͱ /  ] 'C][g&8Lcv>Mdi"kTCy29w -hAȸ#SL8T8t #!P2S'GcnP˃c>kH?_Ǩɗ}@?h+30jD 5/l]0"Ͻ3o|ql7%lٝ+2pX Vj}5 /?IaAe-XV|z~:VD;>6//-2#qPܘZ>G/\;.R^jn[ ֚v{Q!v삷Z%s}| mduya1Gbgq]5K.8nnnNCB nP$E@;8|炵>kZs?YO(U%ɕ~q0L+Tmg<0",C@KFpVII}КO^yo[晀ڨgŒq9kNuOVƩa TG6/%#0=Lr^d@S 娪甏㒹S}8ǝ Vhb-4a;^W:rgR=٤'k 5v3ǃS1KO^6{5"_sy"6GԣGer3Bэ ykZC܆*3cIdqyg4}e@^H"ԫgc3XE#p da D|>{3$ <joA2B.ymUkܠ_ ޷p7A50paз xqko'xߡV\3~?NgoIm2?aPnLS|٬9LU|в3ȶJpzC_a9zs>qZ [ዾ^_wiU9"[̝ Ĉ/Ԓ-a@,|hlA zoWӽB^JH]<p"R.=EۚD8Ρ` i7L{z”,`OAkmhާzdSIoR)iݕj,KhJ F1?m"鈛E^4]&$xrCI?6 \jzqdNC}-,cөEAP%Q_gmg/Y`P1ʔ}z;UJcyTtZiymkH4ݏВ_HToǶ'<ȇƗgE%0@\t L Hh I Hn&=ɝv{ ;0@ r003qwC&?m46ѵ375ݓ3vCӱuT'Zz0vAxy]Y-$!ӥ/^{PFc٫= ?o5H'űp=Hc&*R+p*V7cqqUv?pkM<1"Ӏz$/1p98,z',͖ % ж(a c/l{Spbћ9dlyr>}R:z0Q[{L uC6{!}**\L֊;*hs }vʓǦ舏1_JПȝŤ],O?py>;N-PlweTF}|è5wV3|o?Hw@ kdÿǩL?s U+@mzIqY<#>ehD㣪oQί+8Z0@P:=:$#ZcqضdńzE6ū@aZGJj^3c`?Fa2*4t-(j% i59?i6%A p2[EtrN(MpPG&Mۯ2dqmO,up/ޤ;ηQ}bE4=h (n["׵ d I׬Ð1NeOýsgf$UVMr O34µ :xsk+:vAEQNuo+jRB`4 ~tJ+`S28ILk:5>y>UuIy5 3v3wV~*K,Zu &eȋ\ZowїRS^6a~^D>ܭ ݎأW ԋb!"C˘^X;YSsRF}zzĒl?YOʽ|D,>i/sge:\HyQyܻ{TӬyR|/DRz4:1V|؅c0p>IzǣĬx( {zPio=(]StnU_>zGE, cfPL`dIf>=߃CXi~#1 .ԥTģe繴 rcLDOOx-0?O9o0_%Q fTs-z$ՉrԾw16u N 5?.nafԾ<1p/ȷwI6_YD P Œ=}oYwϧg[wN=T6aIh Mnz (?6S%DeD&Nmd2ߛ5ؙX=p\J cUu2\w 8TwdG'2E y4Uʌ =,Bn2ONбsJ.B3m`4 #T} tW ם Nk\SkMZsN|:Ĭ5#**1᪔rCYzM=;yHztgDfGIɺ*BR V="dntﰙ>W(e\.Qd' +A Z٫2+:̣KZSQ7𓝋rT؍AO@,ZPJ(N8'g^[;xsj>9(5Xd-/8X80bJ&Y-'`#5KOKZ98^-m ݢ")13xe~e{c@wM;k]hsNq>LM~¡q^NG8w|*6,=\^&ʇ=^ 6땃a bjtΓZ=Rl֞x})S - 'ZT<DOsFD KLlzG7ᩩ.+"J"Do` nO-H:ZZ|WPr?6/=OltH/)j׫6[, YTL0W R>Gf6N$[\Ғhr#gyԤBR\'5ϟ<+G_ш%StyN̊e9lq&o W Âl!W6v\.KUiZLrf ]DAnR VKMu! R?U: 6ڷgL/מ\Dͦ5z nI=Op}tVD̐p1)`33೐=€&7V"L$NV(Z̺`z* dAXK~A:T4{Oůvܿv˳'q5ƒK,%U;Tp}P/w԰P>^ C O<'u!# -4۵؊R̫K呱TA=4#19k½@ڀ0髪ZMr0(R8tԌEiQ -z$>K7  q#/t?^TKhuArY p'9o穁_Hn;k#C9lLl,6! @?* 5G $?98a]]GӪ~lV=@I TUDh= ysI<u]oɖc'2>v[u'=,HA[3L?DizklYq&uɃܭYԗb{V3{tT~owRc>EVUh%ܱM$PU}V~*B:Lb #t.gTOҽ `Ic')_5=@hp9"Y9]{C9bum>oGX@ؿѿV{Wl/ ==eu`% RJù7 WL[ lL8{LSk"MfVce]GW8|^SV\`av3o]牷0<ϡ:!DGA},ENd%j(vj؁Hz" K Ǒ5Q$ht鉋Fs 3(ᙣ Ժf*dk"&|l=<=4~O)(qĿWbx@rp)gJp0Aq ~m,̹@$R/DI`jNIDhVAXΔ]բȖǪ|+^?؈FKa`)8xu 8Q^,jU+֠릸3Mq7CLw\E*"e86C,:/xhïyvw+ r$_gڱbf1/ iopB^C^]7<ac4O8hbоwrTE&+>wVcbaFp\C=5097~nwo(\ad|tB) ~]M1 +"_#nc`0{Bˋkkk/F~z JQH\?%lqu/劯?'(JL,A f0thj/σ;98@m7{-lsfNѮMŗh { Mq &OYIMf;Dn _pUc$\| .ArJ&s*4vlQ:O;i D 9ړmKYu]'K :yg&8[t\ܔ2b4S^ fnhLOΐh7&8SS7 ¯}5 wfNOaYq~XDB m ` tm XU #S};k{C},8nHz3B77B82v)=8ـle;{.Y~ltpbYrMYjeU68!H8 t//4lf{F3Vp nNhF#:~!S-Iqcb,r:v3U w!IƼxu{(E9Exx__VY½$.w?Q"|9Y>+ :A/ 6Z< _c'(faɬk{M?YY".  s-}b܉n@ہHEbxS7x{H 6֥Q8S^>iҫqMӏB{⢁s~x{3OrE]])Vª<ǹUEfy8h9k@>eH'óz<=kZ4}jSCCTLwH+hoke/z6lBLSxSTD9*96tGYt9Bj~|ߗUĦ˃6 ڴ^ ?'^fLJF/AGcKc<R`aeco@չ8 {P&9L C4o+ ktI)JץI9{Kq*_5A0xAwrUWm{c#Ƨ+/C1びbi^857 Dm1VC։:gyȕ@">Jçp1M?eH(>] La >1bq Y\uxb}!Sf@`[j7nC ՃR^RDᡕMŅ&NR{cx `9x%du5ȍer\-*p| [ںJ)~x!VrAM4E`ՌgKg[_Gp8bX i{; u$]d#;%@ѠFRK |\$&l?E 7ùCNgnAdV79럢ѱt3ٱYo> @& +ݍ~km#0ɵڕ/._V[\Z>Rؙ^MÎ/DׁD(u6=\:0!%ZȭLf2<ʪSvCu6go3.Od:uIh@E%>$7ngzX]Y` icsa[R.ۏ_7ѡ~L+ʵ]n~7:6ճI8 NMؚ(;+%ږ<kpC@EŊ/Hp&xWf|ae^}2 {uTmӢ<`eQ,j!y6E5Ǥ _ b 2,RarVN=|ޒ }8m_0ԁC) 8}6 ~ t92х_E$3[_; 4u݈R E3C,rabu%SD g,KZZ,QuQ8YG͘XbL:J@G,zv1@>0SaZIsRזat|FVŽ`1o8Pt (V3>9mklUԕmbnkUcNBky="8t\n܊h?u{+ xNoqEo;wShٷJ[bte%FY|41X  tsi㹫[SfKM:D>gosr[(KFs4RPB'Y +꺲ԋ8a Ղ#6.s3qZ{eW$)SW0aNJeD4: A+{ Hlx%8V_c;%3 83 8I.,:d),/UtwH5" !1}#;!v|0pa_o΍c?7)| WXJ2[( HV;* da5h4I]1^83HQŸX5resvlZ1RTRu=!Rl>.4Ms$7x ~@iQ)05t/fEREI3)[k>~x|AFQo@c\k]Ӄ/ q"[ma0]|x67PlaZF S%MZx'fҀJ؃n}G~U[Z U2y81MtPi͟ 9xy|HO Lo{]{Ht'Hp]pnd's$$bv$wq$$V. qǫ6RP5Eꕸ|^ח\jOMl,u*@{rƆƦFR1 0"7K#w {Kй2(X-ndLNߎ􇨨jA22虏FUݟ2 F8T:jnO /D9=&')tgzk8g!!DT,I$TDS5؟P&K}>Q"T1}t -UPu (m@ZN r oH6E9)`?:W2Z3yYnSeתeӛLVQ ''y=纩saPc{i#>zצĜ<"iIu.=Ѝd" " w"y"2jXyg`Givn 쮂VWPZOO> nhH/,;0 rZ;{?2|$`n)K&  %yQ;߃O;"fVcJ Oa⚴痙uS^b|jMqo h;LNУ~S'*7X|@BkbmyNqn.έʼn)>UiJk:'RUxUj` j~ Z9ʅśޮ5~ Fb#s=ľ~x7~'ݧf6.1fF JMȶ6^ºqJ fF3yX>HoYfEy$N%EzY>]w)IC@6&6=|DqfQ~a`<p WZ:|]V'T~~^l;?LWsP=fAz=D"1 )=$_В¸M_R4G1,2wCrx% &uZHъڬO.(M/As[Q&~ A=bXZp'1R2`s EH@Bl`)~c0ibP9P[̥|/|FGb3!Coxio~&6֦LJ3~-_P#8:;U ~r$1u2&1?4M/$츊3i^wZFMw17 ?g{Qu K ЖYoȥ07?l"R_M9S; ҥnڪ}5Y<0vw= Y {p`Ep=*>ВvoNT ݜ{n//bZZRq^ƩJ*yP E)F붗gyDl,`˝R3ac!> x!\(M ,O?SbJgo%-MitQ-"8;E)828|p?׫ KacBz9Q64-SDyԋo&d!t(k.?+֐qtrml?ʚeAPV0p\OT'X ,E%ƌ~^*^f: ^Kr&|]bAn.=FyҌ&OO'}::5c=0>ht+e5ꢹƦ88u<7kY>ELoŃ)ZRA [^8BPW^LAF7+eeqMDVD@gcźMO6^`m|37_鎈Wx7 \A~M >Pނ@ZŖIg~;0l [ᯫ y?!1Kp]OpVa8*?V;^_S!dVdb> /Z 1xL"JfIHuHv; i|A{A,!U`o~ȧ),xP"v(@ Mh2B`t>$-~c?M#p=f68؞K qD%46דT:Jmq 6wF-8B?!tCG+wRjvnUMwP# q٨o*5{1r I)?evx- NH0o>Tx{Їz>k,Kڟ]ӂ5-ܲ2 ok/BJDk:0\dx+ %dk?y@>V<-u$snYgC$Ę_kQ ÑC$j#:̈^ TV't<|vvm֠s#nI{H lW#C-[ ). `a!ǏW`X7bGG)nX9(٘AX@]3eEG RSh5#h/.@f"B-dY죺]6aMKCKpfeCoY;F|׫A5#Iw`|›QJ4ԧP8"eSvO*aFݧG9)hv "1pYa0RAk}T?CYivXdSY{fqjՖZO9p 1ϚY~);3DžM/c;~)&٣\rPvck[8L<Vș㟉b &IbrRcel\,ߒ3ҿ] %8V+>윪]gd0G[Pu ˗`]jr: 4[-^TK\+5Yor%F b+m¾t{Ƥ*+ĞtF#kn'phic/f8葴* p>:X&+)-wpW Qwk1רhLZz*Psq?* 8Z8(Z[[[39צG 5嶤36ӭB1b.me#Ƒ (4:iYiOd[Ȁ tW fAux2d6DX4*xm6,Iӓ:)L0$iWoSЙFuNL=Ia 8D(޳'6(~OH?jmHMe3<5(>YCJ7C*.O䙞2ͱp<  ZҋabH͙mvlɣHp%۫!D]A[NVxrH z ?9BCa(RFbwNJ_ĩxx2W }ֳ58%+ן-$&$N/R.3Eta/$KW+k0I!|7>\mWdpV~ℂ0`Ͷ74i1D+;_=u ! BG4ʯB"`kTA%|8Q8%hY(dTQ@?^|FV%5ָ}WeT& c4GDSr"S9(!z Q,aq'ڠz_%k4vX:U&~=?ߣ/~ Cy{ fV/4onKȅ"Xc4+p#ţH3 *RFK3)={KH 4]ZWL rc Gy>w/F{0-Oۅ]j#|w2TQV'^?!‚û9:o%0:^Vb/BZóvVZq5a8jC ~V(A cq+9-8Ig)އi_i`j>;g7b)O!daw[H4l;N2|u;U 1= pQr Ԝx<='RhB,h];k$,t^Zԉ(a cqHg%q(r[&, ^괋IH{8Dd# }܊l47:i3x Ntjϱ(>7y\x4Ep^-;S \S0~^wQ^=d"O5 =NUM*OC0/p!9lѠl 8[FIC=RLZ%V67,KS p/&o߆.΂Q@1ܴ^;BzG(2+1 ByL^Ґb ( _vfOVyi{MF%zC ZIE&[lPc%[,W^dKЫtRMvh9' ?qgE#+.,TC4Nj’5X˦^i]‰Lma+d?HO)LՖTiy DE#]A6-REj R_ 5{P(r__o?t1 ZX;_]#|S`t\6&N +ps_w39蟜aq%ګmX*Lt]'CO*!Ҕ|U6h&a=z-i\ |Chd(1ԫQ-"Cìx47}OC0>S΁K]\NX T+h,X|̴JLSH},QԔN <^F ':^'Ʒ'0z>sk[՛XXFNQ4#U?J\{t4q;C]C;GV&7~? kadmgSYǏ<5 ˘Z:|hG'?'*,w!wnX%z'|PC ^+p<yE&",;_8ҞHacK^X[02ª3'v|1ۯKm!An 13߸JA]K5/]׉agmz༰oAHJ@bwDZ<̮Qb5該 8 W $ˬ)lA6 鸰gb8'o)dB6ofTd^,Il1&7󽂤ؓ0031K~bS ';Z~2us1fWd:|ښC׾)OB&t5qѐr5GXbHv9蠨`]>1uƎ VY f : _P58e|Fd+5sv1nyn]%+3= bۇ ,uM=Ppݕ'V!nTF=Q/TR"l{i'q`Qp=vJz FZ7@.Ks@$XN=K%|~ )٤٠bڑB*`'( gf  7mw{k {<c~cM,jP*o0X'\>V5tsUWd[vMdhWKlrr[n}Si1:Hsmɻ- ^ lTF;(ڑ[1?_)r%6g0# in6Ƞ;XN3YkM@=KWp4f^|\U^N3Jg":F˽EǷ&Ŗ5KmIp+6c7M-fOIv/td*ܔo7H7}2V0 ޿I2o`~? aavkt+B$uljr] B\cJ@.|;[BяE^%iՎو ϲ mk?>ʞnt'ȥH?J*rNTo(gףKr[6g03X˵7q^۾0~4vP׊=mqu,*QI8x/x xvM"!aؓR}%9 HCXe㤸@Bۋo⢵@<M˗(}I^\<$.U6ze0EljOVq!y'.ni/'+'qq#$ 2 ڟR/)#{Y9Xк.ɋ}%wRy/+| `ݲж0/|Iâ%D_.6;3d>l]3?}OH8Dk58LN'T?SdXҏ}{ 8Z bg`oc?Pw{~tiEAEq&R+__- 5ڴT% vn/)M+@HY.G)*R~#!^(yR)YАG}v*%L{pwKX}V(wW|7|@N&&?cs29P[0]jd enBQx فGHvˎƉϢ`@ݩ2C=V+"_s$]>`;bN1Z.ԗ ϰ|xT-l(O;1O̕`R!s;H_vBOt1=0::( .X,E產}D_bO瓓xXϥ!0 8nSC_I&NZ̘} .*5rW5׊Ɲ9pђ%oE ߣ6@} ˎE" "o@ G NAO 9YN=͡o78hYY?Xa4RYρ z{__XF#OKyڵ >{ZJ,Nt|l+g(Ѧo&({M E&R Đ1ULG 1a1-j.Y\bB G[Ց|<;$@$3d;wxS^{ArdraЗ-TOM!ց/>CM%kv ERm4S5FkfX{@Jxo/|מk.l(NрE6 ձ4 Ez R{o =ЀD.<ܡT=EEC |!`eUG;fy}8;Gx';ĿGlռ[T]r 갶r>׻>G39"6v {4bWyԈLff=z;gZ@Aym֌r<&}'X, J^C i 7獊l3wbM\7]2CfFTyIV+/Jȓwa7ʇGgQn3ZٳQzHOXXNSܵBOi ŖTԫr2 1sd;m%oj, /TĂvC z6"z ٝ%yιZk~3)"ߤfv|!ѝHfU%A$%?!t0*|%7ōY"# @M[x*=̆10MVI㶓i.E؇fw.~j \! Z`lpRݘPFک13>5eDrkx)jMl{[} 91fʇ~bÉvroNl*kBBRl?uo|"@xG| iFqvTII]#PyYXS^#^y{tHMwP.krr][eGWU|&'u,=S}39MYɏn]6畳3nJ#ZМN3ȎKqc1NAVqUa}?}{:Qt|~]:< ρ]\WjpS߾;&q%ɣۯBz2Cj ٗ$C#s0n!)5pO%@[op;{{6<ۅc#7X^F]vx?1P$Օ{)!8uh>̺˶W= 8{b3`P#f`ȀgB]LTuz=|` 08f<7 B浼}o|-) lrS$kĪ 0dY-B|v5YElk)B; ]45 1yM49|?n!=p=s7ncS&=e`;;;^_ϨoU?y&ue &C! ZY0csGc]ߑg %o ֳQC | ``Ƒ<  Qou+CutTx>ik)8LWP#SPE8O=!&a?7R/&k,9nkx'F#vڿM Ӱhx3y(,I@1+*OP^n|t;=wxc0\7uҁ>[lg;"2?# V-Q8+f͗ {dex}\i2vYG~ LkHNS:;8csvj5"DDݓ+#۳G08kDO@pbt\ouL]uۻ/3ܚQT'H@'t֟q6{!ZsG8vWa='Lۺcmn'LuORYO翠le>9~GEdY=t W 3ybIދ1*25Fҷ(H+Ͱh? KL͵hnG_u -A% "8y \}z­IM)\'JA'^O큄}zn]tQ<_M2~V1wrWbe7ु\ 1rPQD#=3EsO%0 j6s5,gGp]i,NQ-xĤh`n3NAr]ͦ.&FGmX_n TDY/I3ڐzt1uTGVxZˁcA+w4c[[/dD;{5U6}9ܞaN`r#7{bTB3^gѷ9p4ب@<^iqZų 7+7ԶTBy>`M@.3ʠ WaOEL¿ׁ`vX! `fba@q9xsٞ뢑}Cʶ^<2O*D)7A bs  5fI1`. h(Q$ŕ[}ZJ.ϐOZ{xoSXcEcx֧`foX?pIQ'2M Koɨli%7Vp"Gb@l1V_ںVG `Ϣ֏?NW [:Od`4 >evQ/&6QjsLszڷ?A zS۟[Ӝg t4PC=4-: Ke+u5&LG13\򢆰'G6 WoT1&Fwn#2ts)`9w(,}*[OLݒ̾F&p(p.~F'a3 4wjG(҉UFk0teMN2 k}_{m$e\0i4"&hqkQSxٕ x Y xagwW%vt8z&XYܧgZeqĮ~Ph)Y'5V[%GʗC|[#BhǣLA& cT U$ :ʣL.\?W4mzsJ!%޸aSO%fhO)`[S'y1ըbz * UA4U%צ^&w[CNeAaosyb6J;?k%&tO}Z0{~st|\b&Y|>Lό ?{+ҍd|ϊٶ4I+GͧAڠ,Qԫ)ZիXc#!F;+ef`aaa'‚.2%+'1? %j}nzᑫRJƒ' FgRjj AN)pGh:з*LL)"M|PZՋ'sE^%JW/uydGW㆑Of@Y-+JHxE:_>RLX\pЏJY7hoR:۫,CYZΓVg#M(~FSԯĺcM)6|sN}nTdSΙdFQ·ِ.F%1LSjno_u4A~_rb̩{,NRI4f}[ǣ].w T&z9b<&W(\w2|nVk@q +|)3rS!Gh5-M9%tD0vK1Ϩe9,pi:t\ʠ)9^/TUJK2}9}PQ$5MtКD L:݁_W1ugQVfS?GlHWK6Q ]EP%b&˥%ҁv{T^?j}{]g\>n/GӟX.2$~6ky( og\䵜qJ᝱ \* ')F4' h@_ zcP+np÷[ʎ)9[ qϝ^A;LFÀF@PUC'>hC=A H M$q32Co^/I%f?<2?4z !s;]+}CB!?%~[tbkgц5::5oޞdնc,иeڦ9|5C Y\#Of8%|.IJ9OD-m= s۳.d/pĬAFvP+~S FdmnQwb "z[dU eG:XP7Л_&E#xKLy~]W#68z (F툚⌖{ @ t - gP0;QMPq/tG!#8t2@MlB~+YZ`^n r" 4@ |o@?A3.6xDR #pHe"KjL&G>D嶐)$) Fͧ3+.kD@ӊI C?䱔!Nq&y(̨uޟA pƈNҽlׂ}٫L !WqM;ٝMI0s#n+lQ9ZXtXApqކ\3J'QmSam:'I-)7^o? Q[7t@tK{_# 9=^ n)IЈqYؼO9ꜨobA$"պfbrt?'”scF2kKQHQ\~-b/x-٣Sf-P#>T`tS0_3MÎ\z5VlRfL3]TK@KAqkUyfFRp 2/êQ0s=i/@zN0IZt2FƯn@2a#j&-DP˰hBV!tF2[ա"VO#ysg-?vKMVYQm#٣qQ!OXc?8wA/ڄ`U$ :$C]&?%~A١,jK9Zui0}(!s =yj2wJUjRQ8g(Ng@5KnQwf !&z=ND.묩1 %/KxStO*ctx—Ql+=y>96 B;1}>tD'?~ ƅ;{Wؖ ;IUz%;cSJup:*#ܾB*yJ.ȓ~ PObki +)/?/l..VjT@Q979!gUO{SM_:dǛ.vG: NYƸƢ ݓ=_h@|s1 ҂,Q Չ'>"u8Edv.Pp&y:mrrU{l LE5CCUP(=~/r}PGH"WA€PPZJLWPk_|ISfgdw}?x'>@-l!J01um", Ht`~zz؞>=:iE#40eS{wB2m~T! %~.gZ=(юjX"y׉'&ѫl홧>lSFOk|nxl+@d_ nNƟ<7rŧ{;iѴ?y ËJKL3?m2v +m w< p栓֡ǂi@@ Cevǁ9SuŢ wAxzOEܿ?ʰy?`+N ĖVOw`2 L?eqWkP6ݭ#2"Q.uJum:cB;DӤ'yCgrQ{G:']aE](`9{IcrxɡYI .6i8GЁT[*HWR C΅#{eqvCstLUⷑIcZգЖ5|H^ .2`>=/<,o/Y.!5 s(`P(=_߀(K?sʘ;"sWqO M*mӴވnxq+aCZOz++ NVx>rjq."4g0)р_6)Da]rH>1IwQC`&k3Cmq35B7Q̝ Ym*?NG=6@3a^ ٍ3HWuM:_V/]E=ܾSsr 44NA7h=Jz~yA$L"qJA+;6(@N3]xcOgG?"&^t4fpT(| nh@/$+W,v@xY k* z3BU-B=Uc>$.Ւ$AɡV=k)O\x$*Z!Jϻ|{o A7xߗ&2"ώspwCdy=ȹ.ge@"􃲦iȺqc*%;d@齆 yJ*JT^G\̎P9Ӕ@,D7򿕝CK?xVO>A{t-md|͐FQ?G(o8櫹M/`)`ddgd* oX9q$7_ ˦1=ʣ'\ qx+VSLPF}\+*9g* .ReհS,%uË*1RP_FNrj9^[ZWp*pq9.nĉ`uX~lF>GX;9a5}̏C?nO@+9E )28U##g87C GY&sk2[h1. %iq>~O7r;jm/?ˁJ[- רڪyGjiTid `P4?Bh @8Ɵpuӧ%H)&| Eqh^9"5Ո -T(f̫ң?NF!<-@0 02?ȃj˅۞` Uhẁ֌#QP.$D.KopB k-HhF7x̘y!5nV旯6Mϣ\zRyX4η>E$WU~1Kb(-] ww>!P=oAVHIނ){^FE1pyk+C;?JLwG*0X{^u ^߈45yzѱ>^3o f7Bmc}Fsp&hO+-R8#]V׊:N(7L+o2GH(_`)<&OvT\r(;' !BAj tw!P*{weWv $۟>VrB"] :M˭]=PQq0wx gudEds:ʁ'iQ$!KD3_Q|qH_ )V7{4Hs MYoF9`80KG@!z]!;k_[ hm6NjJ [][J.ʧ-e,LQibavUMݮnRIPƼƠ8ӗCxf׺D!&zO?y֜gf @^J*6mz/Ϸj r2,Lί^k/w,?@wWΆt ao1$rxB~yzqk^݀kkZ.rSYD1\3/-:Ìx.m˰(`sZ`OnrǘV{l' ekPJQomUe̚xG2w2IVjqZ6q2KOhm6SCgS>c 2ľ#Q\~ ݞ6D2OJVExtBk6i>28a0 ~=̡@ 9=6ĨM0 O'j'X1mL(ϭt LR_"5`am `caQй~etԧbA,3p1|⌅&@nR!5vt 4DD¤7eڮ#P9~.Wr!HqṲb5FȢg?iYD̳+TQP9Q֌z3gmX:m`Es,DN;Q NyC'!|yE0`Ǫt^F&Jѷ9%R}1CIA30nޥ\^#I\j5cISL[HM${ppM$ye51jl/e"J3emOƾ[o RnM2/4t4[VRT6%C/ Q<\ɚG$ WXzb csU|̚]\H.nJFHصib7=CZ(4 o"WlwS^bsK[HvlK#jMZ%M*'be9}?=+P^NQӧǑZ6ԸDKM$t|1SXpsz4;/qZO'ӖjǬl@ xA2F)N~.^$t4p,agKd8Vpq {c0Y ebnxȊm`bfff~+ *D2J ?YKf96.&F5/3R^pǙ5f 4 tg%gYӟSDՒ֙I_]֫າ7+4 ԡ9iz}y9ґVRl/ ^)bOa"٩VeT{#u juvf_jè0RE\@Xe `ޏbڣBvg-ھ MjQG/پsq/)@@FFul?<:|S[Gpu$;}EP6B,M\%dm:A<n2>(,:ZOUW.ͦ;Q3ҝt/~^O̜PO_9Q| ;"K.y"U , qHnBbÅc2fWӰ \V\w/ԃz>Ά2;2BƠ#NPzLrm ]9OOQD ttXg=f#*74d*z^&:oI+9,\[g /_xan^@NkKWN 8x큃t~iчb1yST(y E%y)ݙYQW7جCLU7~Y(JNDQ(j\,;)w99ckN׎-e2I?MSLO2iRI *(nv @Sv;Z(dhq(j6 k z,qVcƿNL &Z̲+J. [Nmuyh\f)+_ XEzC5(CN? c}[82 v?& Cshh{>?&rrTYgZ qu!61=P{R7gA~s![ؿ-,/" { ev9?K>Zv ĕh~ F}I |}R)| Q:j+g0}vRGh% ]+uOUNg|2bSMQt9҆jAE_:EwU8(`l3f%rS0XC~B))׶Kpf-J9u DXN-dH?HM=[(rI~F* K/ٷ܍'u.y09rr~/ Nk 4$hWG˥T>ٞCuK`HcKc `3}a}߼.)+꺟P Yn^T7:nk#茇`JKe4:( vIyԀks{a?|lsѬū9wЉm` '\P|*7ՅV8O:STED [Do@ً0#riQhG u.F3^7,fX(DHi1P:.9^2hŮ_>ӣ5{/s*KͽMhqO F?F"oW2u4ͩg0]JrylUv!ʵ{ߋ8)LiZa8J3:n-o+Fe:( @5h|\aӬ%Qk>ERC^ѷ J@ `S$xҗJ:iVo ϶Y+ED"gO/JTm#4=zlIV^$^Y'jvAm֛Ǜp>O󽓻[0sb4c¯C9nY>A՟|F<ҧ~rG1Md<%ǥt&1PRq7 /f9F.yJCP²2f3"ؚ<}Gq|8]՛qZn~ [9ґRAE} d?k¨ІJ X%.,dLnE`׬z)0#J39>Q#XX 9uZOV_98>MrT)"]l/sUF<  >ǂX0krZL"-t(7ЀM/F2\jXVOZRd7'pS N5EW[ȾJcT>/Omi( *DR_=Pk\ 5xyA+\ۣTMb ֣}XiAY^x3t tet~ AA)-K)ev.%L5ӨWeO٩4=‘hA)AL*$lJ?soW} (3)k]DH@?0"Ƃ/8'݋ɾy3֕txOݝn9'S.FM%~⌽)ttQ鹫t=1$+ پ Y?Ҫe}#^P1v',-&a+y7&[* "8ĔY!Ha(89/)*(ABt ` y8;ɗ>3IMoO44\M^ ?{;=}]wEn!^),%& ǭܱ0Kg%0J-$T&m BrMvJOyNeP.t۩tpXӘQd(P2S@Uލ<g U%AYEW,%c`ذW3 #H)"2|ɇH?|$ǔ?"2 rY KH2:wU-v>{BPsQ)Xha?KIVCG*P D[AY ȥv5HR'&gf"6_~eg;$7# 6'ƺn -ma5A?EtoKViة{mMj-s}"ǗuS@"y8f;2IǝGqeKdCyz)ST'i0QE"%6*q#$hE6"0cȑu5w6:$uw8{:vX7:1{㧕YC3?#\p,8-}y{;?ԦR .J JnB"X({BadPdyj]!T]Sm"? zpAY~O [{%&_besX1$J O,8VmkG'%.VlIA@M0qR59&sl՘yfwxS"Dw%`&'<= |چL'sL\|m>(uY]wtK"͇x$r_,^1|ݻZkatw2$l7F}YXj9a8?⁽>F+ŏ\fG"Cmb]+d^h`KrFjlښ^}%y_MkIxB0X/ k{~ v ɺBeƠA%LAa24%V58Ǐ &F|_ZQ4zk.n*8`n67'HwBdD .xB`g6YY,W0_1~P[KD6ip=6˷x"b\^\ p gtF$ ~$v CS|#J:X mۏ+4Z7džfYށy3?>|@m[A\FQe YV}u5i<= vFvV*VgoD|ee010=[<ڲLқ$@H (4i"i<@, A*.JR)MsƙY3?&9w{읽%[(+*|s8 k" (j<>nHK nV"9YX׵UaC6#N> :/6D*YۤJZ0_n hx|f@7_,Mx0,Vyc r w{LX9I`Y{\4Yb܆2zo_ }\Qs[#f g<ۛS`Uл> U`dgz { i1=:|yE|)` ĥ9Uv;Yۘ1 ]WhU ō'`xoV|I(٨%piq(";aOߒsCZ-'cY,^w3dP旅ú}TA`U#6a Q{Jjd"#!!Y2X]{Zu;Gkᠽ] ijid8/{=rQO&h/iDYIk"N$?Qg5xSJ^Aۅ=3R3<)UB:^7;أ_='LN2^7Z`Dƣ2iN/ongٔtD_u,DW16j oڕ#N]rS!F ;p\,_"1UNzv -}%r[f;ƭr35D`E쳮 S{!?9ϝ`Fk۞bYG0:|TMhNfouel,/dgA`Ūai{Qoޘ=TWH{ ww;8]Hk7%JVe̚ e R~hbswNS*/u2}'z' [Z/k&uʻsy1c3E#3LsCo]_윴A{w {VC.vEƹa֦]pY-P6*-5,m%ͅ?ug5荻g GEJ)%o2Z0}&a wWtT' L0WF|k?Pv먶jM/*W(2t46*d1z M*[}yPuUGR[ɳ"- PO)>xAnjk>>@H<2GB\8[o+=woktR2up$Qrgg4 $X9|݄b+ni./EEEo2BP^<g{j w*.jd~(A@x馮Z <7WLav;TTtNr^L :Ԗ)~laFd|wˍ}-uݳjQIϿjw-6eg0ecJao?.jul ]<ʚD"ܴو xJǃWv'W\Z(ɓYQf2ɟ%MS1q#1uL֦êNa)Jq[|Z&pzԀ웲 K+YIU|e1?AWA1Zu>8 t(Xś)u mYhH/g7}tm7k)S. O;~ɴ7ߚS\ ȫ=)?ϲ* 4?4[;IFV-fol*xwWT2O2jG?֚`#=k5kYQ iaQPD=s|` w(?vswZКAs(xHH`g-xE`\G0s@];ڀ{|a4@̕ouNyH,ouBs24ʽ4[$ZSr NM1/\avB+n1d,{ɒ[:1 UDa%fJQJFVM|V\raGpOR| Cgl7DC. Ltbecךs"rԒOMh ޼{Ŵfoza&U3v[ȫNm[@;n5^TJV]rrk@ ;¹'3_y8NǞ ՘?$Kˎovu00L(B|8 p Exv |0xi  CሿTw?#p}CzZ`M?&1D =k"py<؇O`0DZVEsK?PKiRcf/ebin/cf.appm1O1 (s!h*t9=$r|8ׅ!y߳.瀮cL8oVjyڹw@#.(BH 2R`cE$8ba M//*=EpP!^.cb ;9%qc0\ !0~OzϷL}UW|d`?:"H I'Гh~ j8}9!nŐDVF!:R,$ T4dx O@$18.OlO2z&=h=ǚiʌJw%F@: GXm#x*ZhFPGTT*Ae4^ɞ"d 3 AY,< ʣ̣ l`6dٖ.fW *E^ə@㕑mB0W1%w4^$Go TSCA3(`,h &XBI+ȅ` El-`y@ J$ 8B:Jp9Cah 6~LnS_r9@T  $%e5͍†@g2 HI8* l0z m&Ɠ ƒ=aE)JVRˠJ8bڈ+LUz 73ܲ41^UgDŲ@t RQRXD L LI A`5@`5A`jj..] kPDդvxP%Ѐ(`&r \c.5k#|\·Bk0"\{.χe0E8u.uV#\w.g07D\>s#x"`(~@! F(aň7 XQX4~TXQX,UJGFftp >0@  A3(FZ00B J5gP>lpAT#;̠V-A ꀑ=f39 HA3Oa3'22̜f<(0[0YeH1!0!={޵pa '&ppq&_H ru6ӐAq! l`l|`[6`B?=a>7{XVW Y-RWa6Lfej0c#naKf̖#~ilfy!qi[X٘m 6x_QG5k! t6ք'9`!A_'05p[)9|hF"ZHvjvU`ėY@`G%z@@@u|x 2ܮ D+]:* 5J.?lBH)dIu;h~_g^+CJrߑ{MkUN5N)6'Od-~_{-(qIl]ZmCn̋`XThb1zZj`J} ُwnv.cwq"kCݪ7sߪ/ڿLშ'|䝗hƓ.=gn7}~""5: #r>Eɥɗ 6Y%~e5#AX^Ccgd=yR׫kI}ż;–ep<*+ʇTVRejyyރI "/>=ԟkhw{TS q>)W9^R+Mh3r~U-b @MhNJ_$Mh#r_vV>C6k'H:nkgʚ4ko՞o !߱7_+u>k9vGHmukͳ{=5;aiڮDk5H5kZ{5.(wdWA?Y8%jpD=viAw jiess7j۸0׳/>j˝kxЩ\{|7uiJy >y޶Vl-_1ܺB{d}MbӶ.ͬ[;8ZDnWB7 [&^K]!-&=;ֿ:#g#uէShit %?*J k`m{@F\єab@fo%%a증pȸUSmخ{[wqtϢ*rtw.s4E{[6:i'O{0S.fk3{Yaͯ=c&eόKu]Rw==hs;WWR6{ۏI.kj% k'J8tܬ ToHYQuډ[/ofrpaCa{Ur^;nګoWbs|Hxޗf^i{~44(+w8T2M4tCtfjIqO-)>(pۗ6{ENCBaU*lOOʶBݟvU*,mQyX:{vx_u嗂柔3 JBy-NWN`[^Xr)FTo˘ゥ״ O+'`f[\h0-?\2evv2M =5)i)QbcEqˬZj7 mn_"h_}c;wvL2}r{m^r&Lݭ.?`Wf*gj+W<.:ٟh$&2D,%=D8(JBF(vAYP6r:NC)$TUB'1: CG2*|hT@?A9 : 5B P=t j PPKiRc$,Mcf/ebin/cf_term.beamYgp[`wvv{{FlGA8h۲lIv :jB x^f2;{:jyF]9EKW4ee-~pAo/dee-aV%N ±!P}DGbCP/vh0?\#j),r| ٛ[M< ߌZYcE[YlXek2#e`"VY@\6"aA{Y: Z,dgXKXuZC,l!vu-qZ_ĹnF:Yko:|+ J6 ( $6J lmش&f--۶ۼ` 8 ]FneX = fmmߵfBIk,6Dߞ<l9<F>\<}<BE=ż y/%Y ptRxK.U~Ȕ0L.?豢Njy /JJ*Foprp]rA9mm%B"EER2aJJd<(7r{ Gr4q}$c4;m,QXS(=gQB&!IM:ɻ UD>kń~~_{*"@M6M3M7MjԒ&", [Z\l+5bZ]?xy>/A1PBh߉KjF4 4#4%W€؍+wr3!:S >ж_nv0KTVQMhC6pJ$݋oZW2*-Y<7E1iQUxlH,YJhG2:!t$2M&Q0z;tApx (<##3.XLDhf`NZ4Fq$(%H)b E3V:\(!󀘣 RFʰ$"ьEA31b8K}rLr+Q*h."CR&i*6sM) v.=}ȯB{鮒q8b֚҇OZT=nB݊N(ܴuZ2j*0ʅSS z^|&f**lTͰ=F<6*@4FQ"<H 8Vw0r5bBd>DձqACt t:_ {oH8#jQ "ԾHԿ?`Z0fZ0lZ0fZ0`A9+x%\Tzy%:aX"B)U>^ūLмZ)U5G;:yY\V>KyGv;!:KqMZML#,`}4GmKffx`DB7i@tx\>[13g$FOҊJVTzPZ/7eU\Lv.C}ۨUg"X@emGB,gk Bg>8V̷B[P犕P"tez3u-SӠd3 =E'qNRS*@5PP "8l5!Lj*\:]`n4V{5 4,XKRdܖ]ueY{ڣ@=`^A} nvf8 kKc;,Hjs[FuK7e/ט`>' ЄDOac駰;Ohat6yNP3lX?4X49jM倦Ƌy#!( V*kF8AT`e6!IĀ iwV-}6QoAۥP G"zt!$E8\Dh8ct Xg C x8pa5 4"Z(W Bo]bsZLůPZBץ{oJ(%4K4 k N!.t_% 078U?CT*._zI (^Q5!\/Ê15q ]n#*^aDW*W)WSX8%I("V69Zz ?@99*3CJ B7C݄"K Fō f8!@pT"p7z=׃so>`hjMn {-ǛZ†nN譙6tA*y{"wLC͠ȝ]nΤȝ]I=*Cpќ>9MBq[ O;%+Epjx<@u[xrӄ>)ӔxҮ26""CxЇ\%zЇ,^.G }$5w)y^g_cpz&Qd>SWqS 'G<' }B[IG"XO+CC zC/߄n<"x;>k=I^z oCn!"0XeB7+< GԣS%#a. lEd^QW28իwojksk넾֠Lmԉ [E4\#Sf B߲˲9a#oΏk^t]pʚ$]#i!5߂34 ߡa?]ï>c!y |JӺL[ć@G~z=r SvS;hi0c*LؼrOP);\?}xgS$> $)dD t}9F'"Җw]Nf } 7~MC76:ѷ'';{b =G0Oh]sv9@J/FY+Ԙf-kͯ*TM~W`X Xo4tkKʧjji*3J#EQ)]Ts̥hQ^X+aV6n373aw{yy}}DwEʸ \A a _Gm춞~U0{P ݀iB`?@5;`l!Ոj fFjʘM5PM j)n2-rS%i:-5П',l|88  C1]=`ёL{a9DB!1< ÌQp!DyT8xLC\obX@{ i :0rhO`)o,1@H} 9&b@\` Bi BST3j^LTsR˂IP#!dJҒC*2Rf`PS##Fc1"h4&h4!TyP  ٖbk*13& #5"bRh>d\zX+$fFrB K  5 +X98X`U!\t4,ApƁ@ͻE.N|*2tPG翗XC`ةSYL4?@8t~ @A!]&X|,wϢ)K6ՒjEVMYWʚMPmUlT{N2F%iˡp1Ԉ!guB 55Gp~9\ׁDo&F@MG;q>Ym0!x捻FE/dkÛ]&! >%`.48!xƬ0qP$hEA2EBH0\R+q&$Pm'0þ0t \ͬVBXX/_8.:vBHKI 0!a8SqpK&B1q a  D@8a`0"񍆐sw?b4_@H4t 7Zg]8]r1̋I !1//yX*BbqB1pf~܀h<02B?j6Amb (! ufkEmֻ*]VLM/ B7Ha;#1Y|JCgch61N~]S!s4MtBH:~q:pqӁh^DzL@4P2H $c<4@s $sR$A@6d("T 1RN'V?^6^G\^k 1 H?[; Ѽ"ǓD$:8N8qĪ%>|%8Viџ!yƋiᴖJk&~~9^8sbJdoqPP= O5& ~B>@H tBiGZ`PJ\`l!!d7-syX{GU F( B/J]O)XHpEC"쮮_`U>-I[ %`ޑC)B*U%kkY-l<!YZi`?XZ&)F.%<շ-G|zgLT >::Tr~>rPɖ`^a]\][ro\)~7*5O;n/8Fdž?jWLH\3z_l$waKYO'8}.+jSseɘЂ;+G6=gxpŃ6ϳokyw PkLѾĘ9t|]ncvӹq=+.=P Vξ!-sfMXg3D֋c$,[eXxCys&,[iDΞC/I#wAKʛ4=UA2ExνNtFe&}5;7_8ppe{N q-˴FԭXvǖ7$.>jʑ5k3XiK|G{&H$ݒCV++,;oqE=OɆ%o{Eg֟1#C#N~|liUc+tX} E~}Ȇ:/6zNH O'msrTŭ2=Yd>I+_&ƀؐnyem\fGW_N]! ւk$yrD,SM&t% 9_(!xdIrRX,zH9R7]J*"_=DB/C$+$U^ NIr"45O* d2>I/*2S $!odRސA(|% d@*!ݽ=h$ )OCORi@*V+HˢI$aiaݢ>:ih́9wΟ9pն|;nc٪“]o.Hc}}U:Բ+ԝ 3&![È+ebl-ܶUǻο]Rdhk#?RCʅ \;E*;kgȜGI}- 3.Q"]n)^/Ց6'܌i<ylOvxbKV%W~m}In183˛9%3-+z#^e@u_{*g3}̨+Y-XWeG۫]6-6OZ8sv^Sߘɳ袶غ$mHm^y0ҸnIWvٮqW"C&pu䣦=N.ώ;l]Qncu^ RĽm8k Gsp/rv-=mjc{dCAvM 7f[}f7B=g̹06d?NowJ=MtOgGe!%ݛH5]vDt$]vB5S~-vy#'=Xq Bjtt;Yq81d #{5yUu%@J3wJ`ҝ 疞 -Jk 1{~=> 2]]̔q?q^fR9?yoۖL9\h0VcM0N 3zaX$iwk3jNS#PB0<A X093!/D]!T,Io^zrvK;^y&rY=g8g76:O/ v`Q^Ɨ_[gf%VEqM·<덶i5w|WyӶ1hU9 uΖ9֛n?Ӛa򻲵Vq_Ek9Koo63wDYs^fZs=A?/\ zv}vOe N=ݷY&ݧsYI>6#_kXG{;!eI/?[fӓds[ǕoF\f&gB4IsaVL-9+?COp4^1"TYX#C4bQwJmP-wZՓDEЛnꦷILtyF'yf7Yk56չްf]g&5Ox.z1I͌I_Z^ͣ742Dbg-pB.D>wI*+}3;7?s(29_E1ȡ*wHETwX\Gmh(őT,geR*4Q͆;^Hq`NDLs ͑ʑi6VD?{<9<߹߽=>FB,CNoQ:I k%qiP_/o=<8V3ke~7f=I8RbSRU£ӣi/z?>6i6Wm8TD1u[|ݽ%Fp*#/(T+rԛv\Plq2}qlz= _kRͲ5mNnVM'fAPdҜ ;Q-2DHMοWx_ [:-iN̢*>T˖KUhOxX̆31t2 C$T_PZxdb~L"g>@꧇ĥ%O!0dwXN:̙Q+zc3r$3#|dhQ"1KeY޷=>YmfFx:u i:ItCwMYVX}O<7+|/IHwHS/7^)>Q3:TNO-H8 E%J]movĊd[?&ФͭEtG %Gng9_%}>~^)DjJ^€5<'Ӥt,p=<+1`);%v%H{FYf{-qs1ߔJ01>›MǁxYfZue4i4b_^s{UKa?x'kj3 ^ :/KF"ɽ͐%q;{9Qw @7XԨ}rCw-{*$>ɼ>(ܐ) gi?M'V^9aUFM.^oxذ^:/VS@͟7Zvudފ͓]؇* TXDyaMZc]wnY[c{%U27ׅ#tbz(`&E$Ti(;zoynOrQ Q|) \&w$Hѹkk,7atPe1sRz.pSM/Ass/S_]5b;o@fH%Bs`{g$DC;w2 T4؞ 9-SQNz* 3vsP').[ &XחVM*P^z&x5)yg.ᄂJk_VCRJz I~<3`a?IhBWQ}$|5]ȷ_^E4@g$&O|uv~`S¿lj쭭‰mgxFeЍBwR_`Ytm۲moY5 ;0['[@O?yxˋܴjr"q%g$KNc*c5N O/1f)A)kKбO2;R52tg rP]#+[:}: '_DǴWn^!B8 %G/=}U~5CˌuOD N_*gVog񫚲JV~|'Zt{DL.#V\$Rt$-'͆!;FI|CæR4~bojӂM۰DYx/s7Ez#zyJB N mC\4޼eU]BT[QZY#厨ZW2 hK(OpcM%4Qm~88|(HK̮RwHd.A9[R9.vTВ)L^=op:ߥ imR<5#ȴaE~gr\h ܂&^[ia,溕><@ 9j`:P۰T1L6>F4.n3aAq5m~D0ngZL9"N_W5C~6ukݷT̂(;k?$6"mH1G60XCq|d>( 4ci9kIiA\dB(Tm:vXv?vah$hSN2 X˄z?GW\}n8tA wO2m w_K˄B 6r4 ϲO[@h0 =N A@)+L${J1ˉXyNi8 ><`<-~:,Ҭ\CPΞuZ -S[M~y4iHubZfCC@t`c[h/ ĄSPKaà!,CT 8랔_-?llx™k) ~t:trKLx;ȀO+f-3 U)>OŽ&%<b.!τ6A_ ]#:Ɲa˵I: i!XπjE"# /-O)"3tvR'~{-Q{~qQkx2蚎wyר 2s@.98e T. Jy F >A{o[P=S^emoEtw}㡨;uOh:[g*Η6d};UrU5~h.oѻ+veڀK-Zr DmV$?q%.}Nv\j8&RlI;!KjçRRoGgm|wXt8[OV(4KD:!Gԕu 1?V@#rA'F #S꯴p+TaՇA>KAV-]ozJ52ةiZv_Ͱ;_2U*U e2G%)6KΚX/]-C-A0ER2jW[{hbGFnV[5(gVup+,F> UX%MX""UCbAf),I+kfiւj[$1. 7Uf9*QDs!hH"Xw1 ӭL+M҈ʹز҂@'ib"$aAf6fH|H]A%CѲn*sGPU J"1!q@ ̇GeBd@X=#nG8HQ;n?k?Ybx)ܡ)|~* A>>~H^D3`d.rH4W2ܨKDBeaU˵+@ ŀ C0ЛA` `a0W~N\>:GX $ByD\^ǀi)<HBٚā.ѡKGB4U"q|,,/֯0U:=ǖhw!"4MG(DRH@) ˙"yA/Uf0eeӯ7! mڤIĢm Dȳ!6Ec^C1.UrY|R4ݍ#ymJ\I?T'# <I*Ps -h<5@946!4-/T击 ?M圴 9v[WfRv9O xm L@e3dĩ]q>*.e?M\0q'Ńz)⇊AqȘ4a`%_DƖ@b{牶q?Nw@iiN4OhN4'K՜5<́) ܎*3X[ò%җ%&1y%&A]OKLJ<і(.td8ݯ _Y!c owڛgcT\dJOCV{l& x>tX6z(rPp/IW|ˁ9oR:TMJ&M{4Ld˒t'=LZt[ߴT@C*7zWCP .?:  ip 1 Dd]a?+C{|Ehɱ.s& ^|~,F߯`'>ST`=y:>}?;?ky줏Sl}_s>4a|b>N| v/윿), 9W^ݼE_\ߺ{o&Yi7^ho"/{YԙYDZhnԨPWwЋ''^u}=wÞ?Kf1̆M~ -jXh~rͬVЭLkc%+۹-VVUs&kFy2xp@YΨiݶ)SxUxf<@gJ'F֪x_|x^ J/MSgf˪??#lv]2\~?+%2> _E5\7𫸄Xk-| 7[x!/PKiRgE,,cth_readable/ebin/cth_readable_failonly.beamXktם0c2%c dWlÀL@8& &$4)XYR4yH i#B-MnC-u6}nُٜ9wHFY3߯MWP.ˣcg졜B>71+itJ9;?Bl>[Vݢ=OdݲSauޝrv9;X*sGrj8JTql`+rc02Bv&SJ gsJRn.{.g y%W3- rN~׬Njt}JdNqΌ,T,9)nsJE~9eHcv$_(9!A<3HP)^Rfh ^e-mי;U T9-0U3[ OeÜ3VԜ4wk5( %k2! ODJ9S+lYFY4Hũ8r'K)j;NXq넇xjY.䇳#rycĐ9HN\e<[mS{h|4MկۈG󹌫 9U(:MU<; teDZENU%_oFUC w:]<3j6MEĿ%W;cHӸ]z9gŭveSV_[bJ3 uh\Ap pމT1f*v%NɺM>DUM_s2taW!,T8CiBsʶ OMLk#U(k#e \f|h g2Ҁü=WGBe;4I:G<͹ cf@tIoK-&R> ԶB :Hh|At%+¼i Rauv}nj[#n7W0;f9]j~G}ͬ0[;{5/t9`ձrف!΁uG-+|VI ݬIFs AZ[!Kf 陳B 4-#5: HK;"{AdrWTI&G=xxahWV6uEGф%[B:o;4YwEdeTPgjXA]0Yaj._bl$vZW%d-v:]я[g^h@![G^y܋k CY5${架=3nH?9R]FN.r}HxlpNGP#&}?ș L-`CP:laQ Ƴ31Ti#wCa,pI%Y#߬O66eubڦbxpƌ>Sy:Ajz/;6f2_{>Tw`A$wy#TnG Dc D_Lm熡ToMVq ,@cc`Vsz|=Ѡ땖w }>idH(R~b|C!ۇxk: ˼67 Q: 2٤=2;S3K#%)3YE\iYMbX5{:AzDƸC?\{;D`)p5z).nkf)1\ý}AIZB oP@>ߚбty洢\z? + 2WćˏXF`dfAnq@HIfDQ57$"<20{pAdZ$P;b}y)W"OW9Y$q,A컲) =gE7G?7EKdc9*DcVq@qqzp 8}[#A7|d1?wAW!/\NA"{)N$,(/Fҿqt g,%_ї=qp#e(T?ZShUu§+jV zDuQfjztvoXa V!^Г]2ԩ g_kVAwhCІD :6݂[j7"DZs5n`hq5Gw~G-%>ѧ_uo_no@2_uu&r=Do>z0Gv(788u<}ހQ `}+I;EzB"9tӓ9o9YbapET&Gvc= țKߊ3p٬9*}EI@;};98I?zAEQ摒'y'w3130hO'R7}_"@@h!O=47rޛLe^x~'#GaH>cAm0N¢= gQ]gyu9N1ue^Hp&th9B@ PtOr:1A bt?W6.%֏fKs#{@8eB,cVzi!9؋_Qpj?G.L18+l#\?q}C68+yb/CUxFŕb VkTGr2YcCP ڵXECpD`,r$\Y h i񆂫Z/5{Eڝ[0<;wTB`\K% c]Ԩ*.1rӖx9bθK8Ki?7(OXٟLϦ5,g8kϧ g ZG[/9Ϩ[Nd[>V'm*9y3Q~ُq9Fq3Ok]WbRih\+仆CsXA= Wb(S# #>QN?rR_4O4C8`GN9{ 1Si_uR}2!:oӘg5*ɹP}EqSgۃ_H[2g|sՎ=avߥq#zN|j'DZK@wYBG愃{Ӻ[/t\bեfl}RivaɼWDž'e9o5OTˎN?2ezYo(qaKLZ8ca)f,tתSɓRM=~vukoi-yyA{l~'g>*kIٻ3}Γ!kScabsI(S!O.{>sV%ۉ%ɧwTFY6$OX󝩼PG 3co9cSn~zbOmȿ4{p+K2n9miOpڹ}%ևwU;c?zGgJ/; z31CnU[t <,).jA+LPYj,~L\ _H40"s. ;1<OJΚr=hqlX>u]aY{˾& lW{˛ |Ws.hJ{͆E/$f'݃*}cqƕ{ voϩP Hͬ%싣[c :0R~0uToGh& OalX2t=ZM=4kwb)'orRCKZ@_NwkKc[qdu`4I$]ͻTYMx,>/ ~/Ů7lD$a&No.dVZEfAae59Pw8FB8vQQhj+YћtɬYrLjТ7Vjֲ610pO8 ̼fպs-x\zб|ϙ`dWVz  ~aղ&Թ9zd\Z/mf'71xe}  -E)( e,e7r#=2 # ;*Fj6wfThD5c}zԀN4jB_-ZutB.t;5kgϨG_C 4 ^  2`2 `!a*D,s!C"`1,HY@ZE`?C9mTB5@-PG/ p +pA+@;tA/=PKiRF)I +cth_readable/ebin/cth_readable_helpers.beamY}tՕhdkIَ؞XJر%Dz-DZ=IlG Q,Kb$@1e,)4%SQҳ-,˲ew],96lҔ.]N[=îϹyw߻wvh0T~o&9c0N A[$s4$ hx". &L8J3G t&m0!%cp*%$'uuT$ORȄ DAH#7L !$dLHQtZaŅh2*M89iT%J g:_ߐ }ȪZRuĒ jN]Jgi]<: 'Y2o&7}L*żdJ5%dBMic}=ٱÔE8tp<+ƒxl9!N$3ph" )K6!dr: ba8s2Cd1a&rBl$pX:&E`2O 6;5 2L6]4 LPm(NdD(,2E3h:L5c~q6:.h]+4B۸vhdmJnh+4CۼA -+BۺZJmж[qIpnלݚl""$gB I^SUW 28(JdT0A1(A`$h6pdI33310@$Ѻ#xἤxGIax层8f7,1.9"vR(N9b(85Hik׸H)5JF58FW;@nvdArݬe|MtU¼i)~bFRf)YKXAѥ u Jz 7X6T;]A\Xyɓn错1OCz Iw2HvlNIьòw޲Λ1~dz<޲.hSmt=)t{49>/%ܼQ (h}+ Evqd)0$T6"td^Aj3 0 j* U 8DmpަܟmMp.|&O&WCwYϳmY6P:wӵ?'pNݨ¹Εs- W\t4p.IPnmC7pns) [k"f7õv\nS<ys) Gk%67݂v;T3sY|8奮۩Tށ:Ep塽*p|_Η4pEP^7݉ạnp=דY/Nychz8At7e\CVz7׻,p>R5p \v]nI p{=t p}9e)Vק%~7 @>U spepJ5}50~~<0㼤ƳP[,$qX:K0a7=*e+jT،y*33W6*:۷Cjߡ:ޏɛɈn#gx:!an^ZF_y|1$,%d{ۛg4Vb ӆ :oLNQM&4ţΨ03xl©)LQCy"J7&BL*PJLF 4XēSS0sTشEb$+E}x6*u}A?}O̭ wC|dVӆK c@Nȟ }aXN"w'FňEVT֡hBڈhڌjVT6Ԁ5Ԋ< @ ȋ ?ڍ }(Ec B8 AQ4Qt M8AIFEэ ݌NYt݁Dwѽ>Ut?S:No$oGY(z={pX PKiRXeV 1cth_readable/ebin/cth_readable_lager_backend.beamWklW;ޝY8wwvƞčkػyخ]y5VV@1,iaҦJDQ Q@R"$PADiΎ V:oXz-ڂkъSrJl-fCw:n٪ZIwiջ[4ҽѮS6hȲ~jM3#^KnM6-r:[qyЭvAhídMJ(gÚZwb>HH/kkYHϯvtݨÂִW } hlz2C:y|0m<+,Vː]K ёȌ gTXա-je8 3L;0[i)$)RlAճYY5@³h'4PYRİL$Γ9FT<#KZECE)R˕+*jit?;pub9<5"3M1>Oo"9i𦴞GMg$UrdZ"(f9UL,~E06R($ #ei4YȣH)j A$bC '9WI#''҄ 75;M"*g$-ک4qZRH[פĬ^[J P:=M77ji zG@aw>] %*'bJH K%U%4.&I)I{!e(ZpWf1q+ʠأDd,&nurLƁ蕢҇<GT Q%º-$&̌Jj/ 0(cLYKr'&r^ٖ}Vd-~{6Yq(d}LLC!BJT ,@\vfI^R )$hKDDS4qG^;ʴΙ$QE"# ZWew7ɋFEjI:Kr^)$'x.>ɺ:)L>{}É%*$`|=fͬ?/*,O{Фby0OCU8 1FԵJIcUFP=G]h$>A0(%$C $L3=0,&VG赠Qv% 1O+,k 1r*A15ItTݓ5?譂>`n%;&+@rL&yAy+0$n:0ID[9 'rcm|[\'] C+u j m v([zP ߾i3MGAG7MXj=?T<\ҁ߾`~o8P7 `4fhi  tPrxQå6IW.qN/H#ߝ 'ҟ>֋/e3gm0oG3_/ts֭+۾ }qqnܧ3z`҇?;W~xuo33/}.QS_N;yΎw<>ukfB+Vz߉b% '(@= n-J&4#buA=9^(h\׏3ezfʩ'2y)^VD9RX]h&f:FspVrUl,WEsU1+Ym t%^x]?))vɭO> XT겋G#mRUM 4xJLlb؞jk qjmյTY)|hc܎Za?XlUkb[S}q j]3 b}qM cŏ ްbN~:*}V>ZwQP/G@-ƨX/8X7GU̘Q> 1\C&`؂RC`V,MR@@zr[fyY) ֡ƇӫjoȺ>ݴNT);7Ma}BҐj6X9uOMooj,#_qMD}_@(|US9v?cPMM2,I0ESR+K#ԋ)_MU/qeslQrlA_~M֛\h$lVr16$ʺ䆜 ~.r%ץN sr59wV`o<],J,&O?~^/+RUzU5;2`B Va{!/#{-N;>^_y+:kE;QyY?J(K*,Y&0DӠ\!+Yg<j5WqaOΘ1֝oTXC_ọ᳹Ns0^?óMH *Y#W;dK'7d;#_G|WF_PKiRw|QAD)cth_readable/ebin/cth_readable_shell.beamWolNP$%Em1*&#C-ˌ='t mEH*;s2`2(9@~MCp9ظYZ[h]+@LiڱVjj:ihZWRKp]oFfOlF Uk+75j.X2Wi7MkiېX AWNuqq@+z omTmޣjK3:.dDGoXdž~rSʾg74`C]Y½l`,M ,eW9F<<)2Ž)aTZ5TcY*ϓ%Q{2Y%B#/JlaIJ^+*dmXܱ+"wYyIUxݣe +×;)"+t9][TX|-#5sb?/Gw ~p~BqDdG<-d#B-Š #)T%hɢ#xxōG_=9pGb 6p+^e~N삖GzIaָnN$p&bt6#Ip8Kj p( :TGq)svREEF"3L.#U8DG!XxXa[)(d h<嬗{LrDؒؒ[)x E!)7@~LF&h,|aqA lsƙ n#!?5+ %S O:Ǖ@A$T>|dRlP`@BnB$qnQV LJď?T/P/}Nq,>D?`E9lƩwڕ83D&'A} U8y%X^'/GK) l34?UŧymLKH9} PьaT'R NT'MXsHex<E OB^CCa8DXd'2E"'\}Gf`}+ASxf PNWWYޞ3@X; ctTp)EAQsӆ'y'y/9%Hw|<U͊f8U/(T3=t>\^iO-VqU HUQ갊OY;,BβdOr&8G m: 悰*\|B[ `qD`! Hozwl'NQ?'&4icx׉7v16㽟X0çb<q_"_Nxq3xyw3ִƇ`ARkT*Ieva'ՕjŕonQǥo=W¨KFGk{$'㻎c8ȏ]rO x%#_X=|88h /8fM @[,=wn]o2ze+fиczkowSgZ(n2wͶnA)i6mm8gltu 6m ZP@B֎4>n/ PtZ^6X9(&jntgZP% {>,dƱU'*NsIF&,Ҍ`bbzR}֌/yu,H(0$ 0@fH0dz I`p}eA'IgcGDG'Da8ly%^zaWfct>|/ppVH?GC e?sJCEH /(y<ðD'?At?ɕqfMekWTdy(y|QPgp@xtHbxNLQf"#2zPd[ ? D.eƇd,Du v$\S+YAÒЪ|kC%k=d=Р ]Wn@j@nTTTZQ+:+v/$4j֌iFlvq]洆_Kc̎A3^ V+c!^P.kᗰД+!)|8!J`p^d"C<~ /wKnt`9XL#`,Eqj싘|s: P)c` `ZO ƸV߫"bO͜?~by/U0,/8iX}&%bĪ&j7M<>ﲫop콩 mGOV{?!JyIVy}+˛GC㍁t *mg>?'cǎ/'"r3AD!o?}2O߼U?x,e+>ɾHcyh%h:Wug魱ֳN)>C]:{|(kAÎ]vwmzM٤N)56>mzkr՞rvc})!ɇo#ڜ17?l~yms~ q vwqC F>ٔrt1)"E<}~Doїm }vI7.ݣw7k[t~NCwnPKiR+d'erlware_commons/ebin/ec_assoc_list.beamUAoF{6Y,!(VU)Dȫ,JR @JIU^=ukloUQ+BPe\sh)R**q.R}㝥\j<=όg]?E}qBuB&K̵$\;lvnpGBnxExk7r0JVQN>h ;7nx2U7  Xi%8V2uz퇭 rVlΚ"!$?Q'cvIzma*.kR͚y)2f NU6N3󪉤լ&kM]jXk&Hkv@ jT>z#:sXŚJM{5.mgt/끨)X}Cݬ J n9?ݥ\WÙ]-lTed)By\qƭWJHra2/CY]q*`0% "{T4MTt Fh3Y U Y&kbi϶R^>UCVA2x~=b X?e"SƁUĐ=9i2S4 Q 9_otL/CUeSSXG͔X c=^p Iygޓ 0 icaS~7&(`]>~$c{{-/kJ ?!l( @aIAQ ]Yt@{ }]=KyLԘ㴈#VsNqޭF5qyqk^ۓsOdGZ{~1+r[LB"*;fZN?H+߬AvnD+ 1:FvsB^,IxA:]yvz^NusNR1@ p=fqŃ|G$; JdM'TdžFfH'ruF>%7KuDPKiRPN $erlware_commons/ebin/ec_cmd_log.beamW_lIu4nIMɦmQI|i;x{㸱w U[Rt}9U8x@SB xDH' :S ҧo?;N_]cU lZVdR~X-VeRh9y:-m[v`#obze%_X+];ZJ1u@gb0*y7\ڼA+\6M\*]4J,h{V&+XXikNuwBl:MJe@P,$x0  me ȧA< /tHtHt#.𴋏T>'K L&ӘLOF]R[/LLa@3a6Kg3@vyI$xI$xI$xI$xI$x 0mR2LJAIK4)TfAS\ת©2LC/4A/ ziK^Ҡ4N5Ҿ^?{z{z? B+\Su!/stt{/8X{Oy`a'mR6RT9uC l#USKͩ.T R'TIXrl.Vy8 ݀#Ŀ~؁〾9VS/x!@ٯ{|>?pt@ u3~|7^a!?E߳ǃ~]Eߛ|E?}KDD?6A?~U={õ./|Jܣ՟F{paK~"n駹~;=}Zk󸩾77T#sVss<>z N½qʑ/TlWŢ ݨ^ÔO˷zV~wׯ'/+@y˗x~-V)5![F̮a6,{-fy;(r++zʼnU1q… ' 7L_/W ;n[VエXd3_5 `uc{7FnYOBZ6Kln{kB Lft],TEA9S^6a`'ׄs o7kV.kµ'Cu8535?s~}r ɒ"++uM&K"'? ?$!s=-rJJ^$/f"PKiRv! erlware_commons/ebin/ec_cnv.beamVMlEI'IڤMӦiUTLI[ {lb:M$$R(q VD HU@T!q@8TH'our@vͼyY&:;?Œm#ڵ|6om3Y0mymՂj-kVI5e2kFcQ7Tk# %jmŒڸ V^,H7TFVM˧h%M581QJm1"q<6@ӊ\XB22#Z;'EcSiH!* n46 &9$%<$Nֶ#Dh<]Sb4a F !€K(8ȋ</X& < r 0!H=1z:cgTkT$ ^>p D-Lo6wfxچig:^q@$f 0 m 1l?0ճnWX?e.~G@1qla:fz3ɕcu3އsՂwiQk-ٔY\\ <1s?9ՙ٫ߒ'?Xk/^|yqۣ* ۫k'_oqtm}MRW~ s YI]m^ _tE'_wG7 f"Pg\O7aWx?%|8zPKiRRpQ &K!erlware_commons/ebin/ec_date.beamwu<2>fa+%b-JHC mei *H2nДԭIK\)i+Ғ[bx-sf$l0)k쭽{31k2==Lw65n2U2L5Z4dMcx$KE[&Z:Iqmk2u*MqUEW3}tFTl>Ic\z,hlzRTFt&-$UgS≬'T2{|Sj+dct< gh*ڸ Tq$̙ZƵdfS'd?ReMK")Tect݈lhMBl5-#Ur*ϛ%*oPR}+[|^|CL m~hڨzB́MVك: (3(ce f3`6s{ՋGATFIlah*;`gv>MkbbKН82Զ-:.2\bRmƅJ]T shS;ԢO^Umf ` ] =KF]v%trŽUM'lk5a'tH"e]#z7LcZk*nDbAunTj1#6.6I&nN=j%ԣ~;{M6VĶ;o/Mжr3zK^U^a&#h]=JOhꪫ"GԮG^.#z"2Bٶ :1tosiʠmډq(lcJ:ըM-@\D!r@;5 S愐GnC%Z|_lB0 ^U OPy=*7<(5O8JHks+ E'U>#nC.cY{vAcpC٢NKhP'?щ8q߆\E1ߏOq?#N|B''1nx"I) '>!be -dA[E8ı;i>3i7|=>GwϬ:{vvAף=bd1dQd1^Xd1b61^Q^XlZ4 &> 7xb+?~ƛzngyPbNXqiT" k%q Th<q N\B`%t ǂ(wPqBwA E2ꎋ;l+s-'J=Z\7˸RALNI'$x Ѵ胯._u63 lfPd(RfhSqW!f3-Mq9g$t 0:)tFylh}etin4 o^"t/yѴ}N`IBAszX4^gyƏ:cnHү1/~q]A,dXpz W|sN6|>NvxķpГjدoP R+<ݻ"|Ǜ}Z:|3.x5P^gFט{?˼]#kfZeOYtﯠW }rS33z߃޿D>wV),~U0^Zαo5B$la_+Gk;Ƿx (¯M~KLySjlOwynf]:'y[ B7tos7pH%\8 07 q.Tpq!V_].q.p ٷUwO O ?p6{b} zF{q i;y +wy0C⍄p#%{Ql(s#[T=c.pM[.x[T!|x\#]H#RvK" (U=iQYd\ꀟJ2]AXYVǕKO Pmv fWJ X E^]Do iCtnk(׊F4({]{5q]Zlǽhy7w( >=/xO=z捵ul=_CۢPԸ_Vp]Y<^ `oZ{^:`=Je7\^ ~ەe\߳\djQibqa CA'0Wt;W-xuz9\A~\Uv25Űf*;W0gacccv渝93N< f<< TY7 -uNRvv>3Χ5^t,ku _Bp-~eQTE99]C zKˊ(OQQ?/EB2q]z'\;[2?TRezܷR%- B_QA *B>nTvMJk+S?!#| Gu FMG^MG8Krz X٢^eE=gw=g8SYy|xx&ʲc0$ȑCGd9`ey$p"'p9l8ipp_kVH3[zOJIcԽHݦ =nU'};37fgrҡ(Rs._![`W?d=y(οM!cpo ~8c|ΡCGQݻ)u J؂my:,tD B_e0?A#hK>i:lm6[v"݃A:$%Pg&K$i$m(߉D4cD}@O $rC1>6ʏݐ aPH ݃aO18Ie`z:?g?!''4gQB} >dm|L.1Yc6Tmamks|xhfR(]KV͘?S1OȘ*0ڟI'\U/eX*O"*OݓQy5:"\xT?*ǤDLUL ݃τ ς c KNr/Ǔvq =Yr#$+ J5[gd,PkP:XaVl9԰p[5AbvDf-[ - `)kVVBVU$l;AcQ;A"f96ĠI ahʱt f! F#44,\8\H?oY.G&)/qeLj1Q3>'W:0 S/,!,ɰj=xLuƐ .J`紐T JҌTVrS/ͳN=8jziA*U=.lMvs%aDY1(wk(wk(wk(S;W˱-f;לc爘=ncaSta`IF \Ȕ.91X29pemrrTjTZ` "*bd 3n3Տ2W w%xa]cL塟Flm _Mߙ>N73}\6~g%nv+."HCPED@Gg*hK`mnF\G[jG[uᓣz\*j, (LEi~G[Z{]+}ҿץ p? ,bfGkc9/D]kZ:޵99'wjHkȩHkcGzi+sĵ4BĹ6`lj99(`ޔy'[<5\s0_i=z^I⾏Zk(uֺ [OֺTWhKuuֺq% k-). H@󶈈P '> Y 3rQ/td-̱*2P1b>_Pմ֞J?r.0SN NMbcņ I虜YA\D\xr H:b d$eCҐ34kpH6L4 \O+pv*̢_CߠKvb;~W/oMW]d}D.)txj`Ě"d`Y0@@hV/Fxh,kM\3's%u2MSlJ%]fղղTuZw˃ԜP ?]C|f(jND\V9?He`DaHhah "Ѱ%a?42aٗt5~>T[{)\Ձ{TKh$hkvKMkznl\ǶFl+܊3qn-Wzam>kVa),ݍ kWyDč +ƺú"ҁk5^x'B6Etc]-^5VoŇtd:Bm^ E+{ẁP0"|^(s7xetn]3 `( $MeՁ7/Nʹlߊm?]m!<=!Jxq.d"!cf4jQ3z ݧv{#`aZ`ỌkĄ*lJ|m^fcF`ܺ<S,ah~~AҐ//ӇƐfvjٮ)ZpJj;!ӑ9 J@9:RF^f{xMѳP^{jp'qV4{gNooE!dž}p^۷}w}h߻Ts1S:}+7<FXgcՈ\/Hch\$X@#"OD{ѸXЈJc?BIBYFiP? RJgz~J YDIϡ"ϣT_8!hJKh,ƗшK+h\*mPTrA@{L z RT&q]v/쒻 C%Bћh'PFbkԨ EDGh$m26S;M*3scsۯ;w9y9Gժ,2ȕ@ K\Cfr:2^6\ⶇ!ip֢{G$=7c#pYg Z!x.8K RO|axG30l]dY \Mts9t]2]oXFkMIiml(hb%>BBN-Г,g;i`NԬF$-ka!r! 'u[t!"qLgoŚmj[ĵhCV yG ҟ`Gop<>PyUWs*cĽ f6}&S?6L gTS9? t3@(g@$(L$+$J.qoC"HBxCişUƛ^>2[4Ā2 y[ ,YKjPU-l" dPQF +|AQVS QpRzI(4PH5s)8Y|*x1\<8Vo~6ey8LFBCk$@D*;S!&QNU O=`E-mǿs7My)yt1ǧ4NMJz1ɚQλƪqϴ[[ Mf:7%; 9G3#{o5>Uzgs3>lL=;{ˤ/7DIm+ҦU6u&Vnn`\TڰöG6g#snq_ѯF*Ns c>1 56):mDr?+kٽ:?nMbzLMѮ^{Ϗ5ZjSFy/ thͭSDS֚U<_Qi{Eձ=:n9gԫί}6?m[S3imcbgť kBnxpȭoJl voxc.Mf{ }NB"ʃ9fS]@*h& ʗA)_USM|tw@4>ʻ@͔@M>Kyߤj1,yz}un GUb ~9㘲UχWaBO4G3=d2>K +۳Sn`L1aV64DžiKn\rt 4'9& MDZgМ'%?#ɪuY$5 9Hu͗I7#w@eq%Y+÷ry_@|W]V[siҐ4Cߓ}zpYAiCҠGGN??ރ'zJ_:jILTOΉZnil| u~46/ij[յz mhWy!f=Ϲ_JafTM w_C?*H#DE?evz-CNj_|W*~ ku~qc na? PKiR'erlware_commons/ebin/ec_dictionary.beamU_lE=z7mP@ jҤpr 4( ) ۽iw{^KMCȃ8QB%!Hl  1v+4Mw7}f4ޡCQ@} вj9n5rf]:eOAY?yZqwSGLPXQz:U&i؎Fg!Gԏm h*t_(<7 (aG#pZ3wCHo48!Lj']DV">A&f~M({NBQTRh"b}6RI(Dƹ)LEf5J'ɕ5]3Y>#ii]:_<$3빧 )|X+]* \)LR(yz?a & \\ʕpI5s)°PŦa`69s3"~SVoVZIZH\[Q_n˱8CYKbh%j,Y?~_L+ ]jŬFYA@V@A ʊwsF/_?^ vry^#}maljokWTX~l'Ǿ<ևcS_gx#?A{ƶgt|1F6ߟe|EAStp`h Z:O g-ߧ,?zъ kUi[nyQs+yflFjh@<66_GdAH(9"ݠ@RNJffNr&ZFp GL( 9}ן -T)c[ջu1L"dյp/`mcvoۛ5NrI*rT*dx*r? Q|4*U٨8sO{w޷GbD$6<:th\KRMI쐕g9޺-KA;V"IY;S,)8S2\ yXS^x)sgrB"ER/>3 #UiqzE'[>!g Β m˩q b[|!mye tnqٙWOi+o.ya=|zT[N.a(xZ ( q*&z`O9r!8(;Vp[ԧUxi>sդEzhYrV"etޱf*|TDy6DeH]LJՕ;BdV-kzH\l:N-ekrœ%e/dBe,FT:/m K%ز별x"t8rj6ѱETIBI&0Bh+ !R| Nj2\$d^.#5}AZYiI>1fR4\ pڶ:lu(|%vbNVQ;-2qhQ4lIekv 8C-岵 YfQ! avwPXVǢC'h6 @y0 x83ҰEGW\H 9YN{o;Pw;Pip֨!3!^T5)#&.uv.ۀ'=9lM8ѻݴw8N0V*4ܳfÖ*2@;!mprcX5~B>PG ux{I-2Eɴ06s15. bMrYwPR`yN{?[4)ą4h0k68tGLR%u/$ӽ3=u祗}~]p}!J,xOLwY?Wl{}^S˶BbBrekl0h!Fu0[&3r{!DeMXIo$=qĔ\vNSN% P/zT翡(34 LnziZ/<$_pIjN_W_^n.LMCZ 7PPX`vpjͫfs@ȠSfG6* Båu " `=PCŗ/mamu68q뽥췐%]eN9n픵/h~%e|a%ePyTZՓڸF/6J wL+4C`A}A2 $A2!y,_w $6ػQ ױ3I? D[A Zj2" V亐AY M!CQPO'~W$_@[}|w}gk֐1?G):/#>& 4铟ЯUégHWkX JkPL<>, S8h)/&W [)xfS7 [r꡵DItI2Ul{wRc \MZω 5ybAHR<+,B>+p$j( yF~f˓BKMO?HOO|w>r^wۓo|G/g/W*g?tOl'˓'N.X K^{[?ٽ뿨K?xjZ: o׊rϫ?sϓ?7|XF7@)*( ˠ/ |d\C:˿Ϳ>]C//_߆bEFd+FXfl0zVci6ac5IqHi#c獢Q2%cިƔat<،Y!' 8sB"{ɐfn4Q6` 3m7htA%DDS;U֢^L\笣!4&T_tL˱姶$n%l}'tߘS}̤FYh`%2NzR;-W|[H(~.'l1~@"vDBuO7/~(pݶZa<=Z]MURr,HXY֊=7:koQJxxIk)+x궂\jšM-O=̉BC%NYDP ɳ w\Y=l.*'J#撁 9pU sx'w|Z ZInwڄG R\yue iwnf! }?"Ybyw} -;_Em.~p 1!nS?*FPKiR5Ϫ $erlware_commons/ebin/ec_git_vsn.beamV}lE +זW iһʗ4B-킖|I@۽v{wISb0n4PLH & D *1&bhЈb"D u+MΛfggs3Bdv[GV#/7 mݞms;jLfF/|53S}O 6th\|ײy׌f!fts=H-'gy;ڀe9`k7RHq_^]|}rf/s!L;2Pw2/g&;b,;㔂3ow&BqEO,-DK\*#Sf#Z\݇[p S m@R4E;eImS" sA/L*8Q,)qPVHO▓Xo Ic*`TFgp+T)It(Yƃ:SrJi ZgksS ߠe`*u`^P{%]Sp{IGJzVڅ"ʍa> ` HS5( pZRG\*i]cZJZ" n)'VBg2ai!PQQY ĘX J|<RcEVrqb ,xɲ*-/ 8j6oz&rE7(NjE= 40]Z rM{915%RXhET?'b-jdN2 Unyu:%Msb4TK<6|E8fU8<@<<g|5kљ '=|qBGC\}|f; ȞWzf)t;5WNdol{=^~llēm;UߍO'_K7\xߥ o\z-Z7N=|і&܂[d@'O~tC]@ߝP{s犫w?ʅw鍮~[ۗwyv/ {hkͷ9rvb[~r]kJ.`k&\/g"C y hnȳ{QVwa؟c4bG5p6Ee+deu}L Cn)%4YF0m`C?ԣNca@)i"59ͤ[mM8n6lfJ@2,px']N|&M8Өv4 /X9 cr47A0Ӛ`#v:}T_=@ B:l;FqHIvIwrfBq)D@`e6>f lD>a52=E d89ffkQ' &˓p8HgzCi8Î(D)%R(s: , i%-f̏4#M:=T|iIFKNG:,cGI݁:]~%\y̓[.d< )B;8n6Maj= fm:^1լG#QoΦ3M-dʙ.Eŧb)uŨzv[ŹTU߸V J5(zJgBpsqYs=B\iKxVsv1!kVikt  ^ɲ 7a W8)Ɣ0,ঀ]3O1#Iঁ]х1U3ϱ1'igLͼƂ@@!l,z.Pͼ[7$-y};Wx[~Æ]WZmlu[Ge|O^v+{xH%.*<6 ?oSxz*,_QZ_RXEhE UvGas[ȳ +\RXSpˡmɞEXPZa=獘jq;_RH?J9Y/ooU:N[Qr 8"v힦.YW fyF>XVx$WoW8~<5W)j b#os|;>zeJԪ^.#nڞ賭X` rm$k.+WjA=Xd3P-; Y%h9vk*g*J6_km|Pի@U:r \Pg1U**[,Y?H ij|0?E"(CaS| NֱPg|N_gl8A_{#\c3pyL2wfTB-Yd+r>KGjǞͷdJ 3Hʚl5Sjf+/V. Hs6SΕSJ WϩFoB:ёEϘKD^].{Y(G7G_DϨw`/7}ۛ.PTGW\*U1U1-b6G۷t^-EW~# *-7w룯67Wѿ .g*U%frfm6߰ͯ[۴̨a-_ >M++V[4[[mZRM;[4WP7Zx jGM#Mǫpy,pr\@JM|>>6i͐\KDLhDp sp6v]'@MvE@tb^SKEX$o^+vmjR{<-~;ڱz*Xy=ӤkH}z*tO 9N*=NENKSA;,3yR'J[Z@-=ėa´EwpBca&&W>\Jck:. "l+͆>IKck56 OΓˣҐp<&$$@_.a$[s$C"NfA34ɺٹ#0ÅgD" ˖2 zIco loZn 2 Iz; Oyl-~ƶ~:{Lwl|'tJF%&޻j~McbBHʱ>> xSW!]rC'!eBc~>zNiCKu%%Fρv !u.W@жSw zvCt{ a+z23lC:x=W,N4R^نһHWOMRvLcWJƢF6jy&sE''諐WyʯN3fca iS7tz|$>lD}-Ȑ9NBH[NH(q>݊k,냎ZhX8 م#KO>$ZRe !ߐ1dMIp`J&0&`>m`f]Ŏgp1Jwׄj!jڭԠ(iJGi3;;s kqQdz} bzmk7NG兞65ENz 5ll{z<{f|*)6S&u#E{ 4@c3^PRhTKK :Dܩծ&7).iwNBϚ[olfC(QީFxE8-{ N+jr"X>?ߣH09쑢I6TA1ol 55yYfd wΣηy"PC[yEc4 ^J@m9cUơ"պ,#dFh;Zy ] $Ve˻vgSh;O[:{,h7ArjV{YfߢIv'Tg>d]\ΦP2)@`Y򩓽S+(|>t\;KW^Qjʴ]}Nv?4+m4no_1|?&[<_okq Lk4no[3v;>1@fwizs] Ƒp0Mpj$[8S`g$$m{hx>燐T*X8zJѽQC l=2,QkBH^ »${ @n&+=}| kk^/WDSD Gd >}}4fGLxH:ȇ<- >n];0}¦O9/9݄GpRGTY?AOSUN#Ev`L(yMQqlſ5vx#G?q[҉KK&N49jCj!M6,@h|O_أv|ZZuv`݀Y4HcPg,O8\ZC8P2oX:z U{.04q{50A&}]`;VplVT"f*ј`2<%D]F44D3 qi`]~+vUD e␅&~SB#{?zDvNv.M{eIac c}HG)훶S?{(%gQ%䕾<ц1iXzY=| ] Fߦ 8;@ύ"ONdjd%YK6wyVcGd[QW &O싒UJ#&ΘKG%`5c@"m݌"ُVjµm,>ߖPq5T<;#Y((k FuTW^d _/]CeN^?>uӔOS~_MiQ=m{[-K&yOclp+=W0Z$7C{*a>Lg[ҞU5^??o9n$LV?oz8^zt`OWh@(E>¿*jּ0Ÿùj d& wE}QEޏtC5`Pp6hnidG쌚X4t_lkߡʆG'" ՗~_;~'tUpPc'xo(E??-7P5&mK6~JrG ʃƟhǒ 4$kĶכRd^̔t`??S!{S,[_;ڍ߻Bн߂^G>NcAqL*؟ y6 UA>CGݼ z< @EDNѕ{9Z%p7Wqs~d{c `[j Uw]0ԝ},Yw=!@C^WWê^{8 r Dцxo~pT[` $KȻ` )phm7f݅8XOm"" *(e:?Rm8GqZ;azny̙|s7ifO M-h~0˴r%}DQyIwT5f0fbs(s{GW `X 4Gj^T'F%aBzjT9`C_hA vѡWѰj s= ӆg@Z5w0 FG\qg44F & 8Pi(Ԥ|M@ESP 6i =4f#3\9@C_ftA^Я%{z4J5ŽFQr_hG[uJ>)_-h-R[b}#cEY 2u?[ed~8T59K VzGFhY*o4PL8*Uc FC'$5 \5b n~kN$zGatjLrc0 F'%!)NL-UBdo EcO&6j^䴾я0s:jDl2JU7-oi·פ o/Tû7p X~U LE!D.uF4e82 D?{Gkx'͛ 󪹞-KR'1>Ho@ވ׻Ub|{uEހgt 9W#üx!yȽ{!̍ A"ytV=:)#C&<.ؕ®Ƴk^_TYs EMZerCз' }+F&w>w㴯i/=K&Gڃh=]9=E/Gh=nS.iuKku!]%l3K~З$|?v'`w`!?6c5+wFͭ`pe]LC_4}Mj\Zj5֜c./J*ڊ\L@ys^_t1g Es5/۝LNB>)N%J|2ʧsgٜ~y/oq,?oOE/ ĿϼE(J1UL-j *v1SpqQ4"ma]b#^/eqH xSo"nET%|??|(γ?9ve+cvul[zYeY'K]6qV¬XcPKiR ,#erlware_commons/ebin/ec_rbdict.beamX[l&M&]+GQmӒTvTm%i.P#K"]Je ]Àa b} {-\tOP4P,^;$(JuiηðqawJ,JXk]Tlk5J4%Zv+_VytEkuɱpr- 8fþlbmwW- oO^M4U ƍŗlW-T/jضJzݬS] i6.ԱV Sp ܮUgyb DmMĖS`Ieag\khYɕB6%8UH_HZ-,)I_"D츙tY Nŏ uW 9v=<{/ﱷ9{GU(~~ȹڠİYQ/Rd2)+TԿ_)= P9JB0BQYAfTwWɛ)fѲRA9IֹsjRblf䌒If!IvBEXad|JQ":pQw}ʽ "̯0h%_%%V }/4$ab>Hbn;oH^~3c'kaE9KA% 06(pI+)Ѓ\v8R0sPS\?p 怿1W!gaQydp 9pJƔ9jd|hL6ummaZ: act#7 )qrXYI- &ן%pobVn% g)Tx YKn?ǻ/5^5#[xOܭ!'O -K 6?^"Ϧ3P v`B8+d"Z=!|L:{Xǟߨo` SNu74!p*0ohq $SY(V0à1@c8LIHHF'dCN9h&rD9C3* -P>qzBgpO[ςO.( @O .4*LNb ߉y%"Znz1szNsms5M IYpfFf#s{1eES VhKHkňYK`u kjASa>')8A=-}7">"pCR(! t閺MvŨJ/C~:NWЅY#HHOC^ =Hq|p@튆.jCO!15?5? 8"gqrV֥^/ջL:YI(9\fO\$βy8r&y)rr)DNAP"ܫ+M-m%h\/>$WrMc%ۛD-7Fc}CohQF |AQ<( f.rcl>dRdžeWg{,SΖ=/mYYNϢe4l9, [Ӱ5 [ӔtCx+(g>W6ɫ 8`durpf3gQFXkn=(8n^9{Ćٰkg{@/H)aMEAFAJGP=]8h»G^PJZ n^CLP@ qh -$u I= *$况47y#+!yt ~kxg&[y2^:v5f  "thLj.)P<%ឌ'3f7;Yf`0dSx?4 ڇӂaKl  @ 8Okq-SI"SgHZVʁP|$6лI݉y=~!XȬ?4 y@ޫW] tš%d H tcyk@@OzKSEt7M8 8+Nauw7FSԧuTI椋Ӫ礻;@4w\=$?54k`LL;( 'I/iHҚH:)COJlaVEEL5`O% n}j̰6TN{h4s3$n(Lf+0= U&4!뭒|z 3– \-]8\9Jl#=Y&`{3 ~Д\vRר<MCHBf0L~((Lytq\a0~A'x<4F:%.5]!NBA)!]RJh<">wA3GaeX/o1i<Σdž`NZZJ/Z& p%ܛIV%9y-C3pࠕVNH-Lg8o2j:tC08H xCExTK*Y$aLV s4;IjiXO\.rUdGڂ|5xB0ٽ> ݒz,<jB6C[$p\67|NV}Tƒ7qS;֦6w746M6K`< =63Avx8v/C;HAk0|-^Ëє'MnTrT*~7w/ ?#; ? #c߱ݗrGfp{^^/چJq!q+ <%g@9^.}dN -n47TsWkVu"w-sgwϽk/DD7~KyRՀ9m]6xq| ,3x_ďKQ|_'ppc/:~:WM _/oOO/kW-[{.~ ?O"mH -"EG#"?PKiRN!*erlware_commons/ebin/ec_semver_parser.beamYmlg~ٵ/,om` 2`B׻}.ܹ܅4frB8zJժqB6NJRI?T"]#<|n[;>A8ǫ!N5KNJ>C FI]B$iR^9Y -~K+Ԅ5A:/Q& |GnH9k7UůaMhr`e%ZDǴ M+UUn)}/_|U5 U^F%OЧ ?Ae%DBxW7GKY}c"##2OC_FyytTjYKXX4!n=+q)qUQXe-pK# uUq OV;Zŵ"[lŃBF]{]u:"dEpmZ12m/24<{!vs8ffgmv`uCuYbCVpi# MN_ Ao4:*d I6>/i_"*.-v Eve";;pzy" B͒ , HKg>*ۭmT#O, A7',}Kԓ'111/"Ÿ4t0!>F`X MLC ډOS}1boT@9Hc]rqeCT{F/y"_ghfm\!}Aҍ6("Y$;tYh=i2J\;ި<|y9"=EH,1>c4<"jit5*$#&#IuH#/S[ۋ()T.zd_Dddr"'?Sv%anEHS⤠KRYaK`d8!bZP8}8HNf (b )V8"(X)@Ӹ!,Gxנ|?6c-Mžqq*N 'KdK(ulCNhӏȁ%2'_VYo֮_<.ta6'78kqyiAk"૎fU|1-d9G3ݬ[b'fޭ PY>s><,u4y, omq p'%pܶ'[$Q0T'~PmVT=@c,=Pkx'5'Y)=UA]R;(%bz^ eQJ^FWxr1zu _^B A tW1H'_s>'_;^3bz Ulca<<F a"Ruv oš~PÎaۀ6afMq6Р#zy2 15 pȯg~#01q=GiѠ;O~^|"G6ѫ\>Ni.kf*/2D%tCmkih{Β.= zx1= ju~)}7\Ȅ ۃyUqXHQwĜEbn5(ϓo9km:9:y;$ {occ&|)wOn;`w*_!0(ˣsG>S==A3c$\~L~4Ff}" tfuK\&![2&v6_f<~ g?k.0?b4ye ~Ue(Mf~e у3܃ ƒ'X^ >pOX3rN͸W􇋟f <8\<眞ts s_c9\,oaziszų >z>{ӻ.n0.FaM.~,Ww.}xnh*zO(׿+m?ն¹_dL3_"sgr|s賟=o!˫G]u]džB=u|_y>j;VJ#}V#|zs͛7Wxτ nֆ5w^{ YOoMz Y=g߽=ko/{۳>ոU6xs8Cd8vދclehc890-<69@2?,^c%ut=L.r ϱfn9dsmbs*'l[=hzz8pJѾ)J'z[i "jw U@Jj&gpzt`tb0;gKLiL̘{INrLgƘJ|n0)HKvb[[,%+l1~4e6*jd8, J$F]L3L[>U+&+Tzj\6bLU 7[b]uG~ rt1\}d?E;/Q/[XHt^1ٙkcsgg??TPy?Q_KNNrΩh-ycƘ,7;% cmXWRAl"zL+v=G:T\K*5ZvaHRff#d.f&@ єĉӴN:&LNH%E,]0y ̱Ol!Gs|< p-\r.{Mˬ0B߈TUn`{"oPKiRxRO[ !erlware_commons/ebin/ec_talk.beam_lIwNQDYhīq'v̋m);O\o["dn"a)N HdDi^?Rbysna=t3Hڤh%TH}R2*)Mɒ{ϯ&irFhTjH+jC:pB:);Ɇɾɛ'#EOVirҧД*TMєd*vu$~){,Hȋd#4(liZ"=4&]Vr@D]äSVY+$(5x{ؕρ'VsU] d"ҌDFI Q[@"e @ K s_D}$Im#QE<4.A~`@$1le㠋ڠxP[zO]K">?LF# $(dD g1B2xQu@.5 Zب.np)Ic ދ!2DN:As0"z$W8/oh !!:z\awx/#2ѴotJ Hq c":n Q Hf\tH?*(SUI@b@檫R.2tgE}@aDؗؗ|o4H ~qiU{Cu|ȁ]}:^m% 퇸>ρxV eYMxSx>}s# )n c|ǬOkY^GG9|?krf+]9G}WO/`w*~׵=߼^?ö;}m/~2zko~W/],r6b~mrw3~YYVNԟCwk^_S7?H'Y]m\g1?DZY/'HMobUg֥7co~O7K?`e 4Z'WTU4v4 ;f٫qdmHg*B.^256Y,Bи7ʜ WrI_3/ .V(YB1dS/5rigddf_k.^pԳV!i}iö->*=&v373j!g+A '*ƴظwebgU³و8d l+0);ÏO4>O`g>_ėW7& -| wo""?/s8PKiR>QW erlware_commons/ebin/ec_vsn.beamTMk@~Yw]ꐀP𰴂f$]*R06ٙttۋCC?xOlV{xNy晙of"@{ky!l +ދKvhi b-> !2? I,:t6Ic40# T(8ê>Mtj2O+&V6iX5"^km]j#@:6nf:JD2Acnp ajs.ECKG' cÄ?]F2^P4c}uu?XLgRBQ-j'HUw^CMyәW|}t٬w_*?0O)EIE*;2K p/zV~es= X$,Mm!1yJ)ɉ`i(' 3 \Sp(LMԇ8rFTQnGy$傊ΐG|;9߁)P( 7K'F.g.֤ݎm' 猏X;Cܛ9za꓉e-y"E&PUool̇0ǗF] eq"# 8kڤum^[Җ:]PKiR'Y(erlware_commons/ebin/erlware_commons.app}Rj0 +&`z(6ءj"Av;JOڭtiedIz4wht ]|T3q)B4sk1[ډpX r7xE яS0s Lc rlB3~? V''f̒u&M|ۜ›J\6ۘL^i/BR"#ّfCp j5|1MȃS1Y2NO1^.X$ ̣.욖ۿEr?Wr)o^N0`cڴ0 [F SomJ}|.f#i˫5b PKiRO*p (eunit_formatters/ebin/binomial_heap.beamVoDxw4.4[$d%8LlhE$f6nv!*U8VT V!qP !q p%7:"KOy{7osk}gkfB AwF8*~(C>~ՈʣW+r; ziI9a9~p .ÄK Iq^Ó~pjPuO%᳾4>m/^G=\@P2B@b.H2Y S̒jJͥvfܖv'mQaY K֎#;f Ylj 4%)=h~C׾ÛjRk#ڎZ:E:)ݤ^Jk;:iGwf$[ƺINj?=갘2F.=ab5ljL!<}EP~0erH4SRFÒGN${,joam;XEQqPp(rJ dRQmڡP(+G4{| K ~)_Ijj:DW5`+fH SR6Mn$)ӓdXSf e0-*C, ~&{*f ;vfIk,;K\L-)5,hyM44mg9L"~L/ĘĴLUX<.dLI^D$Sf+B/Ć" Lϔ {/4bfZh(h4lfsK1tT1lM2wnLY܅DhڻN L_~8|(,񡕛͟4iԮPI,NrY:FlYY@~.m-xqI h틀8"s)@b30-™VS{"mb`_ 6XP,Aam4FȔ 9jvZ-g (\\^ƀF"]!ˤmo5nx%Cutd$)c%$PI 0 +Z#Ϭ"Aonv8<7Hا|͛!ul/7Way}jQ/lB7D?srAJW/Ψ+jC}UP_S_Wo Vo}#Ծz'#PKiR**eunit_formatters/ebin/eunit_formatters.appmj0D C̈́ [{, *-%ZE;3a2`W̳eL[2HtnϘ,kSa%jWTulceIB `Wp ܪֆ> =sRP>N>q $VkOuUdSqz/=1'ziq6}[~jWZ89 qF/{є㘁, PKiR)/)eunit_formatters/ebin/eunit_progress.beamtXwWV֣3eW @C9l ٲH,زT6 $I⬐f{gzg}azZ Zn;B:Y*gNRUUN)+9)\*t:W(NŲ] %Bft4;Lfi;.d3+(URm-k`.לPez̔s\?6C|SΧ jũ:yg)T[rXV=N!KLqkNy.W`B0W "$Ns*\-d> JʹŠ3ZZ%T+,-; yVkg\RkPqՖtt72ܛBE#4msʳdJY kjNvհur9WuZflamrV0)e'])j/VrלGjVt4oYɧ+U] p,Tv.9B tѰ!PWKu* .ȕڜ8ּSo|".&ЊRBaS[EvHQ~ sfC$ [p 9#K:|2K\rkX+g*{nW+YV_V8WrUm!"P&k'*v4]e:Rx>Vlhoz2k0A'Ȩhov{dRu7zy=SC{!sȜfнto#e2o^J_V+%V;eI 9%Ld*lUtBg*^uoں knh5HuHZg)OV] Εj0VDNzheb4Z/r:fqGKc~[!O*NoC/p{j{YFHzk];Zfw{-E?0R:@bi\%ՂiV-)YwMmY;e:fqv&4il9⎇T(`ڲbGtBeaSGmRF*J*m]t v%-X5yHؿI,CU " [wBg 3J #GK J2 CwC8 ۊYL)[{X%SLמWDk͢4~EߵS~!{|xW]oIX??-դA\D8 Gi$šn\ҡ4ƴ@ȊK:A2Cםq!*%d@;%Ơpp֮A}O.l*҅D:1`֐N_t_n$\O,Jܞ[ =uzHK#b !4풍еfi7lMNZ̢0^$ʹb@ H*A}QB浔 ZEH[4&EmV4z)-xz?= "n-&= ]MMc9mCqVl-ͺU5LkST8p`H]W Mٲ>l3MXҀ9Vp4 ;tТ;!۪n6#cQDF'=8sHm%O{t;mmYXwm( ,4̙QS#"v.^v0yY"}. G .}W) uWx)۱>5ͲeǐW1WSF䩐u7N]Q՝i.3b˼KK=!WP'=mdޅ- n%vjoNMɭHE<咧{TI,.xKbXb4Sŧ]2ŽX53^^gp~lM@ >n]rL݁8?[~} 0K]0ب@@%Qvh@`|K!QC!h˹?蒃wt}}Lϲ|9aŁ}8N _ #=у<۟Ey6N #Db̨阨ŭsč(=Tٺfp9;,K=*6cX49Χ "+m q%aAX ĮXs=2FH@=G$~KLd᝱8M‡Il/tFp>V NddI4|uI2=NS؆vHl! ,P6"|9yl$T)99 IEmN=;lǼ|cu,`Xu]£hxlb!c$p ?KD8q~y.N#~yOX|s\eOwA L<:#&$f?2IKN 4qiE&yc gq8x-= zL"GYog z_-G!(wqcxEZEH8yz!! ?5˗xj3s,`VNeSO 8Sѷ)쉑z%O'>zJ"O(yN懩W=ͷhd89nmq'vӲ:GOK2Kґ@Z9-}>q8XkT}fdϚl4?phxQ;0X+pRɈ̇9͒#"π5-ZHHHӼ%ڰKx(B^$"FvI Dj@D&٤<"e$EҖ9,W$b$^W$BzUgQGX`980)S?? ¿.ȬTuɞ@$ݺ)[EAl0YZ1d=UK)hia2~zmnvI NN&Ni?dGK.4͉,Yl-%㨩,,jΠf%2fRNrSz1DfWbW8KBhAgafi61<^s{/Di^gϿ%oEYh5g`[~tbⒷEd$R\x?+X-cw]Dme\\Λ71е1Lnl?A;lT] { բӖ"ۘ/.]TL44KxXg u%Aːw%Nc<;LЫz.DḂž Z"lH>dP>,@ù#˨;@]__!'ίg}CN|%$2M [=d:ERӸ;jW:f{,}55^s D?U/*ӛjޔ[S__2hmO'[ֶ ԑ"D)~/ǾE18/Æ˃oT_87 3>Ev9 ^C3 gGgzYX@Dy90LߖWC,G y5݀|@~G@— \ޔyaW ˘bt}pG8^'87";ޟh.}1Xo ϫK j.vcĘ٘u/ ƼHXAa A _Gˤn_{ݯ78t=#~؄n7VC^߬Cǂft8tnBgY M~yh JSab?P"?AYWѽJmmyMTICld؇#Y_˟չr/R;_cd͏= _poS_ o䯄n{?η y@"@HRCGCmctQ9޿Yr7o"M߉h2x|%cq9וېuɹR9M98*T~^T>mBOft8tcĿVGlBuCԡ!ХZA[N K.DQ̌8\fhf5/( j&EAe)ڣVMQRPILD 23Osy޽2'"n;"?r vN9jQb^l.Kdk2,byL&0 uOe,ʅD\k$&IőD%DW$jN'\9RjrR" :" (J`CC,VaD6މ2Sd+zΒyV|@+w%+t3v,Q+d@VrW^Ijj"Kh~ˤ[kc ۑ*H}Y+w]xh嚗b81Ӄf.t%̲,UK-Ցj>xN;#WE\KjZO=M#-%P8ԁr2A̲N2҄rBNUHKYlrQ#}rYLܐ(Jw:)Hj%B Wen^tT}B_=~i#7kPn )Gb;d*U\g= ҴbDzl.%"|VTBμضKdD^3Yc; $$M{,e1E3KM#gO$t'` (z3M T#}iD '\g׏MH0F?ޤEc-\_ܱ4W߶ьAT#5Ff3fF)'/j'&7F܆_ey66|6Q/Iy4mh?f!Ҙ#S{' I7.=@vqz _I=(k{DP?FRIJhSy==e|ȕsw:u ٯ yTsպӲ'^y-y +Pj?NwV{.gJ9H6ޑ?3 %n70ծu<~ܖe?m _>v`%dN1=>eL}g=*[YxS]>LTJzoȎ/vY9ߩ- 4oUgƀ-g&9cg%ʄsQN|Wksmk=ePJKҦ}ǎ{~':oRKjRD!8}_OEnKt2ܚ{On|Yse?=Ćڲ%M1f{&=mnZq߹Nae!ٚ' X ѭ+6>k?ֵ'TKͷK O=G[d,Mo[sSв~|а_6 wyyK9Ч-]Y[|es{95V3qڇVd\}Pع?07+mT.wq +):}pe̳:_m/v>]U=p>-^#39ZM;$'B~}Py޲Vzz{`E%]rZ ϋ}h,w38yqVyC9];3h<ޯٮfǵ0>]9(b\bӇogt!z^ra3?Y۰)"dOZqlyw/y̰٭\4G{'-~ԝ3ʘpFTQg}`}wBٿՔBCq4^F0j̢AcA,! XGM)}ܙF q$GFP^{4¸ ߯48o8OZi,p*4E0j~B XLzSvZ0 A< m-*gΜIt 3tSLѥ٢3LII 3'N=&1YFY ޘnMcNeJ7lUє2M/Nq JbQTϧo4[,:YK@1ΜiS):S\ɨљŒ"SuDiz> }UM~^ޠII^xϞ=dF2#P{xx숙lFzI$قD) f7ȍ > 0&T`2D@8DA 0\A",xH h!R` ,E 9 ` &( `=9p Y8pKUhk7 nB; ><1+Lɔr#ى|vO[yGxZ1V3 kWxDSyjQcԳ7S+!XX9HB <ʥLu|dV+G; |skh[(1VRǞB)9U28+8iwR,u-=S_!w:vkj!+Tku7-;stV]'#%Ee*<BfTu"|UfP gDp`aQf DGTϏb>Sn}sJj~2_gv$?R#|G,b9Swg]RzyF0,G4[ ,f4 G}W&'jo*mZh8A4ZVKzR5͒xkw[¦NT-31A2՜掩R![廳j&[) Pnw%5G9 |i>љ"z~&GjHCaiKde&Ŋԭ-Ttz#uu#EKdfSƁߘԎSs]GSvy$[$8M V|]N-hVn/sKzeΩr!}W~|i/W#ȍr7Γ)twdũIL9:&|523C}~LZQdkp}d97Bi@&l*|FElևׇ=na /*eeT-,<6.g)t/tڡ:h8t׎vϨ " ~}h 46m5;n5䆤NDdl{ Өq F+BLP";$ Ix5SbX*?O-~}GnDL*P\DUOQuÀfsGDpް};,iibuX>q'm^Pآ_`f~e͗zuJacH &#.u0mCatx0Rs8 Imj­ꉤ0Oa\BDK1Ab1H– _p)*1I_(AՈrӻ8ėE/%Y7I ~@~Ra`8!ϩF_]MX G+iH&+j"9 Sur 8jƷp^|Hd%hE2s[Ԛq8s"j$JyWf*[9UE: jTCBx JfE -)rg_[A3z]&zLE\LD[H2kLf 6` f`5GT \?d4(/0>"zdrTZڡM+l Sl\K|B sX4)tPģ"2$̺͖K!!dl3ׂMdp s-F,#Bl%j^8 6PX&[Z% (ۥ.z-jǫ7z$#>-E#ddVnZi"׌T"b"ް!̄eX%1I?Ӯ:1dG;7yLamvף˅XaZ|YOe$rY/ ƾ#DoEߧ8'DznbKvOU_f\:1AVGM^gS`1P?,XSG.\eJa}6d[Pa rEV"[ޕiEaW,DpofjH%jd!Ud- z$`[u19"Isdv/^sݖ )u$1_ΒT_ u]..aH5l"uqi9|٥̥exn ۻ :b?RX|!9"yWehk Dp^ok_QkO}$Ff Ⱥr\@mL o",q|®Yd(^^O|z5 {ynEI׀k`Q3`5Dk[z n^۠IO`>ͧmK};Sx˜J 6!ǧ{jFb|#;Ԡa.44Da],$XzČ'aN+lۍQ~*>U!x6× _7vNG;Qٝi'FZP`\;xG6:ⷀ>)I@2|oU-Xfv+qAsnPUBsB,w혒)_¬j\g+hXQ#ub?P'){Z~"WA<4tuS6zaHݺ8cJ0+졆ƖP 6 b#1(lQأڈţc8im=y=pE }~4ƟgfUؓڰSžz܂lU{O7nŘG3%ˑ WWvlYoZYr,mu_F/T*_e|qnLd;`r_a1w;8}:?w7|#%Z`;9EeU<ӗEҗtD 6=M??lVp܉PW6+S'_JC"ɖW: sǣġfU+ UQjNV0BzwCUbH{b zBG`&8}86P[:tCGf^RyS!N +lqGRdf&T@1SIX?tLQ콤D*M R D)pqg6^c}}p%α=[ 3V0йSn^'{/sx/ F_Z7.M_E lVBrRFFV+޿῎8E<}NϪZ.͇hVoo[ >hUsm] :gN?B&3J y {{ ;e~>H$χ1ގPTpďy obMqJJ@nGeGV8 ބ.MƷc-&۽*J+Ai8_Z.wR>b84qP&&8Pw,]ޱށ.;0^gdu:߃a]siK߷ٓ0~vӿ/)X{ ލ\H.oM6v4[Y(z"ݧ"TgOjC= )@{D }7t/ФϠ& +$[ε 2!gKpv=~gF%έ#XVyٟ  FZ0h;?|k5<ެgomqe0(yd>Cj6@I*TmSW31z7 Lal{ #}It6FYQc`CT.Ř I 1*YێrPTfuA\ \"M">|Lbѝ0]p bqpAyP? Sf"@/q>K$Ma90פ7=!/bDRD,pr!p1N~*`|CY&1y3w{hн*₾q4>(TeA0" fLtRҗ 6HD "R&;O*ce|~\0n/k}D%bB%ӈq4_T9xE`F]p£rML6.Xbw߃ P] @^LYiY|X68sΒ5b?_YŸgC0BZ`Q'fGZ pI+ [08֠ P!BlIDZL"*׆ yī7HBup)Pl6.Y:D92y6ҡڝfƵ0 N.L&oX GvBZȇ@Dz_ 4w!0{2Yq EA[PkУ6: 1.Lj3cB /~L;q}&j9!ok!JfQHu"=+C}T= >)T# EOAzc.0x.c`.,>¡hS'c 5ҏFj? a0bdj; ayXo1W̍ue)bv9CgPjk0w.I;HH4扱ks;zT{j44`fST,(h|0>,~.CR{Fܱ]8bL%,g3N$>65 ?bfچIuSR JLD(@ j;0A< @yG*W 4Ejl!%&k7 ςU≼hX,ʍň ,8q/a;C1GN` ")O5N@@z@:@(J1MILȘ 7BHO`dx X0CD$$+p@ 9i?r@ǒ8E$% !CI~P u 9W_$.RX ?{HLj&1Qm$&1IL[@\5N\"6w~?Nօa9O( s(ΨZrg&Ur Z<)o0+# m0ZA2kJF[Źg, owy4ش#s;,?`|Z3kJU=>YIl"z~Fج08(KzF⃂ ,xgL]ۊ=)U&lBӸ͡G]?W5Rd8e-{S7U3WS[l߱[FŅs}Yw| gAG~#o|ޞW~bǸ]JtmY)Ru9E|!xN#eO*10Q U-TTt*M$r7yjZ:-[▞ZRSrU21?/yzn"DNU%橔|99=YU UeS  g$˕I9b:Ε)Zv"-QeUL)Z<5f=R+S RɄI~J!+~IF;G-3GMʾ{Q @NOT)PY{CUVw+\YZݒlI;|^E+U|Z%K͢LeKBTU6Ra[4'BqQ>reF)`]rހ@ш{ee;e F1vo>Zz}>|QkDzS$'6Gt?ݜ%3uU7\{\3w>|:UtXm^{hQhۤYuOfm:АG(eSږ`752n}siӏ=JO",yyUQ|,!xˎ7/ W[/(}rF/-lS4Լ<_Yᩲꏿ~b¢Wezڽm}1#V՚ҭY?~/z"]uߪfܭ`F]d})4o>xFo|V_|}[B/ַ8zǷ3|ib{檧 g(x 3?|_g݈괾2n5o{>.-^5=5Y %[ޞ]')1ݾ1?u3n=%BksrCzgXѸ#^(o;quW4fUE\O<&Eg[(|kǯ\y6KuYs"qC,L)ƅ޵h/7'|Kϱli%5勢LJ]G2;hބ_#~{%/c9iYz򬀳Ǎ8 ;FJfjVˆuzw!ή% =~b]wFWO(Tb.ZnNBu\9 0Piεo3I\2_x}ͼ_ /R=BwZSS8NkøԶ+I,9#E7|n=2Xei{O-U:6mմR5=c>c摵AOZz=jTtQזԻYu! r }rp ˧Va;ǝXU XZWM>< Oc[21<ЌgT{U>3ʹ}|zYՔ+_ՆaGϴfIvwnCit?5Uen ]fe-+ccl5.V5gݎTr(呩qLz^I?˻o] :$77tunSR<n7e?g7_]γAЍmMέ~ȡs$Z5e.' v8_qNj7u&Xpzj!%N{̤fPg$jrCpLc)97T)kiXszVDO^c/gr{57ii),{͆ ?U?虹:G,\v{L[JkPQ; >ͤ_}[C6V=Nr.wKu4R#)$6ⳕvLSYnxti sO5U֩w^?$$YHCF3tVÙ'i"ZtdË2+f^:}}:~hwmKn[fx饳zۇ<tCǂ.d-{~82;<;#0ԴnsM:z ={lrԵ녫~[ W-+G7)\n㌄%+֚iX2bΦHrKT~x\Ϳs)}o0{u/pܣ?zXJc^]UIcq#U;tJNlc{Oem&eTmsU=M(wU'$.(]t'ѭ/>*1>~I ouك2l-G幰_[ήo+ه9-bZnkym磴?šeƉL Tť,o_HfU^/n~~4ᾆ͈+-CT6+2xPT5;.|ZE٬mgA[Y~e[yH m"[@zbg{L3T)b1[c !Z>!!Zg>R7bdg G4)UzW6m\ۨdBɠ~~U`X!nvmS0m{h'!P>7"4Ԧ&#ya#C>!̰;;PWEL =}m{iO?!CjM~#V哱*LW}X.3WmDI%:|I'ip= z9-z[T<)ǧHyJxEft+TDT@4$_ůpEQzl+0MHlF,_ſ$M$Z{Z_PʌHRf ،Qf챋ىN%VwXcu'՝4VwD0b/bly{Qq/J9E `9/(nP .ۻ8n~(aޥahK11&ʯ Tܗ5|\ c\;dB_e+W^kWs<✔IHݠWȯ 7z׫Q9kj{=f׉^,)4񑴷=aw"[Luq߭ܥv\IƻiTeg*$Tp,Bؤo[Ȝ'ٶ;zp{T읶^#;VAwcG(]tލ.O:jO,\bGA=[r{1hOXwp='d>^}.Uر:w*^- xWh/Q\5>'t繗 {m=BհW {i?}hTP+Ea=f5$BZ0d?Up!ch)?H(Zl|a 0ܜ-?dDOJ[ygkƿ8\~|x*+?ty?ʏrcsX '0 ԕ򑫣Y@{pZ(| 7'Q?&"x=Zz$3H}1}ZԀ:Q[݇ջtt*MZ\QuL0A|).ln ga8J7|yZ,9N>Q ࡊ0tV)̓=Pt8cэN0[*'.a+F @!hE-*(ZFK[(YoQނddݚlA㴽}eT_ W(x_Q}E'f2?4Jmr~` ޏmV-qQKqΫ&;H<6`I st loD©RD@&S~_p.ӶHNLml2HNFfV(hh,T58kLO v+5"MLW]]d2PG =`w`1+jﯣmTCC9 itU?D H6>,pCDۖru}یGÉ4*+'ipm .lf?\4v|Y%8Î09!3*88d# qG֑thIG9(܁x3þ5|ɕQ2GN[GՏY6:ZGyquG^w6GH{@b݆DCQW܏芄t3 NS uNs` ~Xn4=)dt e$6(;/htcyt#;N.bkd:mFvQNbI(CT#x5pRp\7= qЄ}:NB.wR` |NCZ=w82}',@]{"xGd4d5NvT2דe8Y_Nd1NSOq{J:ա$FI*;UV٩NUv*'lENi?lG:ST s좇a4>+a h0шD#lBr(7ˁ1ģ33ә$:S.K<-dL!睙|{_.?%_/~rh)/'=ң-aroTbN PWU\:&~; <(9G1 ?4Z_`={:ZU=d.Qz(ns4A͵82mѪX $mBR0mIV&s.)(/VCȘ$y?M;w $5ƗKF&]PE !Rgy,6֐J ;s"O/Nt A'tA+O^9$NsnI,' ow|d 9}񅰉g?II….ۄa`9ўL.&&\w[#俪NN۽^dX(t:G+xS. 0UT]ĶO> JR23OπgH :CN߹%R24μ1 :/dznf1zzL<tDOEȑɭlHผlqpI&G&yW$Bc\C6t@sf~`&g-`Ղ˜j-h޼EEڛ0bag E,%mgѭ,ukEԲuZYqke A[k6"mkv4}HjGI"[~$-j[E-L[j ]$VIے%9K"ޖ8X53y @9\ -b-e4M^;602R]ٺ2sa1)ecx?3rr,爲\yb 9\v*C-RP[4utv %0p=l5![TlڟnRvT2U@[EUB[eUD[H +*"Պ HuH nURj 9[ t7$|wDXЮBBeBEtT+Wj#eeֱeօwY 9!g^!n7^6G1?0~2&`MrGV*e /`?©o|CR`{@̍JL[8VLc6rw#BSQw-!-hŕT0%zw)bRFc{ۉ;lQRi눵 y+sl%.B/|c?chB"G69 RUzD/?X?Z8b8A ?T ?w"LꏎBS~)'a?);^92>D?*}H4,gD>LK#[x|}hQ?WV?~OU>e q.v.gssG>Du_}8M}.IW]%Gu9_5GDWF7 / .7--\/y(as6s@irk' EvhuۡZvV/ҭHVX'-eBҫ&,($b#4ͳF$2iL:]0xEYڳG[z\(l6ն ^ ~Yװڪ][*^3pq@j@,445^s)S(;l":տlTsGȱ'l s: js Xn:XϏfFu=+qH 8VCR 3pf,f5nƸYM;]9XzI:rgtRP}*)DnfACfT&i%i8扺o7AWÉHQbq!a;? q凡 !2jB~@ -:T \!u u,u¢]+#u!uN\ /pCl t%HC z\Axs.{x=X%J&p7zOh:s s'LCLc*DvuOtD:T@ ҫh?QDQO7Y&3Q&;{d2 lvm6^ޡq𙢪&]Н!VrS\*514 Ә9`n0b!!ecQu13ź/!b(|=9'L7lL*dZty(g4zR𝐉&eKffB<̈nk]G]ba%NI;5K_'lB:LWtUopm $}"&JHJ#\ھ#fWu%RKRmkt}~+˟+e<\=H3ε0lC"CaqܙFQ<( !#6-IC)X3:w"`e o{JN{;.N1rƨm@I0ڠisKhh-q9˴Uj kG +LNq-phl-^'nPH;*@uHHVW*D)摼 OR]WIDԚ]Iڝ5IGE `n;5O|ѝCs:1bE }PמX"Y@{9 KUe8`NNѯ$C7f:PKX}t&`zf68;]~ݘF@4&ldbcZZ erCECu6%Q8eB>hjq!Yk5\l8D搷Pݎs(qu$Ǟ,Azi킵0?vV)L+HF UZEtF棑{.̗425Pn> s z5AW`Z 9B< զ^'&$eihZ"֮%d ~MsB>^XsZxIPGC1y)*Wu1SX7%H% 8T$d^_Jv93JNد"<;|uenk1toC~v 6ҷ#÷F^ ?"Q9s  7"ѻ;"p9ti-i}ubVZʹL Z_Ƶ^yIˑV!2#Hekukp]#뚊ˌ7=0KYC@;%t[y-ݕbnd!"w,>B+Mv;ݢG:{/ \ndf]'!JV]YGK{o/c5l8&_GU6:t+&ߠo@Kl ڢI/[[DmG:{>6pTg/O'+h{%a^~@ް7=;%cDVdGx(t(cD{wevldelGfdį~s—'W߮=orn(|XC%umgH߬#zsoUeM?as/DjZSAJ [;wŪŞeT}w2wn榇& DN84cqsxՃҪicࢺOCrԱ^CB-^G'z~q\/I/cY-\1WhtR%%궿 Bhb޻JkɷR*kKXєsb:ݔ}3-kG]Q P7wqy!G(VJ6* ({hWvnq'ԯvE #z]հn(<J]Tk{F2@hph.-ަ N׻8oP'uޱjNbIOeista 8#i)Z7?$?y$D=k~k݃Q]oUvOGn2go^S8j||߽JfFg3lFj[ [{yr )`t5)9e_ٹ׾p ߚ+Waϱ"UxvF>Q,\Kё$#آ6;yƙ%fc|TAQ}`ƛ38oʫ7yx,7̹^~/jGX{^Cg5:+D&O.qcē7cs4_%K$2gtF3 Aޑg#OR\o_#HF/ٜ|U7TƳ}k/c d":. #`N"H,TU\ #$PͼKATZޛx.+ KW(^DkVy|)ZM =47TwaKJR}Sd^!xg+t+|ǏzO ԭe6] s2".ԥ 6T.uJ OZ{PGAtYF|5-)82Ѻ۶?m@ˎIqA2}c7\c`3(Z+צǷy=nvc}`Iʚ|*Zgm͙&,Qɏ(0ZگbwəĎ'S5jsMa4(~?3t%H ~]$G@ tD0Jп Ңl- ܝ dk(,ireu:J{bj4Xގ)W?-$^6ٮZ'ښ̕=)*$gwsZ[=!rKCŔv9 ^Rl۰n~ٲh=wDYif;.&r.T pϕD#PvM#(n+ P;5b yV}|W7R/)^0jw^Xvs^q0R6V埄aڭ2\rPK7)0?$[K%6h7 0M<ܳ.MOjZ[菟CPl|? j._>xr4j.+$Tg-m}%:#a5EO4C I0xlfI"`:Zze?b'S2?\CwVbitr9>g4)>nSHut? bD`R` zcD.H pFE=i?U'_-d9U>rOӟ};PE?wz&Ex#8kwՍ3d R. Sm.kmM` lg\ {8_҅~1._e'F8 -n*' EN͂sV+G1d,ɂD"d15 )1T3*R=+Ab˄VJIh~k]n׵nGFumچ.sc F٘ӟDi]ZxH |Zh yHA=JD"$1s$[G8Fv }ä}e\ +wá]EfPGEIxRW XԱ=VYFfNBeICíVrUH۱HǢH\BJu`m?Dik~<0.NH[ "@*[L|\o:ܕLSK]DJ0f#e5YC%~Ĉ3$e׎K"2\/Y@qs /# "dw, ۓIs3 _."fMIURR%C|:Ji]CU up7[(hL ԡőis"O< ggQek197q|*q~1} q9DvKw]#yXr˗OXdJU)O[&T IEg`|\+llXP"1Pe{)@:kcVHK,vTO eF&Ŵ',tH.; qs[z`ϙtjR5nn"q ͊ Ʋ˝^8:lEx7+!3Zsṙ,^@ݣ ]P#grYRT޾o MMop FT<-Zpgdżc;vYU뗛N.YD{̸k8x 穊hdzm$kb <[?Jj1VzgagV 西PrλtFq9f~NRGy1Sx$ꤪ=*4; 3=%o..ƥ~W%⻛=7Ej?'FA*̑NΦr }!piam=}ퟸoz>9}Zvd@~ǝP0MFU~`;ma|0V1̕N*4I=wOq\8ڛƲ&ٹ;͂%;GywsI5RŨGN")N:E]a,@YBƛ"n6r PEWa5SQD.+meذ(|꣰ӧ[N~G4X-K6WgŢg%:Lsk3ՖD#n NV^o}j#Wpy /Yܢ);iu /v9JTUe--2vuD5!"!-]f?QjZk@x M.״J/O'K5 s^4R7SMX׿| PGNe4[m)ZD/kIf3|)=|fahSH]<OeTSPV8M+tXYb:Ų>=؜[bG@^1s?&$7uoxoM0λf/,'䐅v?^6\WzKczA;URȴz>rU<ʋqBi豏J!G)?*itf&kMuUיRsx浡0EdeoƩͧffF3{4ʹD:>T̷W1II*?2N Gp#&}u/=.4Yoo-fdr9LT? 2d(& Pm- ﮈ5 <`Xx@0$@. `t ʁ h!`&` l7@ $A&dAAP%@(>@PQ9h  >/Ƃq` ~+M lAp]xs*D݂X!v  @!) $ A:C!2 B=!g|!($( ʀPTUA_jj }ah֠uh]@0L$0LŒ0߁`cX ~ jsX a$0v=`=p'p2gp\ep9`( {k)Z١і($Z2Ea` A K+$IF twtD XmPKiRtproviders/ebin/provider.beams2d``XEe)E|IeEyiɉ99IB%y9p!ҜTBԢļtԒx$aT@,3f20 ) 0*2[3;:2830 3:80 ! ~ `fa3CpIQp-L@3"\+j4@nbV<Ug,b9 ) 1N:#ɣ3rjgZ]̎2%~ʉ rrr=3 eZ,KSL66]fϣI5_1Rwd#[~6|[rdb{T:V'^n]|ƽ_le=?rm[j֎hʸՙ=KҜV}v(tA۬% g){L֌бClϮwEfSN ݏR^AyqՄ1+Ǟ@T[k>/Vș>ǯ?ӾZycr{ߩ,K"ǭ.]p w}2@4PKiRJrproviders/ebin/providers.appm 0wB2X;nv)1;]wo*(Қ}MȆwR[bvVmOZYEwdbc~̙sg&ySK$Z{1gJ(`;$IYlpn˭.d~Zt8fu6nu-v6jus/sp6W#wX9것',vٹz#}5˚i'A l+Q&jo%|z9Gh#X͹Cj-v>A!zs~s,8(W2gC}f!Pe[opk}f9Rs0)&c LMh[|.Jvkr%LJbʃeI OV0LJ(Cߊ|0*@ءlu4Zrq D7f!%yR5L4iHf6%Fj%ILM GSe&( %eXnTrg瑴eQϒv(}d7Ua b+Mƻ1LUD` "XQ&JK GGDc|Ipb0Q8U4a| M! jb"]ņR5m/.œ5`"a!R|0Hb Qt#&, s6bjh*P![J<> hraLHLg!a: (=1DGc4 R| $ T3S#!<F~J/)(;2,pDYYXL29l# pe,M@$S4С(GFǰH'V]Exs22VGC71)>ǃTD6g?jC H$ T5\m\DC@.5W'+0(?Ud D@-7f&TCqYs3 ZyPP0Jq,gR&1Y)N8%,`@xwx9EUG$'|ds(00EG!V8(kJG D^a$ڡPLb*QЩŔ  NŐb!zLV!C!ӠC"d*?L`bX*D"(%zb. VwNuAt$@͕+1< c( NB5 K=1<1dIS`& Q,8^Jt/@6ÿ]O; !Yz a4,ߏdb.E'R$2V86E=!1E.V]LX-,JDKr=Q6*_?"Uqnj 2NlЛ`@rPg4 T!9<-xJ8y-h3CXa*yQ%ͨKWv?euj97k&aBg>P5`hnR ss7Eς( $} i.Px#/<,\:%E$ ŝ~.w4(&ɇ:RZX.QC,sZBX?R Ȭu0!t cF:Ae0A ;B`q`z! p` $ '(:droHM#(wP-D n1lwմfю&KJC,tLd;gbm 1b O 0'qkfe(FcP/g<$]`ڨ _ٟ:{uc%yꃑ)ίFz\YeR/.P=_<[<2RLS}{l)vWa]|cؒ{|⡃L .S>S~gކCv[>sh}޸c udoko:q=;|w{>3Xmuc_{KS|7r폅N+ӛbT*?)6?&EVO1|?+o5^W+gItV׋-G(zYk8)aeO|q{^:7wa]1$ts=Nn;4i47Wmqkj^&fUK+WmsHNe0[.{6F}uOȮ aIlWB,7HƤ2t!m$E5?R9}.4T ݕС ;:+ZT-ikσ">2˾DgHrU-ş\rކloZꭃsIߣܻwYѽMQg؊N=;nءd޹{mޅ.17S| r xz/)lzMG~:o^i+I[kUwGZyqξ.>_T^ho]:;mQ5QG鹵G~EƘL;~3E] >{ܪ]:V~r㫟9g<9c[nfnmu'8Msgo?qGW+yt˟j&uO;/+(s ) wuhrb^?1trŗWlmef߶}+/t\S04}bWlb֦{s?akZB$m/[^ԐTfxetԤ2<Bd5ZtpNVYxݕkROՅ[nˈzgk-7Ք+?ttkc1ę6X %6Z!hr[L&t; `m X566@ 1bV?ƛ 7im\?A,IufFnFec#0 j4FT(C%Yv32p KrfkleAխH߶}ٕj:X0y Ln{Rs!56+uSw6ȱ99#`#{$o>aȳ23Q<=)GCo@&hp#,N? ӹ)RnIS"~/%K^Fh#^F㽞f%zCX2eeLm +> X1+~3J=hJ{6X="netVn0/URżTEyFb/uċ|F[S :50H٠} 7+͍x,]ďۈ%긕u P9NyoB8wIq'. ﴕR?>Kie]z2yI(wq|7+9Y©Y5v+Qb>愅gBiN0[KP۟uv4[V;3=*?:iq |ܩ\8~+ת˯Oe5!5y &???ǜ<q(~\bM]<[cv x|4ӒSәxa62.k 684md%?63-E&j _/mxČ,z@ ;ǷÀ3eتϐGK4]>XnW-C>c5 t劂\--'djxeB  '4'_idȓ1JP]8g|CIȌ#4n ءHzfG"OO!2dק(>d)HNm'zQS#ۙ";YyZ~ڞ"X#%E6RikHOPՐ]:]I s {nVwÕo}qnnkлJayFy tCYUp,Ye.޺=Gnw?CAwgQ]=S@hw{,Ig, \f-(or/~NZ/({1=/=9zz\s6XWcұsjfC:V] ''+7޸=y(&j[%(h_̠T" $<^HFR}-AE~j"=x|1y 5v{^M^ ۛxoyIz)zI C&|Cgw_Lޗp>/d5!ӯbbşHSICjEufҎDZ$+^YJ:|C|E"-U˫WSAALR2׭I~͜YF?Һ_>d(Q^ҁ&ʷRI:b8W3hX`LywT͋@d@4GxaI#aȚ:[$-7#)X۔62{Ay 0+ķTϷl0{S,CA"E N`5?-GOpsA-=| HHbloc6{P=hM@iᠤ-\A 5hI;0[qmsbR 4kC 5!z8PhU>H|ҝPC~riL6iyE~rFN} [;uͻ8w = T%^ۼkSak߆ZhNtwcYr&{85u߷G}'GD!Uj:Ҹj:8B#i-1GP'X0g$rnUvW ġ͇:S͇ICGlʨ$GtL 21Z'QsbMGѴYx£X0|A_ 'u2h<N:a`N2#Msi?"̊ssU6z|.I%"H_(_XR?/1/S6_53|R<YUF?N8e1QP .?;EБ)2цg.4$N>x K\i Y57ʧ>Ai=KK`GSm;^aQgX(43phgq8Cl~D'J7 ` `j23%%+xJEU⋧_EۚQ ~ϒW?)ޛZnfzc  %(q^|7Ni(%~(G5L#)B( Bhn@B#pM-,1KWU㹁B7gXnO =ϧ\ՠx:F0vcRҎ1ol%MVM؁>!mxc':53N 00Iv˼u24;yVjdi~#_@3SPA`JzB׮B[Sْb]rTFS4b6z* J@MdĶp̳Ly$0? n!Y_d:BV^l?LM#^mvj9'W/=y8Duœ~U߂ gR U7bt$k kwZH4H|ʍf !n$ӻQDчR8T>to%> n|OiԒ(rL?42\7wvAr,.Jdrr%P|CLuOMѕGWW Ʈ"O֠Op4Ӛ 9*WX}/2\dHZEI$"D"oQҪ~?A-ߧOHZ]>k,rdm߸s _}#{.(?cκVbby1*7%|d$ZG3&JЩ^|K#xbRv]ݘkL1g-ĸ)/iqˆC.b{ Jegz = jU?쑓^g(^'w(&]lM7lHXzk-:SAD$2 >xg ? =:K{8*,}Pee7syP)J"^bY<"K], ,c/2B2tV&2J6[l,$韽-$:.'q#۟f&M?Cr40 J:_:OΉWq]^_ Line+c > ? A C G H I J K L M N O P Q R Y Z ] ^ a b f g j k n o r s v w z { } ~ ))) ))) )))))$)%)-)/)6)3):);)=)A)B)C)D)E)F  N/home/runner/work/rebar3/rebar3/_build/default/lib/providers/src/providers.erlPKiRU ֭ "providers/ebin/providers_topo.beamW{\ص6 J0B"FED]DaDQok@^nQ,QTDU>"(jQj[ExgC_w|3g|33ˌs@sZdpNA0 Ae$eH1:AGTFI[R˴i.Ո:݀P`)ȦH.LQj 1بUVCj$4R&&U6HLE>&5ҦHcʠ`p0XVx*rRFِSMZQӦ(2t|1l x"tJ2P0n8̳qayL|=`"6HE N&ĉ=."e & pAP. ;a,3x(yB8'Dl&w""63 !Fh64xO$BDARB̥åL˅VRF֛ 9YX̴֜sPE"g ,!|\lZ o38e<ݖ_k8!,.b@eIi(fq`M Kups0<<  \qn"QgKg=i|$ѡfJp3G 8&w:`tK9 BDWDAEދV?5FLPW#Z<ă< ObU&OINj8#X^<(H $zF#"@"ɗKKfQ$yIIbIރ%h!B!~¶7 1 %'MMtSҋC?'@Q.r\+p\p s0:F B%y w -`ڴ?i 2Alkզ/Wxϐt|4@T*UJ"U4V&%o6Ib7gΩ lGoΌ9I"hF;9%:͗ 7[yi;`U7j{X.v!Vf5B6ogQi0ÍFZ5ݛPBHAkihq(@x9*8eJ醌m*#tNOoaK;lu&REeBtR&:*SHr5-EnRB%*3BJ.rV iu),:^& %4DIMi 2!R(E*&%,c2,Er&d7!H$cAgd&!|d@rrnk˳aiɳ;f8eu#Nۗa~L'OH:_^+a{jJJqsbSӷ/4\i+KJ."͆ʓܐe/ v?|)E~q+xt oK5;r-d9[)1Eڏ,x=^3v!LI_ Y kֽ~|fϦ-'7ڔWT^%/X*aF7._q-+9&ug_֭A֯ `Xq䵼l\52yJ&Ywe}>5;ml5*kl$uX_nnռ]tPjOqetA>Bkw޼?فrN&f2}r6Z4#Y'88a.9ݨ f3F)K:.釫ا._HGcր/zg= hljlm_Xͪ#!~qwXRv"YSyW 1k|bSMN xo)s!Lf4kK J._,^]憞`ϡC:guRѭ;Μ^_# z•mbc퍇%*1ֺ{zMX\P /<103iڸu-е3eW~f^ψÓI W~r3tGw<צޫvr[>SwƇ{5y>gȈ)K^.u8AMc]{+6:Z E37,ɱtN>S<>}zo&pw?Dmi89lMHdj)1+E\q3rֹSWZ*JW[6~G]哱 j{vbkS_753u 3~r>U0 w.L؝kSu_OuԕҰ!rƻZ_L{'CK.\PP0|t@CPQ.]C],<;S{%ljJ ^~3`c116t,$X<6KcRLilePKiR>.rebar/ebin/cth_fail_fast.beamUAlT~͵v][m]@JN-TBBbcĎm $P9 v0!uĸpB!&q e9/[/{{y~ܫY g/·~ !:(ZZutk:iz|l8U mTIPm;Dn(m?qC$猜dmzVy%\W{%T+t2'ۗeY+`UC+: ~"nuQ`W|2Á5nv$EV~Ʒ`%yb)HG%FM:*lpɐ+WTF )̥Xg Qΐ8ʹ%M;rbF|ܔ490+1w ^уʟ̣^+1T2~WMVZgxWg۬=HqtO:e 'X(cϤc}+?x(AZJå9PB\A<̥E Cúr$1?fsј$\Y< 9tKgb]731Sπ08)qJe:Q,d M9=|SA0K$R!*2W$!aYS:zCxl6i0@5eI|p[̞xN))_dI 9MQ`{)Oj)Yn47+Px') =kA]dN5KnHs3Vjf퇟/u?aK}nz_>:c |<'yϑͭe̛ [,V6.dʥ`qOf֓SO]: Ze:R]0N|$=kxݺ G=>^_?s[ߒ|v^@~.nr8<QNĥnSGn.onChԃCm^jM8!r6 G52{W" gi96sՠj*טּ[];sp=N'+,}^f^M?#l9Rg_Ghds,Wm{U8"I(pZ8#EpE[X\B$\a1PKiR3E~ rebar/ebin/cth_retry.beamWoG?M漾$s^fΎBsE gR쮓*( ]VE7HEj R[" $CA i C\{VB}yl\aK8"q3>,4ꃶ۪vNIjBi_mFǮ N3u tmlnmخJ;X:#*ȷv,4ZvWu>tm{붠 {Ru tDz gpqU?vk[m=e Ci;^fhZN s-u)B43Vώn 6JQ0 lVVi-dxg&[V>坺qaMb @q̫qЪQ0qQUJH1)B'.)ZI,X:_R~. B)7{Z4HZ\Չ!Jh 42+sQ#P}UỤaxN(V2+$kXשR)i z($J-#{˘%-BeK5G nz輄W"HC5!Ä-k2VIV3HGC\0 3a^p;6EI 9\%f*@a٘7iYrJLyGG@I "юf1 D,f<e FJ#Sɗ@J|n|[I0A|[&f[ %ASMyT˂ |l)Fʠ* * :W-#BĤ)PrJQs vmF'Ym5^ڎ/u켒VPCN^ڡÜEN_m [@)aVpU I"p0[!~KMuK'OHi 3{L9D2!#-g &n.HM sn404TgVSty/%7?xi/_*,`xl:̙cf5U¹bADH~Lz m4e9UZ֕ZVO#5pLhbc)S6`' <]s9yR yB|IUHl7XoHR!֨y쑡K'G/JqǢ1iq)dg}Z1?.ZTP3^'|`׿N /COu`쏡HNo8*$tz>\\6Ӷ3nӱYmf\VuR5Z ܟԹVv*Vβ=b:\z{v:5kz YM߭ZvKFWzu?̻ seCczu{r:<}7 }bjb2LM?%kӞٚM}^a}|mF[v=hM}h?2QB( C:Z@Q}=C.D+蹻PKiRvcrebar/ebin/r3.beamU]lTNcߴi;mӮvLl&ݺ1B{:M&õ Cҕ&1b<0nOQ&@c3$N0OYr=::< Y߭C,I _4U`Xn[`Ǥb:%ae l{Xh;r² :&Y=ϰ+p[|O(UvĊyRf^\qcuvETMz 5s%!NWn2e4ҪKcU03uZ{յU¾ݏ7\eqEK,_LXcf=h>aJT TIjTe[Tͩ\jpj9M5sOP='ip. Oхuȶܠ9mn'j]<=!Hc7yYRuB6L3 dZ9=KzJ/K*~bt]2O7m^ԛduT&)AQ ~}<낶"<:(t'2㩘JLT_bJNeQ(`-TΒ9:4,+M* `A`SUڨYUh+-bPڛfը|"f֞4Lr+[yO{Z odc|XbgBN $Cg_Ȼȩ= C k  _ rBZ2!@-;#uDND!Fz 8ܾs$`h pW~yaOMp糩|;wa_mW=??^xvlş.c3S;o P{ٮ_~ykogn=w/ՁΓ^LsS~{.u\ƦAp$9xJY,` $/|4>p7޼|w9 %%8Ά_(;#Ą̰Sd%`yB q[6)[YRwL+.)gss]l<[يo81s\9 nfIjc<- :ՖJB'f[@^1c;%0fvLZr)<5S-r-yi\p  {&;'&:)2_Y-Y25>`6}}R<# ʯ_C2Ahh݆h'D>͢94@'8 PKiRErebar/ebin/r3_hex_api.beamW]lY{]O:8?Mtq&I4?m_Pl$v8$ƙ1qt6nW) Zx BԲ+Uчelu"$bxw=sGwNƏf槿8ڛqSa']7.fs%SX3\\ ƪid]Tq Fp Űv~b8[ٲZhݲ+5N1g5R 9[Rބ0 n)[q̆j敊Y,d(kCR]oL*vry)皶dJGljʖMr˲i?v Q#[ 6VI=B݈+NYefcE怽Pnz](p6CÂequ-nl^2\b# c/UX ~ c+ "_!h8툴(jeU%Fr{X&:_9<]kNmo.TFִV0ٺqK>Prb%+֚|5@"љ/G +s yv^@ʫDpV<:bD"z(K52!MbB*@/?E^)|FV !{T5Hp|ڨ}hBknG'{Hŷ:ǣf5-,o卢u!iC [6*LcRElP :ƤZD'D-HDy2A#1iAQ]ۑڅi#8QF06,E4>KtHUud6 vQt1y6۟7MF%sBS)43tJR0 p kkkƍh2s$S:H3=R'NsHP+$_KdF2񌰠 pgF.>u&K\>??/H!ޑTC\J^W^ 7TҲfPmSKp6'T[㰇!NX[r *q_]YT%.Q=zY,e:;`8$`3&>!<0*݄ Y{I &"mA4(4DvBT =t"DJ+-Ѡ %JKiE,eU!*C(}DTHf48 V"/ ~)1-VzDb#J0T`TAFE1Jc0\Cǣ4x4!Ox81$Qx z}`)6X&`5Q}, % p y4p i 9-nxJSNx1z܄4 YAӶDY{_p/O<}7   F Log~; HH P]|.NO~.f]`>X'# uq|OP}"*oDa8]ANk.wLݗEs(0|řcsr8gسO1aj9SKۋ;߸6.AW̕']9Ⱦ?۟ z/?hߵ[Wwn}_~WC;{ubW~[d.^{xGxr^eĕKoA2x{cGqq>qG NaIL4>i< ~/e6.aWE_į&~  k8PKiR;&8rebar/ebin/r3_hex_api_key.beamTAlDlvwҐAd9p(ke!tHi4=ydY5c')Rh{**d@āE=p7[=p=pfx8 όf¥|wmj̇EǍg@@Ƴl7 B#78s"7˸Ӑt$8nr? ^"tYoAU8Sj~T)'cP$%y4G5RbJAR[/PU{؆ҭմZk椠lw-mvme>gYvm1P/h)=T5ї3mNmԶ3%ީZ_7(AMO>G鱦"j$M }[G)Qx sD`+W [FYP(+ V3Sy&; p*r|B ~vʯ:5T2ęK#Nxܹ~p~Z_XaG64{_5Z\}-w wVX0|$?Ko`klG{Q(;2#QIb4|HM0F88m1#}3 'I ;2raT8JQ).SWN G}ﻂ'W:T.Nxt;}:yaӱKT.Q!@ea5^k,/y"<:ZGk vewX, c /Wx_;8 |i7PKiRkLQ"rebar/ebin/r3_hex_api_package.beamTj@>O6M ]/RԲm-"BM3FR ^ )-B(|3g9ÙY^ߜȍ>/>_ iFjў 봐B=c߳ (!e/l9rq7}$x0i;C$y|88~dGzd֥9P%Y;P.GrmRSMݔu-S@IY9 *U"3SܬHQMմZi\ߨIJ^]\ՒmX\Q5sBl!":{]N7%J) d^*} "Fl(GnHA¨ >QB /B%"=ݳ^/ r|GLjg.xOjR{(˹=zTwHX7w%vbm7mo̬6ӏ|pپ &@[,!?x pL _NίNfotv:KȶX˱!PLi?0CM{`}B3v) 5V=tQq6Pnj%K{8cH sgQ37Dx&!s\AqaD<n!(;ۍzQ =tRJ'b\ r<_Ki KRw1v(C?SBgøNm,QL)w]12W%4PKiRoLl8(rebar/ebin/r3_hex_api_package_owner.beamT=Fw{k {"4g.H _Nr%\؂k{n띱f@+' H"QRIC$H*:>nH"2ҧ~f,0>6|Lz-GFN]w 9d#:,޷=%A"‚-5K.R%B=9^%%DCQΈ^  v΀QT4AU+XZzTЀ-U1)5˚fj)KeofIs4]U"ʓi$^ChYͪMjB7ۉ(ݾ1^7u]vL!JoeSJV/]^Y3a;z lUJW--p/ˍZC똞φ؋2͏{X#D!3 l8]m)BElyT>r,qp7~Pk? _=ۺ]}t},=]l?kz_>?'+W_g|Q1o¬pKoO@p)s\ J `no1N!~@4d'(G3I`CB4鍛Sl_$ vbU>&kIyN^+rKvel5PKiRY`"rebar/ebin/r3_hex_api_release.beamU]TN3[[cUq'uw, be:ŊNvIzvőjmźH`JZqE ŇR}) ZkA>',;P 9|织nA(׶@AmXޤtótFmjAa]mBs}+pٴA3uiѠc@OvH uI!\?`3*>NwQfΨPݷf(o6O4y붜R^MgANev1X  ^|älu3 ypЦ匸bmehj0̉6.5bKlU(R* 伌4hY>RY952^ӴҥRn(wn+jUj|Q#B1>Ϋ8$I*.1.dȃJpHCYE$Gr-gDtZ eYXaQ.j5fꏢ ZMjYԖy|F*H*j|%$_6~qe <+rU&w=WHeNY#4ԆnfXLJL"A#h*w}#s+"nIt,DBJ*w:96hhIo$)ط+٣ =yu>IEzA &zR KN8# }`@Ϟ}37\:og.8WzsSo}ϱooK~ Y {)ORt5˷}p vZ!(ZJ3N~<b})~'xg[=>>?_PKiRHdrebar/ebin/r3_hex_api_user.beamTMh@$FMEĿ(H?Se]Z=xJtnv&$m Ră{PA=)K՛3Y*^:^f9u>~q 3$e{ n Dl5+Ԁq#Ѝa_i@ c;r)_~B;@8I ˰Ob>]mu yAIQݩz{dU:JiIT/Z6%LhZUɩjIZe:c%EKQ%'$sBS,Ur5eWEUQ= Wۉd&]Xd)@xhR F]xa%i]R/IRLAδ"n01H3?YϙA+sZ+dWs,֋\ļp{97qp4@Ыj)wwkʞ7ᅫ}ྼΎpw=:45D׶Px1afdJ4CQ){s ۟YCNn/OO<&YlGqDy~@1[ B5 .)]O7"}# ]ڀ`36Yd{!$6yRC} 6F~2A6]׃>Pg]ԠKmH& nsx VW!@^W\Ug lMh4|`x_g'ăM`2;1 ޒa$LQ1' +k]OCa|V>'r¿PKiRVHrebar/ebin/r3_hex_core.beams2d``spruu,e``pbV"ԊTԴҜ '/-3;7?4'5>3/--('1/?=$I9?%h7 0@L fbfFLBJ  Œ3g2 00E!  %E! Y14tl[V^M9e9=10Hz|d=Ԍ? +tQ`ՕbYǐ0ݾ>!} ˚%YLze8 /?`$VU>8쾫Yv-\@"I`j"D)ڹjtˎ\3ڷH4hwyuvm׼aY&O^+S+kKl:鰉t݋_l3w3 UXz Ӵ%5>htu%=OlY4GIWߝk(( εy,P%#˙֛^}0L?8)wtyWY'|N{zO7cUTמ>3&d..|7wM\duE/UZsƪ~m:n2ݐKүosx>3V.ߺ9LwOc lzs]3]xgYܫvɆeI=xosOr+{z9BIx{9nijIw􆯏X/p 3^,g\'rMMϿt{,u7͓/0uaOCŌY)Zzoy0ތ7˟M %%EޜJL) ey`Nf-k bv_`YYΞyi@` %y [~iIJfQ6~F~n~Qi^^j~y~Q~QjRb1O*I/(OL&e0H$K܂̜Ԣxb)) Ey)y90Ҽ쒢Ģ̼Ԣ"lKW\ 3<2sJSRaeJ8RAngggҢlB&_,Sl=`JI%4ⓙ*CVf2 qPfPKiR Ci_rebar/ebin/r3_hex_erl_tar.beam|WkpՕVh-ز cH%KflQk543=t C !$NH4aBށ A%oPUcQV~H6U{ι{^'c瞞;|4cA5 }loemZ>[F juZˮ]`%+"m8zvivw׶EZS6]yˮY~]uܨ/㍺{vKN˳rW_,4AV,-Y1;zVhop9e t:b3ʕura+Vw>gZ5ճ}a7q۸pZ>V_,wM:.&IgNvqX0eيv츿: <7@jeay+)pj0[N"\X)F#Uu[K _OKw=毵mx֚zkF: Uho,;jݵ+>nTV,7p*NknI~J.ܽ^_$e){>SݰUVU^A܀gZΪnʞ]qZUO!1FvtZurlʀ]? MQJ y7M(Nn%OZo\uPqm*=8eIrikGF '`i$Fm$ce5m!D?(5%jYNŷZn0 QΦ:FDS&Zkr~JV>Pj#:]ŵn{ ۵ ^GmuZm :mB *|^"*//tK>)pfèEW:X|QD/|#<%ꆐ;JӨ63;2$R{2&2cS#ܫY{;fe0Yt$ Qq}^㎿bC%ѣ6Z^^Z2ڌSH6N Ψz"UsfiՁuM5;)S1'Jr"@`~j66:+}Ny2uZoKJb@l:-o$,0w'20 6U&-a-yd v#C #P8;"nNQ+wtwn*Hذm \ڏ-LGIP onҗ^pjCj3 M,¬dd'^ :+<@/VfLu+'IF@R/~ںNTM(g!#սxU1 pSjc5^Rr (:Zv>N:ay\;kTF:?V#wYnNeD3@EU N (p.*C#W)!/9&l+tkxܥso>v:qrtwMI Z.Lu1P&EKF.uxSNDqDwέ,V,=ŴX\,&eI ݚ)|Uu93#YO'F.LL̝ oG2ƞ>fc.yl͢&D&dŤe BF "N0$)u)%"lJ3̬lJhWj6'C"Oj1XEb^ \a'RQ!U+fX2gkay,^Y| [wi6Ӫ}LH, (%k k\ِ߰dwآ1_-Eb,2M7rpA4o.m6٠m:B3ߚߏ_i&VYbu0 3zQ)Qp @0iĶn%=x ]I2OK8\hg|YgeeWI B,iQw]B7jڈawO|J,;w| iWì0i~>hdLy >Pc۰fc|Uk{t Sͯ6e3MQ9n#B fk |/D^#cYuWdȥ#fuxS9lXю;o8)a4\:ơѡ#ӑbX.̈ ]tB+Ie鬩IYhܬ|j]\T"{q'inSb[ڢѼ!ӪqyA:ቑX.%X!u &p[<\EdSInmqo> І!~~7yAb7LFB30=#HO4~Y5}|~Xlr^gvO]t.5-0 =0Z$ߡ?ǯ=S%"t2o̫cR%2g%vS.QcfXc3;+ 3>A6)Y0%fD "9<]8;RslHmdc @-_ue&F0tYڜli}V8d260Xn^X7gt1~fS&lL@GQPyQۨ;v[|[/މML'TËt3 rK@8|{k;1 ~[rߞKX XrߑRrm:Arn7U!﨩NzThTodӴ7}0 C.4%v_@=M? _ sy +UeLir-˜… $@r6SBʽ{#) +!}8vN-j8 ggt7ҝpjb [ܐ_Q͍e<7)sG,bY¢ v0Meu/SRoK4xU-p[e!g yx,F@v.#e; ,LBTdHbM}}^P9 Cz4^zKLi.͚ݣa)@kLn& 44Bwk(+} ݀k˓A$4p9aAqt 6(n'k<#[Ct":ކ {z(ba%Ǧ'%`O|ZbOLCg6ǠiOY~Qyg)z?kIa?g^ղrbV(^EGMy)(w /yqY[0pH c" q\@%24>G}Gs$/"V~.`ȍt]9Ŀ,s) K$eD!AR63晨=^%]3s|I 9]TKequ<6ezFJhfJ[4Z /e=#!r kjZ8kyG337C7 9?Ϳ[8a!>y]Xu+%,pqy&YL?ߐس .I 7̬R[\w]Eʊ܁^"̠BnC܍4z g;=XDb?*Wі_3c2lDb m؏6D4]b~:Ru뺺Sz]8gp%3ѡN7z#dhes! \+ XB#)xii}3`w`$Խ uoFF #56;hx?{KLI̛x>%tf\h1|6&EfP3}":B9FboM_Coxp'^;i{pAbRAGkmFPbiNWOSy?+ooo,DIb4Gl}7_O6dP9`E* 4wAca&,;~{hYFq{>qTd;L(tY ŹzƞUhQuwbfwG2:3x(HE]ͪTԄ(MiH3QE[a!~S]Uw_@ IOIZ6\~O7Gb~YI$pI癎0I hߋx]ڠؿej%=8b'@_OJ:T5PQ(\AʱXcNOE //5?]_1j>3UsKU8no/-E['T [q' \EI+ h?/el=n1^|dPNRKMΒ5.G{fQXٯZE5b;Ţ "O1E^Ĵ>g9J5`͏Cpi (ȐxCD 0Nt`[ .)X ˯KW`7ZlA+psA 4dj|:~Z`e-߶Xw%{}s2ky1dL`D䓒.N+IXo}K~O;/]4S~ZtU!pѸ*g{KF}W~ϫ{mt43Be#c3CXKe>]/YK `T%yMiqd"h8-fWU͗ybv؊w}Gՠe~GeuI4[gFzY4[gGzE4[g_FzU4[gt%y9Kq=&kja0IW#@/ dmqDdq|sJingٸQ񝵀ǬܰQbXY4Z)J:SJDq\c12(c(c"y!J(F@U Q2JR<h (VŊP^ Qƫ37D+ kDIkcYhFZ=z<_卤 Ony#-~k>6n,u[ȐjJ;lK2^xAlbX&s|"D[#m@&z; AX+sODc Xu$":nّA CHz^֬BȚI 􉳕ۡvI{4^7?YY~`{]\2ȁĚ,@#ϵZ0C AA!kGu q霈~ɊA,bN͍ LwY,[;\Z Q'_:ơeǻʐfAT@=3z,O#o !픋ʨ|ͨj}-0}~ΖXV*5KSV>f,oaͺHnͫJ}ʄҀN =UQ٧Gҟq$08Ul.ˆBZ'PIpu*ujnG~ PO:VOfz2SדVԓ,Iw3ҭ#Փ۪Fxqǰ5 v^T'TT=ɫz `J@j~ZMrjRPMpa9R5Q Yһj2#Iu*Iդ'wT9ܻВfwUȔ+U35 =YEBMIu{`팣"gL \p?((?(h?8~p-ѣxS*p;i t˻*$L8`#QNQH$[jNR㱒>ظijN/?Υ.Rđas\=. oa{@T~<3 ڢ!$;]D;F1k_#\LM:%f'IK>$$!N qXB8Bc)oMwꮬ"7^J+A ',se Vsqu)(TI <_ ^ TXņACj'@U]9"39RF@ܖ#}2#ϑGzȦ(g(ж~fG21+84E?|' 8G%Qo@,nMAWdzj=5<2U8xL^v0Xs粸Kx"@x%nfm e?ypt K<,g H7V[%,@U0iLu; !å\_۴9L5 <g  x2K\Ox:O׀LJ|.j5O"{ A:bſo}`~𿯪l&! GhF8#j+4yGkN 9zDXݟpV3?& ֆ˯nt^jEb7!aC)\͜RMnkC/z%5u7H]>:u|ppVO*l刓4>d ZSO疬V sȆ89ӉW$52|S ү<# BYa[1 ^(7Lg(]ڵ߼CN7HX_.++ȗZWq|t~{O#w |O?, [trXǕ~cl0Yǃ?˚ފ8WXj\qܽ[U|BM"F|5^in֖mY]ћ]:}E ʖ؝ؽi~bޫM-E{4'u'vȓf~љ}W`< Ec'i`v"f nT+sDTҟ QחC0q?ƩDOsܚ`j`7[<ՈRi_j&j?x3 6UaC6ô,?us\IO^f\0}$ie5x[nFd]sow(UDGM𐣤?]m݂-h(d )dȞBC;XaNaN]$34ØH&´х\zjCAcu'P'nriD6IZH k[\D鄵N>%DSı1p 3`'L-Yb=cBqKedia: y5U`aF"(lv`$=[omm@Iö́ybAC-.= B>X[Xbm)f|Bv/tK$H]omad&ޒDbu`z QqIB7z1SYT/ EX} # :df|4!UljMDg*V8{+٘f"=oյ  lB gp2zU/l'\bynǩl +_|T( U4m*!͒v^nڮjO؞t-ЍK`&^:7M譈[CvX|c+#5"g:dB^^ΞdvL_֮qi%dZ .˰w\ܥsr%JxZH*`HQr`j  ~뵢֌da:!»EѢxQyQeQK8u{v`}`͋Y _ Kua5΅%D5kgMDMQHE 0rBYb&ed k R\̉ڍڍ[WDA<R$HnҊfECvŰ961ktWKO )HHV b, /\n"cz"iF[DyKisX`wHG]j 1+2pK'v]N0E7ۉ -A˗.P! FiBKߧNJ{\CXo㻎a'{hdw%@MYup,ley&fFrClv9B tB3(i k $Hc,fí'X(}^8/ʹO^cjLP5 ҞnU`AO5E6#i-%rQŜrI. bK>Ce O|IJ\Y/OwRϜWH.!4vDlֺH˦i!/֗D4^5~ҖiFxåc3]bMEXCK+ 6PQERfdX1,K,πzLfDҗY(tKo h2$?Y+HKㅙDRK$&ܛd,[`}v^si"CoIp:4Q w{ī{u|{ğ:gK[Q FlᢸN?ĭѥB-yG24MAX?aA4h6dUSw6Pz${"DDOb C:& |ۤvr1="opk82υ4O> 1 ܕPW:hr%/ ZxEc@Oƒƀ)bCkv &2JUquy]Æ/GAxt qt^ytx_NN(O'M'k:YVuYՃΪiדNH髤.t^)z_*\Z4J0#F,䇃0x58ME ȇ)sE k%]@E.#/ c>rD )#\y G0!zԲUT~l 7L GAJ(>ݣ^~Zh#dv8B3Q'B D x2TD8 ?2c"._$V+} K[r6ddq.?h_ʏpA+i,6ㆰbut^ N( 󀧀h^/Q1UҦKtu :xXVQİG f2'fI2 di1S؄!=|v bzY 3Wt6<ѧT%Īҵ0X*ú-t>ɥ5=9O,4&l"b,'1o->b·y\!X㞮%6&nF1?ǒ vH!.4;IIbй 5 ZBQ.}Z3#qjXḦ́ٓP8oV)4~(#}peJz ܭUi³"u* VS"IJeEnGµEzG m;@rz>Nr[0IDvϸz>^xSaEĞBVSś(&8L [%j(NC^ʼn$~1y o&O*K7-&^P@U=çeZeZ#=\yI8=qVtzVLID zfdO`V+2]3S);D)~)ʦT_"%KIi/mt_zVJ_z^J3\o7Rɩxxfo[K_'KR>iJҥ/a>-+E=R3|7gF ^ӨsBJzK_޻;k??V?!<$٪Qgޚg*ҭH>GmBڹ|fw^RS,]]LR{yV~W+j/"2SS8&OB4Npa rg -fW¥FEfƙ1=h N lAWV*| W- nqu|>0F+^'nȺbb%= :d]ҋeznUU\18|#9I1}U<'LV8Jj&ϩ䗹͠qA(T!1{\n-`Uҗ^fF|-9%Ռ8Rn#xXRMέdKwwJ2 /W)kp%_A\|ټ~9QQ6\b"LpB~btrS$ӳ ɲ"-Ή+]v0F~*0 5֕"%|1&t"$E%E:Pd|4',OuMл#XHD-n}W'b/̈)vqƎʯ|{^wx hx>)-~ȥ.:| :\up2p^w: K/#`$z7"T~uxFI7`fb'`Ke3Zu4">2'WWX\}5'܇s]ӂ@}qеxͿd T<D@04bKq*Ī4 bI$"Sï35JX&KJ`, 'Q)l@^^ fh b&q_gL|)bNow爍n__(ISީ7"m٥_>tq4KWbH&L-+U|eu-bݘ:84 [|yx_o;ZrWI$5bXӘ& c%%}sx-@Q%Hv2\]b{7h%>tHMnhC}%*q{p􂻏^=ZKVоMIѾܛ^<_ߧߣ ppz{-pFp`kõ}yz]{p?xp<܏lzyX yս? SxT5)}Fѧjts4\nF_ѭ+5-}FGo`ܷe;Xe` DJNkeSi7zwSl'ݛґР#»HLc;u([PC$d:$3X 06"5s]f/Ka^0cFo*fOJD ?zkjhm3kĝ+"wb5ػJ[!ddB*!<]2 yq!(mKE{lxH:d$=v[44|borA+I+-#Y4:ԤQiڂ L  #׻5)d9}5 ;,'hF&f|>ٔq%wMmᖷ1=!M>"UFAO k&69݌&L{a*Lhna Q{"<2nTy&@-M< HbO|xt4SↅDODD'a* 50 ?qC^=A<iw@tppl_=m,"X-fǫ A{)U>Fho5z2@!ޓd'kxシU??U3'0324Ӵi2O2M$2({KTM&c/?2-?-CRg253HFODy:Oyg~ D-d=KT%ٟ+xkJv+2ǝ@IdcCcB^+@D_Pk1E~[+Oup_KuMx :e@$28e1zf !?Ub_ \EbN ,o |ΑS/b?1VT.E A-ۡ__1 0^64Bq"qޯ?%z1ƴE掠W7 Ro 4ېW&xt4^ &@Y0k5| ;[?kw4.@ǹ 4\^A^-$'ŊcWbUD}O SQ9إ(exR񁡮_ t::m8_$!LDyiO?·o4'%X&ln+mM9r~SA^Azۀ&GHq7 do8z-岬e݄+kǺޛ֧ȴSaq\̉C21Ժ2qva!16G̚J z S^fE{gYm/* bb>prD$3[yoմ듼{qqQUWP b21S&8]M}7p76BA4aP0u6oŢCqhІ8Ԇ&xd65O ??Av{kcKw nLeeVx^<0C}M> }/w{|61aɱx_ Q7gڇG8LlxGP!~\oل+k촷GUXV4TEU쓴DL?"4US%tP8 ^?6!`fO3m9\e r$,yƒTy-(kfe6WiMb McqWj UJ#!}H }~/#.q2 ~R|3#cp/Ŀy' ˘LT(5W5Zloh3EH*~ taU_$[9| a̙Vr'qֿ?GFx^iY1)1 t fw/'Yh ݟΎT ?"} vWR"=͆"f 58Cm9!hr,aǗE}ezZpAsJ#ʪdzhL\Qi?p4w+S૦WM?_Èa*U @jgBK@chkZ@=%K4WK}FrvL38q&^5@~+a]\/sx骓zw"U5b)DWN$vY>;p'Eq H'hi(y[N"3{ڬ"Tb" GͰ\S;yW>i ݥ*@ aJOW69Mx Y'ҲèJg2ք*=4E*A"pFA*PWl:jqaBؗ"PmuaZlՖ8ezWl~vF&W EtcOir5%jQ?qQ[ 419Wr%qNtd(Έq)8Kdq6Q wJplNV!ttt1sv+[}j%I@mV4=+ЄlC=ԃq '~N@L eǨejm @\oB$JN ]\!@/LH@SU# fAY% fEL[] juǐ.VS7m*RI $*E|aQ6'kBë N,~Yʫ̴⢞D{vNYvG=Nζ5/Lg}.V rS9rL $:**/Q)fx[!G>4gTv>7G=*z`U2(#i@|er.U6\G]#@u7SֻrA_L/vkZ.U2gmfڠj8 ǃtc"bj8m2Dpj] ^:KJE V@teuU]k%\x{K]H/baAZk <tq(F. K.Tʅ͕֖@\ޓؒ\t"QOo^ B,* h! ~B,%qv[(;`@\ѓذ[)v^Ib'_|<`-]Sfgˮ tEŹ\QgvgZtFvvr\E v+~G.V7A ~RgFokʧq ut?HS7حfS4β ;f6PW;{aN8 eE!.Ft׫Cz F0B@܀A>K#x*3qJ&b ʓl:OZɚɁiYB%D̜`14x(j͔RR$K]S37C8vfT(G- -`}8(ʥ`}it%PRwrSKײaoa@{ŲRw{<&Ät,uSK~ˮ5X^ ĮQdvWPmUK`ly)/7lF|^[L;-w{epY(V &MΤD;R1~AΊ rE]ྒ}$>BA6ӣ`VfU fm~j0*ՀY]Y]9(ٴ;Jطf56Ӣ~^ԮޫhtEj*=F&5 USk%ʧ2r-mmM;v!v v_ʵfmIkX Y2ûC^x)/-)0kݟ\4 "u]A TGC8W/ gb!T V5+y}02TAS>iK1OWq^lWFD 65!5?hWkqѾ@t c胚}037 I$`0 G1=fWfZV*F3nXO1" m-= c#yy\Mi7mJ)cDچ互uJC4J52d(YTJ؆KL{d אVgu^9zs{}.v8ڪ)J70߬e=ն򼹓KoÖTEm3lg;ڃ:fdc<^YtuQE݉9ym\FED$%{yZro`cZgZՙ5\wi^w~ԵA<`%Oߣ)!I̽6{UQ>Dq%~r@s(}Sj{ō?dhn:VuXkܕ(}rĔ1yO u5{o҂##%:5!==TQ^Z2#J!Q72Yq\{ȰXu2Uéj5vH KN8p 6 bkL̄X3QnE51BĘ؎=z(cb\g={kBf:eO m;;)Ř?+x[ ?D?jgrE+>+9d2^El?rh*j^͸-M a|6+rۢg"̟|(UU1pߟ}/1FW}0NJ+;6 XƟqߟYyS*t#.*~LjKUWt9T)XTp7wW̹BwG SBypݯĚ.~c3F̀e:?9Jly{d!3hϜ1*=v>B( uׂ~^v4CWIjWj]^8},Iءca^GjP1k͘K>Z+8зi0gYz̗sOg- r 3jf/UN{Xą/l}7q߮rm{\9g‡{e-?ƽnonfr/mծ d[vҼ͆w3f*,ɨ|D?:} ݶn&cnhY [ M|`?+W?v һw=/^ }(]ؑ{ Z=Ӵp#/Y?*~z7Ąі 5hJֹzyM.hTռRťtK{>ߡjʦu3jvH-2J>٤MW5=ٹ%O||Yc{gSP{6KaLjޭy w.Ի[R~ǥ .A;9N~w7pEOv?ޗ|f޾B{ʬ'30hc󡧀+|եQ%s].h׼mY^r}uՑ\76w[Sl[]2TÃ;,ݩ} 5o_7+_0Ȫ>)?ٯWܩ 9¹͒w^j8Vsw4噡<'geO}to&m.}}Řާ^2Hq1i3'1 2w'lŤB>)h}L֔{wQʘ˘gՌi=wF֋֑,}1ͫ/G+NN<`LuFn˜t%t2w3T{8ן̓c!m9kutʘ?R;0: ݸ*1QbRөv|ƴf0i0nŘggVEw.1sYδfi͚ZvcZCcLk5,i4.f:1?p=xig3tw|ڗ>K'm:Cki6M</z(1}Nuz} ǘ>qߛt|SY_:CÙNvs|\4Ai^lhT"613u3c ;#qd9g2Nske`r2~$9J/Iq3,{q4dMnу@YWգz'$$*S0>%9jb8wǨX&%.NqL8ja^ MHx 阠tu:ErCRB"c$&EHNЄEMҠ4 PGra871)$,)Dkta %hEqR" Qz(3?YI"SqprpqpM:E&q:.2^t RIq m$~q0UxrVQe! "   cB)Ita!,V UVX#,aE' pY p[>:QOEDl"bSh-mbNEU!:N8TGb8^ Fb*.D$.35ZqQ7;}~1_,R\<+VbJ[ 8Y ~&4hZ+`:n;p=A`\+܀ 7|/p 1`@8 h@:f_l0i`  ,,+@&X6, 6-``(A!(( ,8΃  {O<59xjA ߠ x Ml[5v3"  {Ap #?Gñp 0Da 0p:L+l΄jfn[6 v`><< xÓ 7xg^W5xV ކ|e>a-|?OC*##d! dlPG uF]G=3rA G? D/~A#(Fph"F(!QJFQ9hZhҢ,6hڌ0ڎ|tBGqTP *E'Q9 F =DW >66Ǎnp l;`;석 = `}/q8#118T</ xZ೸O2|LJA|_Õ·q5?O3\__7%CX2$ ^ZIm%;YD[rܤi/yJ$/i#JX)\$(H R$HSүi@BBiRZ%i &iK#K1X*NJgrtQ.IHz,=j7 e}YfJrGl'r7Av{ʽeWM+޲'r'Q0gy9_>("X.KSYYO˗yB.7*\n[ U{/Pa{PKiR.!(rebar/ebin/r3_hex_filename.beamT=l@~g7M 5v$ jHHQ*::uYwv `d`tag!1# ԁ Hw||N̛@8q6BLֹá[؉xSlFX < ,jPg<%0xCR Q? 6-bdc|1A} &>58j0CXi&24͖GRԬjO5H%AKF\TkS[he\3qRo>n\4?x ErhNjeqTS+>{~3W?L{l(^$!+%LW M.oMaШe# \{g{ ٺF!Ee6WdfsA- / + ^|9.Xv?sY] \{69/X>fZ%Džz,0ڔ|8L9q?rNDw8˙D8gE~pڤ=l̬mʺ- oEVh Bh/0Ae.J!sǮ׍amlpf g0Íe&>f=UY}JsjV? pebmaS}TAGh㬰"Oֱ^R'5uN]ePKiR rebar/ebin/r3_hex_http.beamU}lE߽9RnW Kz[8'X&4wnoowGkEcFbDDC $4$EH*Db":{g77yWZ *Ktmj T4N!ikQ2{ rCV|VSsK֪r*fcZ4IT!aCZ$)`HURuژ0LjNF%dȚFYSdMȪ&(h4ɪ$40ZQY+:+(JF ؛&L\C+8=(*C̷=XXHb\e)2^SS>/Y!`aUo=]Е 9n@< 8̃ !`Aa@B;#SLJQv3E{HI`{a0t,\ҷd mdښt?fH6nD1$dCTIL1CKzL3  7n&HX`xCNZ\~#K5 uCfPc?>6,^,n,  Y`Y඾g*~:ǧikxym7YGK/v+\{ @7 gfV6׿zegњ?}~rMIkG^\{7_`r.T>a.7gVn;u{;=iwݼ|2gVџ\w)g'x֊3n~z?#vV7K_~:{do+創TQCxZ3O;cU\gO7Az)#rz%L?E?MGVz6PKiRP"!rebar/ebin/r3_hex_http_httpc.beamUOhVOOI%']briH.#gI6͇dRŲ=Iis =(;`Ǟ;vY/+l:SeA}ާOw޽ @JgsEZ5vO?Gb;rin 鸶84mDJMOzZF5iS{g=# g8KRg[+i47}*i Np=;ص ϱ21{vaܫrya3ik8q40&X-z09D8J&F2JdU!Ki R5Q#ebA#-P%H!URCr\/wPJaM(+5/wzG.wN54ܨZ8M]iZUnuD:7[7 `7 : N՟9DĀ$*i ЏT =^LTNae6ƒ*jkhHǯb]m)bwۂw $nAIIrG1|mcJr7 O/DF xax by)va91|VLԂE\vRrÎI^g312&=xImTBkJ ɪދ¬0`2112 L`f#3Q2sf2H2 v#{)-er,(Ќ=ίe\7qW3of0/7֙,v{n{[x|'G'k_\/5r_du>~3ʿy쫵Vza2\vO$,ҋK˒&%i^PKiRȏC3rebar/ebin/r3_hex_pb_names.beamXTE&›܆ 4tKohC#">Ns4I;7 -Qo #"83::[g =u|:ԩS=N[[ %-=;x(?B//%#tq όF;- pd.3NX*re0l;ؤ]ߛɎf.MK+Ac.9PEWRӢ Awj_`2ᆁiNs| 稆/5Iig}Osf4;)e]*Bz W2AmPʥAG2Lw';(;X́=[WX(=ɍMa0iJRDo2&=X9zDA_6*Kvfs "w7"%q148"z.|/v~җ^ŧ6B4s9`bPmh'ݮ7} _(U 5C$hIBu|j cid_j(9)ͪ:إ\֮Ǘ2/ '_44IUnDԼj9 r5'v!^v)\9Kpt[ HХNK u"XTEAChtE2celd (2e8ed7[;\~;Д, VGJnE.SF逺t:f&9:^&rf"ںt| ysw1OJ䘝 *5[~ugdhM!%5UguSZh¹ 4*עz(-'˶7q9͎eP%+Zz2z; ;t׵Ȱ fD&Lӊ-#jim +l45c!s21:j -٬ MĄn~Ɋw{Ud];eq+2s.̾}Kk=hYb":vs'>IZE+ P,FSԉáPE [Q-b%1Nȋdƪ!m%Ӻ?@?ڤ&K `h_:T8_c*5q$ㅈ׍yZn0H準_|hn-!ZQ ,%b8%i WHM641`|QnJbq_U׉iX`EC@J8|HU7TI&J+F~mBa|Wa+ʲbR\i֊ZS̴(%nPVHKrMAb/ä0iee")m^j=~lj]CE }iRᔶRmh3RK!ie)Cz6WbVmFMakKbqr3s=IǼ|UW&-H!2 E;_ ~ѩIZٍYqc"nx]t0~ÌQ`v;v7_=vi0zלt0.DJpfO͊5HUg"aMtS]͐bcU ɯ뀹 Z30Y6)nъz|Ptk5uH\0T7::c!ֵZp&=8vSЁ$YD|c\li=,n-֯)61~sA7>7;$y;L ni3DѰDFN]0&4zqʼ[p:G7 GGu8,?Vm8 pt6[@8ɚܨrvDQIQJQIB"bm4 =J(>XPZ~D1G '6daE.-̶Z2e4|M@嚀5DvmK'oq;; 'N :wr'k#蠒zUiʳp} 5Ug;gyyv;;ɳɳ]gٝz7%g𨸵w)b|`̈́>GqE<=ثIiG3xTm蝹8ث8p)`|=Ml&{~E2j%[pOimtjKXѤ0KJ܂50#w}٧5Ĉ}>?(y7]`vNK]u&ɂ^e|@$2uGĠVgЩ@a|pmgߦE.6*pf|H Ɔ1D&EQE(E,hDЊ^z4R<%xu/ )yD5[X!F:2>t2>润|LWg<>$JyPv䜛(rѺ27Q1Mc&(8Cɲwu qD%n'nMVĊh戃4 =ZqRJ UIzEA 3&k+ǧ8+p||)A!wRR> qC}ݒpX%y"0=wzX;#cC|Qг'BᡪD%jZPSpp|\+^a @}EP8Τ{118PgGf^3:׋^}8w$ڛ}ڏ{q_q~oH~?7# u{q/~z>EXCdؤl{R3gJ`DJޟ<k Vɳ%߂Z#viL_OSXW%OS+ٌ~)P.֋nlxSBKrtk@%Ȭjɒi :;34o ZFyϵ} gi_|#/JnWyu_|K˵*j7~,I~Bi%% rzE7{$}Sa{+W1Y4 k$kHŗ؅x`>|VZZ*ڞcorC oe@baWsny/ 穀o7K>( yH^VtKɛ4HqC%WUiyCaJDPZ\Yo%^TA 1&_"ߒ Q3o_Jfiы#^>'EoWJیˊO6!~Z]ޖ|j;w$?[ q?KY[s/nQ_>AX/~؀_lK"mIƬxG;ݷ̊wa8GIt}Y=xǝN~eH>]'ƻ_Gj5.T|~8+_y ǠۃW{%>`%|U>}HqIMnX|O!BY䏐DZ\ tV L}ޅY]ejV|$k1>6;m1~QI4T CPsmXLG1KֆCQѓ.z uˤC?VjCk eS,HtMub\-}H5D(&?SʢK6 [@'ZN` u8jo(aB:S`>Q j?'Upjԙw`>_p='Ӏ{'rjZ) Lo6[ɟ\ZM17g8?L:gFw@[ָFyPw+,X24Fק&>RX=x;gú\/ujߕ jGt I˅+׉?AWtV tSU~})Z ̥I){}8 hH}}/馠l,;PQYTFP@  T ߗ[юstzΗ{O ^WV% 5 <:$M<:irJ,f$պK = ?8N9g^ Q@%˞GE&n.=ނ!ʚeԣ虷eGC@le zWi=zpA ڭei}AvG9~:re6,x9/HOg"=tQN }5B͜voGmmr=,{u@ l6h>6XCm!0$:͡à LB$AL o1B|I0GXPWv(Sx .u,M0ar:K,.2ڠGJV4* N>6RC!j9D9%6Hkż(. ̈́@QCD0:-BA̙5xMYʴ'i%ed+9 &Xw3,(Uqa6:$ƙq>3Nftcq/&hdc=E֌ƅ릲2#@ak|P@ @foT0?匟x?rpGw2~0O1N PEzƩg- q ԟFĴNu {<2E~Loq) ;پ|azo+]i:W>+;k ?}eqN%foy/;'{;: Y'?;{[²GJ.X ;Oa1-ղǗ*CoN7,5܋[:zmNy̠1#oڸ_:e'Wk n(osQ՜cڜ3'qaë[̛RCƂKjΥ`Cz Ew?xFW+5kWӃ Rn=UUr~rwߋ[֭=yDZ'(dါ%9tכ#g΋WZRgFiOGؾEWgp+}[P."_`Ev7ES>;1Npi/TPiox0qz/1N@V޿\6z0N{qw;p0~ [_ib?㴎q<եև~tg7+Pt'ύ=NdSt_]T<⮨Sr@Zb6T.ӿ҅hᦠ RB?+AOO>(гԕҀuaVsUZCShm['A5sވ{euSPӠAƯWh DգMU rKr{BnH\ c mXY)۲ CY]L!Xj.aH3{`v@ZQW|s*;u+xf٠_YO?'#E8J$bx8^(zE(5b@ wqRlWxD<*Ogx d9vCpBLh\KX|v x <p7)xe7':Ox3~o[6'I%ngq5#$$ODҁIGL$}dd0#bRFLƓ vr Uԓ2L'd%YO"ϒF2yl!m}%r%9MΑi%u9^N rY"wrl`y|\ ErLrn*/SVӾLmռ~EݲUn\tPgG/ZR(ug(Z)C-.ZLVX2uSJFnTʦeF5VZlL3j W&m߮)&NiV-iUkRo-8᪺jeKuUȤR׊V3zfP̮_ԣ-` Z0׫]KiBF}>S6-5mcݰ`}"6>95:>&S^|_`cĝ_zn|jVV 0ȜZ1UkV\WˌDqAE(YlV@)uwM-֠ U%aV f-VśBQ =iM x`5MFaN-W5-:jz-B)ԫ3SJsȵ5+ 'ZsV--KM ^VF65nltd. =W*ZuZ4*奲yqumܑaǔӄ(rTmxp4RK;~Wq $\]+@M-C&YnvzcCWfQ6"68tge5?v z̎;M,zZ gzi/sם{q+zزIܓx"#=jyUj%CLWgn5z =# -rSrViXEj*CR܄+ŀ2鯝O.5^xPGZA5m{@ ]&s&3`zV CyD1}H-R,zt޻y28<իY9VL` zH9v=xW5^p OY> ʞ|pRܔ!9L-+*ù 6hʛ1qP C1d=: )$#)#@$#[tXrXz*],1xl̾_՟|&AR2B$EvhƝ OQÛ hN^Ht#VDHdI%V %B2V$яʴ@ada?< BH!DZ )vO)*ᴒ" O!K ^Z$DpbHs$\XL #9|Ord8vFފN!X[1}>QDG)>-s> x'D;!sOP|2N7H,KFxZ<ߦXKSYrqE^rw(>x-p߄G$tK)~otzEPD? Gɘ> "DT`jT^11rFp3v_18ci:A2}1ȡC1}̺tԤ}Cy&m!3D'Ob E cHDȵh\|5rChOW[߸+Vx…HAB/z|Cf.vMֵ(S] 26U$,,Hψrs868*W8j F/"ɒXCk!޿Yv"`d|',F$2x ki1W&M9?@rz$M/zu," 1|%>{"m| py<|m|]|6y_6{\kp| B+> >2["[#ߧ.ؒհӟ`>B1ŋDE>w}E+U_0;LW;4`ς8s<V_0t?P ?^c!_vON3J 4Fr8}pJ8 AL^αK!bk krB"_؈օhѶ2R/)6 X?ȗS!0맮Sf%HSp=S:;]L~1o'`= o,{a买WӑѽMl 55,N0uB+w_>xYy|E'P8I %v;{R0H%8AEGO<i$ ͎"(PPAa=@U(?tUuի}ի25_> 'B6}zNiP @<BQYM_5^&ؘm:vz_ |Й@>TM䗂BBYEQYi}|Erxvֺ^_@`?m Fi@rx^po;Zʡk>mV5IqT7gFk k}v\MNSÐf#bp}Зg s;rs %gK ](HD~E*UmBK.U7]f;a2cTWD6Ԙ/7JBBu>!IuzЕ6Fw&؇RpSVhkSlfcX$"4CrM8-ЇЧa=x5޲W?-y3<]?y3i^-k> ^! n|w/d6M 拖qu4ӬX\%lzr]r,Z_$ `\D4B,g{~1Q)hJ0DSOKڜ:#M'(MP֛m=puZB{lz`d=/#֍ޞ]++<~ X@X_Wb(>NTX'fcn tbX_dz`}_qnX7{{n!!uuu 7B  _$Ѵ,i?#p|G+D. Sb]Ak*H؛%) {J6l8+nA#m/oXm{6T{݁݃%;`?뇺ökp$Gy3olh'`ckfmiiil@\݆lNvd1d-`mKacdcN+6+6v9l!ŋDq×DtBUWb)dzE.:Lz`}w;X@[U Bn\$݄oFv%Hc;z>@zjpRReURUlA^llۋO>`.r?lWQzQEdMp_- iDE 5?5(c.CA'nF {G>&Ma0SK?> }Л}g 6]zHzĐ.#%MÔ8g~TKMヾd~8$ I|ðC# ݆΍e5,(G#ĦKu^7m{X? HZ ]0~:9G!aK_UT^l ҏI-99b$}Zؽr,״Z7 [^STV¾,컐%$oU0~gIS?t$O ^MP Qu'g6H/JtOBKuLN>ZA>rHc>iFaX4k9 ɖ`z1 dIkmӧxk8E;zXr!aRZYakhݒ#| _dH9 D>c  )4Z0+  1H?F34ʣqqM`| քl࠭MImyXy;`W`wZ$\9_2C9W ^^(qi¥g"qI\kq#,q%$ isXЦ@o$I0ëV V9 $ iׂ/tZa#ror>u:::~ܗXJ G GXX#A F#Km `^fKC<#!ޞCbۯwg:g/\$"===(!襄]aOA PJ_7S*ToP)9bX)(D}%¡!Bx(! †)ڦQ5a= ޞ]++IkÚ<{l9Ӱˏ1 G݃=}N~ nOrI.ݓpfX2 ):;E=쬛u2kɖ'{݄b~o,.Iрq8˲'g3_Yn/q?B6 8_`a`&k8=n+jXG$8Op2tl=GJcޏ9Pw@8@6h+ 48I<&G4G ͸0Ls0aV$_ /6Mƕŧje4 4G{ LAt)ɩhJ"'*s81e?%c?F iX͔ &uo-g;g,|5AzBJcHHmjQnGD+Ч :?.*T8W8 | 珏3̇y#aB8wAo3Ź-@cˁfeymSA5xz3/Xf0QzseL:DR_l'i}͠sci>5qCT N:%o: 2ΜUm|>_3™3Í. Zΐ`4ꎡl9(c,,_@uNNd 89 <e""(ZI6pߘ/7L x4_N;)1R"h5"<7Y<ڴY*(s c30s֜lr"PEp_k4"21^iBĎ;7Yj?|X |L7Li_2&DHxD2C6+DWE1"ZXؗAҪVi)U{c~w{yw_I#U0R$[)o`P*)o5BU$F0NyNslRhیM0-:KKds[+oNv@:muvA'Kg~.Yufnhek'UM#-}ܖցqyDHSgo:;ڌMG2٭:>Al@P"y#0yQdQ/.ڍ1r M'IFV{UʳϢ*XXyBQND,Q"B쓬V^ŹhĊ'"N>CCxC۫$g'b-YxQ08҈GNPrb Ku`4ȡS>_uK0R^t]#Clh媢e%Qv>&NZ뜀N:9K=_ӓ0mMK(q\ )hDQʮ߼P߾WaQ>M7dIoOyь.Hc@N[rcgFbzLEwWQi&~fUo/ffr9+Q-I`9oA&yA1$0 E4Ex%|E U6Ǘ*&%)Ԥc"rʗ1drCbLZd[A2$mTfLa$˒Y9ʨ,^.)i_J=t٬攦 "6M$(~D)A~f@0] ҡ*ͮnmUA"%ZmvC.X&[â帪-L]}Y4D}~tʿ"_PVI1')(Noj> ! i.(l `%|T _7aOnQI8@~w $ ]wlbЈC,]7ѥadQiM sDz[Q6^E}B(jgEM/xWwQԈ~GQo]jV ͤgUwN>]D,y\ =m "TH-+v/v.ZgeWjީNm6O$#u]ɿWC>bTQp@MEU7g;1N*h'ÉoïZ F}UFR$kjdU 3F q+?Fjٰڇ>S!i$AP:kd.CA0SnF;FGcSlˌqv3,Yc0~\}ROxh:# ̊ /5ZČ5F0<7yM}ɑ{OyY30fӲFl, ͉TY's3ùYÓ0V;婶*B| 4O84M;P Qӽ# .k8黟M+ %8`,f ]Ep, 8ZbzSJ"Ce*U"Q<8c7a]e pܪW0+ ezY-x/Z5%磌 F @uXOk- p]cuo*k Q 쫉}pwp5:XR7%6ЯǽJā*Lp|mx Vu3 ushJi餺Iplnh$n6Obߏm(s/KqO!ެ_Y/~ig%g1<𴴡i N+u@44v8tU10|0^xC:0|a71:űܶ%FD};m0!S0z3c/xB| }i.> i=}x-s89*p9's\ @q< [inp<㉐9u98#<!i9Cq`\J{8 ^8U3N9Iv۸λ[9V!p9n F{e#8=\1ѵ\#!?J-+Z5pY18P1SEW%U81bh/|c{OprLuhD0h5ˆa6G ٨h~ "y~ _9_tgyM x:ʷ헅DCթt)ETNI 3 jS)h^o;Z\7jq3ꖞYԌju"5#c&Ics us'th7ŬM/j2qԈSRZmogŲZT~}&N3[>h7#5ul^g~.|f#\;t'cJcv\9|o;uqNQ>snT@l1=k^x׾ܫsO.8r!1AXr60?!8ǿAG'rL)1k<9hc3 $I}r(PIc6aYpAfrLA8Nq7 C䘾pL%9~ 3͐D:aiC82㊐9º0R8ѓ?oB<=8 ;W1qL+\Ӆcz>?{lE #THMGQ;JW{E(W^~ =cyڃ,=wV\ڃxQ)ŅૢMh[t4'ۈ|Wk̚zOgW 2`kjmUs-1SN bfUr9يĬ9նSm%[ڵ%Ϯodjd_1NNv"5ʾڴTտ0UuZ~M6EJSî>}9Jgon3P^c&,J .sUΡ36h&dݔ/f**;Bs]/ģFCq_08Q#qdš*q3UL}E,'jN%Hk߳"vLJXҴyw!bƐ\ҋTvqSQʠҼնTK3o(PXR8ynʋ1cUVs U{zv4<3V,Qi '٘6ڨ7=mup:22$wY8'BGi=mn[gtB?Ѓ=J_qz_֑etrXgY֛>lMg3=fqVǐUlkdϲUV5lֳػl+`1b߲} }$;~c'YhY:A\p5CA cV#`$Xr\$b0Ӱx^WZ[`| {`/41 1b.|^X8a,RfSpN24»prt>1|c=>+ոnp3n G nOq'~_l]=x!Oe|7l>[<=ռ/c|o|5__|o7|򳂈΢)(׋ADLubX,A<%Q<'>;ŗb8 8-4FfH"e̕y̗> ɁDCX9QNd.gYr K[VȘ|mv*If:|IBMgӃBzةTN:lJD5Yc_ 4::+U=GWIGFR zӪRT3Tv\$H[u[8 .iR)]i"Q׊,bF^xf0[,׋U:ao(m`B1]ӽTK0E|7Pjb CȔRQq|SO eh\.x4T(WQ-gA({Q;<^N80GeOمxTJFP.[+`\ö֠"5K֚))%7hl5ӤΊf3G*iW@;qKB3b`MzC|S3fm,iBdiveml.%a#3i7Xf"戥uɮm1-#3 eS+FNisZU  6s<9ݴe]5Bbva'Vհ s%\E,1&fH>J34ŶPk#I%FYH}`h 0;ՆmWMBY~EbpmX=K[L#RͳRlv^,oj7` 1N3ōA1y#n) 57k&oovEeBlDcdg]0V11WsUvjYz(!ↆ?qMGR:^M7!s>h\'dqKh]tCH>>W,|\2QK{a=T9&y2R=[1 e.qC,2bQXnBʆJ⸵6@$=8_+.FmEbXebo4 vh|qmYQK$cd4rfg4[ًq^$,v:l ~gb%,.~r)h7o[\+-UU߂"QծXaMP۩kHְt*Z 5pZWdddi%ɬjPFYk+r^/@6glV`B٭\W~7yjׂI9U7ӭfD^Jٛ8Ib[8OONj:Ԛ{K=Y<: :gmiDM^g= 1.c?P??=ꖥ^sSdA=MPQ8,1}HiЋJĉpN7ᆫOae!n7'I8$'%V4ܯo#aWIf1z~K g&ĩb9Lǣ풧iZEc!R=a"Ө_c$6kXk -s>6e2Wtdy Ɩ "87UbH!݁ά0ZU'$MƟ\!ʝS-zd~KZbQ.7F锍ci#`S]dA gTD|{ :k7 9 ɋYVn ןܕ0Z/$ċynTػR@U/ YJ^.8waW!?)1/+U_gɏb/hn[xy/T쫒>yHa>%q`#׽?- Hbo}FM_G? A/cOAt?D ;T[O,}^xxKt\.I~4`__ϚtM16_E?&@~B?.@/%ėYj(/zRZ!G@+ Ud:J/%(t_{FJ6קU˒< 0-גhoC$?0Do۩$[ɟѷEATv^!*$wY겿wtާO P*C{ }R=P*~To}mРo|;_@_<Ai>y>s>Y&>u>hϿ/ PϟA;|ς9=>OڳWT[+홒3?1D`o0_{Lww''[*w*(j 8Ԁݽa*51 jb[LmҨҘmlLkJZKoqIh'󽹙!y>030\&%?[X)s}KO&Z>zeקC.FG {|kߩ΋5#fýW YB:V}nڙG7.Dݨ4c[c{{В_}os`iiom>۶vtm2  ?LwUT0X{̽7k{n¥?l:ؑ(yWdYV{'uod_Tk/V7=W-}Sy3:k9_<>(=즙O?m;Î$6OxN"$[vs4_33 f3f؋W1&Ze0D1!0!% N?s`DCe3tCJ0t*ECߏ2(ga3`:i e 6F MSY;-5$ȯJI.wdn152= jA=C%¡H\Ori,>+WK8X" #2*94A}!^E4:RdT2^=^a#Dit[\.$ BV6Ep:9}yyDSMpOFH2z˽GP8TU 8$%OՀ'S5˭)(%Q*Q9 "+OE7|S@YWA\/ T\UMBS)IWe$*^l~AeEmJ2W*Tr^'_1+er-J5"?',:Υh6ͥu6 t#=AOӴH;ezޠ=[zާ>C ^8` $R! C:d\@Xv(EeV@%AmUޅu P a l@3>B; > K1 тp8cDL‰SqLBXED:KЍQ \؀q3nmwnl<ƣV<X!PKiRqL"?"rebar/ebin/r3_hex_pb_versions.beamXktSǵx${zcyG 3Ƅ !,KGci$BMZhmдMrvZu޽<,X+4gߞ=#x|OϷm[j>_[R1+f\2RSEsu+lJQț:[ta\oI3c=Tgv\Y[E7f*W̰`c_JuRѭ|E.\^/d=s)F^)K#UKOgu6䀦 z+cv'`v]OYUCOKVl5w&UɗFZKyDʌ+.RV95/! 3 Z8]r$83L2i+3ځI7-{"5Q*+zW*J@~v֙XʖZ'q=^깅ٜY' Tb$ &KoNgicAvNPމTI%lTCXp.‚^Fk!_[)U.T'S,z`m'ϹۤY+tg%UEXf \5$+saZ/gmr0\96\niuaW{BWFdR͠7r9#uCn*RJScR&,wL4C1j73Tk(MSL']O܉+aglEyF .؁GofP*c'HRs0R64tv<!T yl~|ZԋeKE{s:t J9n; K sR ɁY- }fW5lNٷD>q%#cTF Cuf.٫99rԓO~_qz31 ^r;5Ϝ%[9=ݠ8 zO[uU6|Irgi .pBr8mvS.mfdo\RQ/Y B*rkXgNLB^Y H2᪗Tڴ/kXn UAe&֑&y}YR<+lyWoÑ77^FJ@WoC ?(uF/q3ml-jL>{XT*ntzoѭ 3Ax w#;O4I8:G՚"aEj4-(?ͯvTh-a_t<14ޥ $QU릁0WdȝC$vHM 5τɔFAL dQrUz* kk1Oz z#e\S!785bL2:8ըO=4D @ {a!)n8JX"珞WXGX7ђVS}ѐW`Mp"C?~y0=1:Ts`X%lN*ʕ.mS!yJTS7 ( lB1^:5H9Fl@3?Ц|@ *```[<,XҎpt+SqS؁C<].)X6slmvam.K"݂sf^6ե͇yGÂ- E11Kݎe-];wv5^2g΃WG)'l"ON+>Hli1PQܪ),D-2qFQĪ0Aa+aa <Oڕ0$GHvum1=-ݽ+yz[q2h멈FQ= 1'=BjcO{ k}TGX o "x "*6QX-ؽi)lkQhM{Gy`|PM Z|#( ;`[a:=wR#l`Q5t#k*|a`#B:o)kJ3C|a[  VF Kk Wm[h =C؃I¶mӪ÷>"CIThloos}(SyJfͅ*uvJ';Z2[ 11X|suB= S3=8M*GC7ԈwUKwAv`zdnؠ7;[4Hdc';Ca^&l7)7=l=RTPYǼ'1vN{VhNnG=زH+o6*: {:6ӱۥcw#O@75ұ ̷$~D+$+{ '-feS>>Zj=qÉşƝG xLЯ%aox`[T2@1Lo7TZ? @ek=Pr gBRyf `Kg@uNJ~AtA@]d@=q!!!& "ȨDOA8E";( xy}Ȏ]s{ (v;y`C`#y|f# > ;@y|j= ڃD#ɀ@_ 0b QG,?SƎ R\7wrl@ X;Og`I}'<>W?z )~{>?;xgD3-nϊoʿ]ʿ$oݴk'7_h,fH=m?[{73>9REK<)B!)_V`~K^[I߄MJnV xS(FS CH>b,E),Ӗ$$)R(a-k,Tq 䡠"*l ("ΝhO;sݳ3޹vmr"PrU\$)YP%Ij3TS }2f<ȱS}&rz-N+,,ڦQ qXevSBSEƑ*pVCfMV-:T֢sUɮR(H]m-|.u4{B2 d7bT9wvhdâ/N.dˁLrS],7V\N dAx_CAj2i! [TG}}P…8K &Kɢ= Hh!,! {-jVE_hkhSU.EUTIGc͌Moj'^}&J*gQShp~IbdXj6P·$ɼHTu6)ZT_=v': ]@= .JA $ɼbU+PzygP86ĉ0% I!-p9信q((@#xarvD1=͜3NqHb%6o&.hp0)Goljpqr;m7A=)p)KPKFUPܠpұ$ɼ0(| 8OIjadUZA z4N ,Ț=信}P8OQ@c*,:-ĞfəL %P2KI3 IPsIJDx},/hUmt (`r `9tWFsu?, hY&!V"(q(X0aL K j\?.X+Clh6p:z:mcF=mvgܣYF6ZYz::U.df+P6 8*`)sCztP5-WMdtTLg %Hw%"kF!XJ 5FZ2XC)s(2UML%"1P3ꍛkfdYPD Ñ?ԪZZUZm@UҐX0 nܸ\زZ at\ W;*"ijVҔc m4a) XѦtPd[:+I\[R$)eZҔT R`֚HW,Dg-) fB9%A2LGBnn2݂XDe3v')Eqs2%N8{d[/R; L% 4Bl#FxKF} "!Q"ZMs#|0̪s'f4QGGBZcʀjRj-CP: ehQUNU@pr~b, fAASu)p{G4ڔv\WNizBP{@G߀P˼~C)V%ړ? hW6Oڇ ,ɐC~7'͘ EF*)T(XkTAlc``T/)F/Յ(c…2%r-Gx(īx96%tF.>#D"qxro;W__SWړj ::j@۪4{P-2PA)@ Vf1jPC 3.8pCups.p:: \H0U)✨ L5%SEE&vHXG̞HUJ P~%`X EVZRjX_z[ 2F G$ѤnQn,4VQLNG-œS<5TK(bGb/ )vørSXM1Y\иMq[xhF%^b/W@H-^G1Lq;~9j.tnMhd3Q`+(?=?gk~gپMLsds?FMbjhUhgw7+_ԬNX8vZ3cńo;k17Uv 3 yX'/,.bȒ`վ|_^2K- {{4WayEQqKx#CMXvlkk޶Q~[X\8gXKbW[\ +)ȣ^}m;Jȹdk.Fs=5@c>!|_—nWS'| ^A)_ſ['2#I/'ȉr9Y6-4mN$wʃgI<Ox^Z+q,~5~Fo $_5|bXXKZh4&k zk}ڣZͧUiӴ6[Uk mL[kkoh &mUۡ}վh vF;].iگ@T : Z @ˀ5&]@@fg +j oPKiR%  rebar/ebin/r3_hex_registry.beamVoDl3n8m6i6i4Yim)rZi4DҮw=I+6DDo R/\@^P!8f=K Xzoy{3gξqi !yԙًWEH!4OVV ;[lVp* IbTja6uM;벀AjVyZaA[}V'.&Nf_lw"rj)~v=bhU *jzrfW<˦º/lu'j4nڢv|_gmV%`IV/Uraǣ0..5:.u7|2h5V-ĩT2J].{̯XJ -0,i`"WE vSR#5R%r %4/m)6XV3/!ZnZs\2zޠg٢{ZpaJFmU3 }='"UꠂW?" 2Wkj8"}p옒vg7bl@9`H""` `q0PF2 AiǗ`#YnOLRsBL^ġ샔@ğįae2kDcٓ<|R#DlgXl^a|lX`MI<gX)X{˜%XpBuU.yxqbQ~z:G .\}1an?'_?Σ5ЂWҢ@s>fsu‘Մm4\΍)ǿ|~7n>.C߳s~ʫ|k@Wm_C^2?>+YtQ(՝WR Yqm'fr eZs*n!6>9=D:n!a}le+(4vx/7#|[m֌2RQn4f^jPIcTc2+2Ԓ\,7  Â}v⛐Ҵt<5Q:CS4J-6hiVK:}G/PKiRӮLV}Xrebar/ebin/r3_hex_repo.beamWolIۮ϶iRǛlBN]LK k TYk׍kv))$@zHB 8OTC@*{~ y,@B*7y/~I,?g:B ;=3žKv=X/սb5۫6W)Wiݪ"%+ ;CZ]vU-L u;WnڢUu+qH)ڥc[ٯ뵘gaMB7Pk!ݬV sRڞzE{-ڝmd%{w++Wt[NU_) 4b5kZEn$CʖSiաN^KUIQC?*U)-Rj3ĺf9Jn要fUnBbQ\w8%w/kQsw0;)5ʴMLv[^8δO.^P3d'}B+ZEX_ jPUP%:vP*tJ"U@JjRSIS#0")OPМ,GRքK%1iVXdZgZPJTS$ݍME]Pzz)M$14ׄZI6Au 8ܒ MVܕ y%O3[2ans[6/"u|K}4/*>:2ǷL@,څEC͑܅EM=ʩ)Cn^$U0{T7' 5~0' a&MS"G:!+-i: 5N׮ݰ-ww@zJf@/Ϟ1^gCς!6^n}6lx tH4x/a/[I@MжxhblclՌAgFGg1 thɸ+#M0L6 G>JSb{4gmЮp@|X)Cacx<-`Ƴ@ȒP .c)$ւgl<x\$;fs }.D?>% x9=,_q:L²=Je' ' 4<b&TϵaNcNa)`yNP.·c,,BX:^c~ߝ(H @|">} }p]\g8x.= >LjO3a-9q1uıspņ0nؓ/"iIn?֊ *r{A~RgAvc{uee^Yy%7Ҏw[/]ލ kYmW_z?я>KʜwW]jhεaA^tC dmmtsHy^FSeQW"E$^Qk;>x`ǿ]p)C;k̀mԋo evk8e샂噭[>$ $ȗ%r̒#O /PKiR'v0rebar/ebin/r3_hex_tarball.beamXwW$uJ.H"edbĖ I $ RX R 0MIBJ}v,Mgz&3=gpa恓`ΙW%dz|\U~߾ܫMl?6n pH؇RN<mȔc+i*>ہ|Tmgj!S̗vbg=<. 797rՎ8bj0758} [|ڙ6ayIgNt=ΦJX\+ rv!W+HSăv͊xc)Wm^ǃUC xlfD 9!6sXک!WEB4("S٘-|4V pC`gR z2ı Nc-Wa7|<ہ\!D3C j̕#bilJ', g{ m֪pf?l$m 2-Y{=ZMS9]ea*3VHb Rj U 90vrg"<lv8k$;Ɗ?0mqNgƦ/8'<\EQP8Z V+ŲC͵pE9eI-Ny,U-eITmIÇPrdjHDF=T冫c'P,مFd=̆=j;6sT)팄a"xӅȕf?6mLgVI4{Gs}Fq7Ty 0 bɩ̇z6@pKR`4Çs`Il!<*a4CfӨ~J5R9 ~bj +d`nh,PuV7Qg^nhȋf|zlcHB\+`#|k)>waZ*kcK܁`)fjݽiyVtG3=Ɗq#. VFħ0KS-IZfMX5x*S9At!d!XfF[O͔$?7)?YVߎ8UAdBL*Z Bi-Kiшύ5VR؎3,P:3a8(7?*Z Qk0LwZ@] aStM ^3{USMKL= 0;/M)`T͕[ ia6(qV6ie)VԔ},ɛjVo*c `Y*^Ͻ=*①t#AMg5}'ը[^PN 4Š,fW,e@ vc߅D3f??%uzDVzÓP[P]]5r4ؤ%,݂H&Ē{&M3YZH0̀g4iix~:yNm5W&& D¤ZH])2lﲈ7WIQe.ϔPŋ5 .lqU$${xeMpEӪLኩH=\I1Ud 6Y*.<"uK-)#ZGZM9W8;y h\j0k!w P !m t!dFt` D*{%y0p? V-杆nsܚs}}N;AnwٝF[I߆y.[TM ܙ u_jj$_!iZ@Z:_(d/$_Ku[ݺc𥉵 u\6y/YRM2}6.2} v6 f]IE`\&-,|ŐILy o%"k7d%uK`#Cir1LƸ$$ t3ˌmڀ:] VBUxD{+]r]~ Ӵ )?SU! I7SB]օ!itjG*69)eEJ:9$ekeku*|-ȭqz[I&:5~I5f x@=r[BVty40B것53iNmւbL`_7yj͐l!c&8@mj^PنZengȻNY^^7 9 Q;MKT=$)#'mAћj Z!9n=(0=MW,v;&R5*MZb[h3vMȦ IV%Rhݳt;@p3{InPn۬^!k%fT~%4![iJ Y=7I2X@$k$oH-.6C{>pR &3]P s%ԩޯM%:dP}toy-`-uf[=fL2clYip:`粭oPͨZc@Bkz 0 Ѓtp:%.jr|o/F$ NݼۙTSd*MϊDCڹe;pGn49$OCLTLVU|ޤ ѝP a8U ݔ@CYv`GVf<RuRHHɸ{( P>4%G`ۣS|!$82[׼ ۬Q c.8 jϻ#hD+z^f&<p._sџ C$uυْ? "{ѓ%ehGi` 8U$2h_=º^@ 8s^E -OD1}.{ɠP%[z5fǼr+`qZx~}q \*Ax4AlM?j_,fP)%n_uP2sWIe-INy-mɔonIow م$N)qĘi0o6?8%+.{R )XS Q;ĕ^{ = i}KK((KʀŊE?'*ypV>H%#q=E>WJ?vD:8GP}%'.^Ÿ77qGXlǗ.~Q>ciKZ_j5/E9'ݸP>+Ğ:%%_S0x tW"HU,AH 6 iV*gLa]@2k]3 Kȟ˒ O5ZmTT?[)_.b%[Xט/n4(H^jL8h-T-MhHlĻq־u m#+9f FdC!ߒނ֊P+%I#QҎ)N1ORuƩԘOHK'ǔGAda-H.$6|#zId'G˾0X?&% IYPĞ?[ Oϫ?K̍_V{Te쀑@|?!mU!! /VŢb E>R,h(TKk=ns3_|ofwfG")8B) D&ʦH8E&go8ܞ[m\X?-'E ;]YN!w'94-|/ad^+H8g6`i*ތpFS25s類s w5]y`ƥe*$rE?UڄWHmWH\0뵇|Oƹ%XPV }|a .05.^h^-~WŇד,xZd-, N ]1^Gɽ~o#PTg'c(F)z^h_C gc<ǭ}%g>I= D5K)'5,x 7R@!z(YK9O(_O9Hk!Q9'ⶴ4QfhYT_Fkc(_aGy6$|(gh׊\^4o"3@t&PTG"H/Bg+I;Dss+}UT'DITOm}0>㔓(JPr2ǃ0R}DwORN<%iK6iRۯԳMbؘ=Z fZ=Tc˩~}VLhw:=c)/:NnIwѧiS^mY3'v<߶;:w6zRg(u*\\z$$6!}G~h˫][n7ODWK=Pm"=Og_Eχ.4ռOfS<[XMU5RNr?Kr@s0)'$YK2gb,~KuG9ɛK99I99?A9ɶcHtf|#0ϾS)'gHIN!D`Ourc՜MuTnG9$ڨ.la1!gMJs{lx3))\0=~)ZLvƴT֤c&x'5ަ4Ac0t&MomӴzwɨk{eoM֠f^2) CʘkLqاҘͤVfIiP %V4C9Uҥ*qP ZC9NiӘLFSZ3Ŋ3Tք/5ij5e $|dSc샼||{lL34:fJcNFFmKdƥ*MJKc'ƛ{4z%gc _QQ F( EHf9(ibA&}Q*Ee:FgyԈP=. P;Ntunt#X$`Na0ƀ0 &`:̄Y s!B,U>5ra=l`BlϠJaT@%T>pqB-K2\fhN ] ?}x<`# Hp}'A&8 WM)@W a0AxU$ |+C ~PKiR5 0 rebar/ebin/r3_safe_erl_term.beamXkxxe{VhJ+p YŠ撎-Ldr,#`0dhHD (M&iRH6imKڦi^ү_ٕ0hwv朙39ﻳYsǺRnCx:_g #la{#^`4}P_9#Rݙ-*WiҐ<6c,W6f\ۄۖޝg`Μãe/Ұ1[( M Ѧl13Ϫ"[dz7 Ns3[ \a@_:{NJBHvHpdxp)_,`&VnfJӜLnfGenJ67̍f}OZ7fq94L&ոk|! PhiKi`xd1@ddx4 #R5) Fb.Rv3nѹO۞BPgO<7xkO@ D>hBkՈ&X€geJbYsy4LO;k@`G9,3Jz [̤7h@xKvaA/Q P6AhiAgy9\$yWt]ZK!hHu)lb(D@1֧[P{j/ޠL+ cs!cWwk͕&[ vVz f,,"Ce_g/̖ڮOKL!: L ~NdHs=J)  S&"CWAie8"XEJ\4B3C^Ag|W3{kl OIAM)Qq0,Ehau*eߎN}Zr:RztK W#\y_LpcE2Lc0] '  f7.ʄvOO <bqȳɳ%z:VSK N^?SMJƓ}ٔiir)1L]`](K0*T-6Mu範KqdZK5jG()"aK⒋;QCUFBo,t'SJoAPpiLW#nA\⣪%Λ6-<7SԄ\YWEzmU" @$^ *$~u$ih"Pk;((XVBo*@Ÿz6 }@>uz{  cRL]`x^W|@)U쾖BIPI>e!~= ԽSЫ! y3]/5 Ҡ3qHwMwS9 RR~bqaFBOF$ Zc|.x$h;BBUeF|yh/)^Mqi64JܖK!T C t1N&8'5JG涣mGˤYB3enN;272P\1 ]/$v.V:g<+ʻ6uCquq\.D]2.mRJ]I`ѻX%VaB7ؖp-,8>y-$tx2bT[eX0l d0eڹQˇRLH-4[l7dxc5 :LU4^؄pJH r@9"y.Hv((QA{p`u{P'GQ'#44ǕQ;@x E;SQ7}qA36Ѓ\y=$ 04WG Yo#\m'3Ab{-C>#f\#Yi1}=cvT=?&elSAs}t+̺| n8EY1vtR'`9˾t-d;{65,ijm,h hm m3 =^;LYa/cD(?71xBz""4Ok&QWhl:&&@§ 3 o.!!(~KiA'_D zjVCu{BqN~eU|#uL M|k?3.  ( P? Oׇς>SOTԁ;PxNEoX -K(>[x,ζX j̇OИ%CA_jLT p^}lػ /$O(lJɱ3TL3v-pw^)VNL -W+(bEtZf" (F}.͵pgw}ޟoj/_rma;sDJtO[悋Iv A &9Y<1i k tH?g Of_/%:h ;JL ,^gݽe2pCPvS LA!Jܡ4eE(vQ62e wXK(ϻ(mredO9c$+pNj\#H#b2'[h3˳)kU|qT*8*#/ܮs2S)c${mY.>rJ%U)WRYC r2pNK%(P%>I AŐVbR9+vLBVEbmw@\*ّU$WˊDJ]qz]7@JDW'Wb[\DK*]Y `Hb 2caH2!upPIPɇlᵲ/rHEigJ!PFTF*cꔱ~Cצ:NQt5`%iQ?J7l4C}ӮǨت6jЫFjluoHܤzH- wIK9$ EQ&@媽:}N>@$NW%99(ުIqvGWe ]b,߮9zW!y=dZ= w̟߽|ww0oC߄LTO,&I<7!%TVĚ M,zX#7Sc^755o2b-)ezQN'1q:^tngL//eKÙ?rT>E20j޶|$0|'Cʗ<*ˠ/!yv3ys(F=\T:cn:, 5[c$A!VLbɬOx!ЗvF!NuJa+sf,%#&/iLjZHhxm&:\k[x'ޅw܌}>a#Cx>|_Wq/{}3fp.Gshb^kG|ma-ڂ[0E55-lѴ/:k5c08a,s\´|6iAgt} />dPKiR)w[rebar/ebin/rebar.appW]*}﯈lԮ+?pշ=Q0 Mw x]e=0,ɍHhI YB\hN*">c4Q7!ܝֳHbsل"YP]TCN%3x Ǔ|pU EEl1:& XWfn$qE`@%`[=&݀YG$)`0p&Jd % D¶Nqȴugq'k/P)Y $-GѰ[lEۥjk{Y-| dw31ɪx3) }4IF%W^+B5[9P|;ӁFazűjS1ϚhTWiD=(L>ӥ\Cd_?1>w2Û %NjgW.hEޗ͟4i@k^<+AZSƱcL(`\NTJq797/ =~rrWB 6՝l=0P۽q>04%]Jԇ'4r9[Oj=p;O؂?#.,EƖb!6#Urʖur|aْ=3_!ݳ??G(6;u@(ߕ~؟/PKiREy rebar/ebin/rebar3.beamY xUJuҷ:PIwx?:<.!IHJBJ A F"9uDV")m\i9I 23)}!t9Ӕ,r~lL:UgR!=!܎D"a6 vTX,Dbpf$NQE )\\O ;a %#fKhI%=%C=L'QϢ<-$x@qe ISv+=urVqLYXdI*zz{ R'LZ!x[LERczZT4-Y4̜R2IjJD\CQ91h! bʴpVZD&ҭ=@G bJ/d#m,c&zr[LK8!4 6Bex"fuOA8]Czl\ J*&G }~sQ :z J,Zg٪Z,A 14hN+%ړM#ARVD4I h&B Z䡞VPn&'}u/*XImu+3`#6d*R\R؞T S 0 c1 1-wú]!1Wd,Q)dm-\e gAIb9cE]!9"mINjglŎܽnvK60'9](4I8%$sl+H:l1#"si[*5I.KWnvqz4΃z=%%9DθDax:h)9r⊂q>FN)#9͵{[ӒML􋢁6PZq`I6+=K`\i<7{9[%+3P]K> [9cU ,`h]#:. \2wdQ;1{i1i.@teK0mtA1k#.9`Nf, JkT\&[v%`8)% ,OH]"7(rE9p|q;\0IBs 4 bpNcQ8L]&M7%Lp2UcJV cM5Q-S@b0#R'6E#$Ӟr:c *H;Lˬ\Q!$cL$ 4FrTZ˸2򢱐ģ"P3B% a4rf4u]l/R ᅍD/S|M\ɢQ?6h,<ral/cmTQBCe,\fD3`a6d`Lf9 os9L Ӹ0 Lp&Թ |2VI*? ʰJ"wm@ӪD<1H*c :WG0@T }-a <,HΛi)GӪ,"iʢ*a*tZmzH5hǒcy6Y^E K@=$/0P Df %\5 KBwE[CˀbQ C9P-gJ%z%''yZib|а\NL(\tL--nV30Y^wSó8* aFI9:$2$+D`KpR)@Y^@#:H%f-,@IqGf}d.n61_fM~ Ԉ4Vh9@VhE@5@9@M Ȫl7#9ZiY {+PS9߬LhPm WC&b+i\\HXiAnTў*I,0*0&#eyz839Wř^a!I΅frVCrְTfUˢ GZr@ځ[Q=H >]-OO V RM)ՔBB8mfA)-sNP3>4@\ ˀg]%Z*JL]~"F㝤P_O*ه&XS%,abQ#۬n-J5nh%:Z"B\p&bPT7lD,;!V ݩ;7N4!rL'z;uvҠ!⬛ĆnvLXVh@?C]DbE*$ I8:S,W(gdMgI(, 0S63 f, t/$04C膹; 4P B4W=I;BXɴ:U f2y*O% wi)uxZj]R놜l1 -%WZ*%$oh&CD`Ed*Y 8Hү1P`ol亸6@_K.P9zgu֋_6i yu"Ke+\eU uEW^Thn I$HJh3Y]t;ӆDzP.0pPx~!CBӽ|!0opẁ@#Ԕj@MD\Tb(ٴrVK-Ęh͗Fj଱^# iWIˍCh=!zP ~) δZjslyѽh Tlȩh8|38J@vWh'MAЯ(Hχ۬G({ne;AYG5 !"#$ , )u+2:R븥JbQRtjE]I3Hh"wCŤ\`EEq 6 nT" " +{`E/&'LJ $B7gw>5^Ii[К8< ap0le ɰF6#PaFer0ռ!| (-4%}T~Tf C4Rz9C\A6QY c *f|2^7^?SPzȹT' Iא)?XH{(9'ɥkgӐ8\ !g1GJ'6f0)M` 3zIM1t}'\s(Mc-IεP;$5Tf< 3)M}VTtm}L3ĺpȱ ͋cŮOͰ36ug/~Ӷ4wo ] OêPLW/Zo.|Uw˴gO}Nykυ--/^\O[]nyG/|>KozgdKfxw;yt5O6~_l':‰oqг5'wCz_ԅOk?~٥h~%pk[-8_h>|/[T9johw~Sz+>ҟ ]EΧYdh԰;/z_Ԟu׽ god?z;l,cE[:2!vvqwx /,zߦ^YVquy>,}?6 ;\i?;ε'vmf^E[?+=7OgNhmMUU[Ҋa^<;?ڇd&<{ԣyN[%ݲg:_ޖ}`Zf}`߭3y/:l9ۼdzv:x䟟ynN:P#Ƕo=q_z>'Gnn޻c{O0_C>x۪]^xq1tw$ꍧwM<:{ܳXFNJ>v3Iϼ>RWt3!D3xiϒ`QDB7D#-ȐȄUEfɂ7>O$_djؗMӟKFF,.#U'u[fd8,k`q$fdF1{T: 2F~'=3*jH˺况;T4pHiij *)-~Rv19Eޢ3/]L|dE:6=A 7q;ɋ\~.?k"_ίWͼ̷A^C|=|/ wCAK>( $8<?Ͽ̿¿ʿ_??,vl< `Gx'b%/ r܌Wx pVq'(N$_V|=މwx/ ws|ߋG~+k0~?I| ?O3Y3K-a``PKiRQW$rebar/ebin/rebar_agent.beamYmpGzѬEg5k\5FbI|bF|ذZ펤z?$ Șm xΥ.$q*/u8W+8JrU*OJa~ߧ};[]z;,\r)CB<1bfKb)Q(3!/E0nR999"p:.MLƓ2L$d,|!2Dl6=k[T]. :Nglb$C"1c"*wDXzkHY \1ə :QS>Kg=,LzL3dM,3%ol٨srLHY/zHA "a's$#ùL*3W[Pugs=y0JLjly8Bc{rzBzr!l%Cfb Rke x[l\uXX <&(t,7?tk-kB4IaÕ|MXkl;! D6f2 3vL2x@oimɆ- NI qTEUeoAKر\qJbxr[ZrJ+Bw0-bdc %RЭ9M^2(M\P߆,w%R+R5^5F%[K-K }J_@ eخ+RmĔ>͜v5MY?*FO3jUagSh3U%3l.9g.]/Efͯ2+uW1}}{ }GO H7d*vܚ[S fG}eGDm[,.zSۨ["g.EZU!zv:OTp;7P_58w ({a6+^n0 2=O7XЛ_(|KWxnF@"UU"Y侪zo=.K 2]0 uȠ%KvMt:4KV]7 ҔHlYVh'7jxp'`U ~v/b"wwЀ-"X hxbvХuhF%7ѥM mZWeYcBl U݁~߬80~#ۈN$me"l*Ae9҃"Ts*@eY2JD6l*], Y$f79atFwG,_3FP5z #߀EV؜d,8' 'ԣMlN+i\ ^QTH,Kd=3|Z*y1ߊNWtD6jHaNXѳ =гjq&iK;yHj0Bd$яTY!7d@6TxG2d?4aWh~->у))Tܼ+T59]F܌]-i, \PM\QQM*`xHG]4=ף| o̒F#F1{e%N8-KԞ hD6؇ ;<0d=Jy&S *o054M6,UZ뺳v!]a]M.ͫW5T[a,0z#щmHл-PGxzתF4aݣpܨN3nvfylpyԣ>īAv2 =4QH=qӊ"w&c>hx(n hM4*{{Qi {1t H<0["÷nAjT,|nt+D }X"\۱f] m+6,ɞXm38{^72dUgE>D t;*veُ+Jú 7,` up;]LS+9 a?H!7|ς9a,Vg=9҉><ЋB LY"ʾ>Wuq;/q2-,I*4t`_,gp*5bT:`iD8WcyU l_#:?ϒ !iir!$)]P\D&Qa|"@Z$0ӕ6- ; AqF8QP=&M8;X)ޫUOʼn"*fAO5(#t),2%:l'q%/z~[h'Fʢ_EϱKa,ʁU'vzJN%ഠ$&OEl?fa igelfϥvYZϳy~LC0Id9jV$,P^y> ۗ\C;moUḅQNo-?S^:v0G9+,, ~uf}gxV4DA/ط.9^Wh'v>0qG {V+-uewNfu}ooIM V ~&[m{ H_+,wkX.c7Z~A|>!3@څCcy<ێ&UE#y?_ز"4 MC >_@[)Ж h!}XKGkmx:߀&!0؟T1>3͎Vg8;N~.^1|Q̒"', gk~mTA͠#Cs=[%Ó̂_tp9L=<"hKŢ^,@LsVԋ>G%W3^g6їc_W2.)1{IpUcR $1^ψg>/m֒ok!֝w<{usyIuOZ#_'~X^loW~k%KZ~ZOY_l>YiHj >yw=ϻ=esbXO> ׿,o=?{uG#qܑyﳿy{R;.^b~w˳_ږKn9-IQ+h'D_B?]l Q϶XoHCh$!;ڪVfmdvl;33 "B""U?M q wT\A8{ZI|=ߜ30B1AVЧ%QŰ#!9i}TՙÊT$VĴvt&輭Vک3Am׺CѪums/uNg6*j+ Qߗ8f<Lv R[A`'\!``NOrM.+P26lj_WޥA*f|^`L )nַbF; K#ʔ!y̒bƲN62ҵ+9f5f8i.Ms mi tEzHdV .#:(,F9*0 QX*,rDPD=Ј5hB3Z [ ъ6;щ.tcz{bGpD i`9! $Fq qp0K+k۸{xxxxxWxq;|'||w1*_/DaC4P%-*ZJ*ji=N.j4uBPKiR{vNrebar/ebin/rebar_api.beamUkGY֌:q֎R, vpTueҎVs|+ɵPL(=`B/i潷fO}PO֓!L9k:v"oi<B+ gNѼp򞓌vTvrÃy0rfpitXb;KBSFcP"'ٍջ< tq]eQl'a3Aeĉb+tY?!/iQfeSYW!OJ-9AY)Ggùs{ry9ワL)7o}pĩ=CqDD"ZlU(5J]nwZ=zvK*Ֆhu>?yUwj54ۅziisĆQZN-4[ji֪m\ש9Z>x:+)V|֘gx-ⴝ޹FR dZ{]r mCř֝BpI .$S.H*+=sVs $uZFK3Fk$D-U Z,>j]NY$DTC ċNU؜YhNk)H[Պf Xk~swzRf HgہOJѪ#  Hiylguk5!loQ>ްjNiͧG6/.%6څf3Q8H0Oh%l _:5w}YAI Q]Kh>ogf$# ©W8$ jC(t곝jMFO;Z/ժur 5Zݘ/MQGjC(1 Z\jUdQ<2ZVY+ ew(Dz jVu@XפDqb!zA(XUX/erg=:4iV괝_xp 7 w#} dZW`!ۃ$+[jx X=7RR:X <ˆHB͇pN-5Im& 099a'N%ȴ9B4NuZSJ]!)/axV#0.+SAl'd s=:;cd@75({T =Er|[gH4[i~W@a(pTQ˭!n(#TTsٲFn]l LF2nߚ0_b,v~0e@~kZ8zǠYmY~H k&$ aЧKAпGJ{=>9֘A9_igf V|SL8(4s8IӔ=9( ^0eL%[cU%ON~ PD? Fڜ0,}1@}@h'jǪ}B2Oolq2 yC8,A-&{Fm l[ɀNpT!<>Q}j.YT=FN5iea'%-C.[lڴnj7 W#@n?IlcHy)YBD~tٴ2Z#ma2<#?kR ( Yʚ<~AzϘVx*.5YJTd;J~* O\O $z^y$6:,Ȳ0t%i:2etz|dTV*JL%+S,!,zruP~Nr' vI9$*@ޢF? "^"*a^_v2_iV6?=~V}339f$5RZONFz´L]kpшdY(K0UT2JPMaGԒG  6cAPX?i`!ǘ@ N1ɹA B /); Gu - RVh8pƚDfђ p=Qnkۍb@|b9mk\.@…%QǍy.Uܗ| 5 όqNڱbJ9)ft ia8U[?Pˊ ѳLI С1³$k+GQ%VZ:d'~rSN臵myP򰜩^tӏ)^Ǥ{o\^tр4Ui/b+9qES] *gQVO8^'P/KvR:DZ/ N{rw9h-*Jݯ/A̋78a·By"7,~Rg& L5dKL~JNIao-s~klj#r\b i9?)$ Rx  ~ßa7z iA kV|Mo%nToai1+&l 5ij؄m RH׽h5D?=%.C wTǍiRmHH}S\m@r_ctw4ǿ3+K_=~ˆ5 u>`@^60!@]02K6|y|}~WFWnTL fyrRO MFј1'UӔ`jZxm9_%:/ȇ7[0TޖMԶK S4珷ooo+>{At.?ͫo$Տe\O23WWjOE] 9l&׽}g}<~k !|C|c hD8e]5!MS4|>!>& ɮ(2 ZBu?Nw>UFd ¯zi>O<)1㸽)3' "!' <5[BYkor}8~eX%)oң`qP?=o)nH}s| +2R cD6< sMreC5QQ:Ẽ(eyE~~>I9= ~?ݽ+upX~_ }{$t5)3 BmH0l| >$ >|݄8_BoG2HOV(?RΫ/AғYKPXٳA+# OIn+܆#Q19tD"3n+O,6V` R(7X Bp->Xφ`= p '¿#oij)Ds ݁ $m< ބ3tv8'pZB4z8Dk+?Gxm 1<Cty3Ot&C2 pр1XXޫCj%>)Zΰ2HM9tuwQo BN53 SzeT")_N|xm3JJ\p=N=a`_dRlkKZh&ߍm8_6l|٨'p-Yb?iCGԙjۚoFɍ^}*J^"4x֥7ڳju'.uh? }4ߟ,h;P#uޜ[rm{؋ݹguWzu)=a~H1FU<(NpNUtp9QA'Wv(25CcEgaFܜ [r -3{QWd}pL&r3J2nuԸ̜u]g-Q]aɏKR3|8~z-:2d33l穖r̓zsSI ۩nRύzVðHsCPDz*eLk|AGѽ kh yy,C<8 γ(8Ǡ|~!3~~h]aפS=$$E#~\Ztv|ڨ{1Y%fHb(#e%GoיEV,rh drld'e$*!\)瘵C̊6_V,Zl+!fVE6iEà5D$KfK:c͊fИL(zɤB2"!lvbWFI+(etz2-WkΦ JlLJ)#Ϭ[3C焆 lMv(F~A[:h3,1Ԭdg 8QboNߏ0!04X !`%u`TH4rEJ Ja;TBj:={4ah#p>pNA+p. A܁@?_1>Eb0 <z>|:| K!7!0 f?PKiRc*rebar/ebin/rebar_app_info.beamYl/|a@Y]!]UTol6dj"XFjB[=b[{zsNm+=~;Yl{~Mfy݆-| 4X-N|k}>β3.ҥR*W(*g_ TC'j[u&r'(։tⴻzj._iSB8D'TD.N`w1Wpx3.s2La/ɥAD[) |-dG>IWsBXHes`V+ T{MWTTsta2iPUJSSQM:tKdPip= ^_&t5P-לrqZzrdh)OCZŲD-E4ldOu=bګe[h*u!Nɐfd/N*eĴX!a| uÐQN)w: Een/5Ur!v3 ^Ov2TuiEyB/y/O)M޹g)VKntM7=j O'Nx>4뒕%߽rU*]ϧjn"AOWr>avݣ3]Ï<5CDٙr.T@;SAۙf]tU٘7/|6ߎa]sctfq>]9FhSa&V^?Q+j7%rU]Nş}Dv!ӵ}D1|>p׈_jK2_T}B|&*@fd<9OI_2Te֔nU;uUUVŸ1a$ JDR5!?iVg%qLΒi-+fus1iV"VEULyJ6<-U^R&,G3~xHn@gYдlZUL:GuqeeZ-ZnD?%X|G;gFlTj'Qvo^^i?hh3-n_XYQX$̄l vU&uEej+p( l -UB #`t~vfW6¨r ([Ki]#]ǻЅvwY%y]DςE'tHBIHR#n$fR2R!|T5Ӄz8a%ˣ.ز:c*g-Bt״8^ۊaM @DD@9C ~uv)@Q6xy+gx,W@H - ^׈ssVN@8LN$! V.our&(H~XDV J#U:5Z.A^EUd^ j-P0FU?çڡ+aaUP?k%sKPD5QֲQvh"K W[\ިskqgH/,JRpа2H-º jb4h]L]ftDX<g%BVΒK[MRrŗCjge5o]k"M Y|Ti-Z'8>-P;41gIUuʇa'Okv(lX텗w7S J;%sDmm]Aڎ@M!Q;%wl;%x HyKy{tvĥ MHܸӐO{KSJd-#XxR5DuKCNX܁::BPwY|R꤇jFB -|CN PԔ:DlP :M3ZlId"jC}9Gݐj{ CCZlZxR⡾ ԼD 6 5<&}H w[|y<(Q VE@C~PO=!)k{ uzP>* _}CPUI ,~?PPP ̀B{`܎l!np}⇡p=,q`_ 1p$֍q-T M?K0Xh͸0.9jZM[Mں.' EA? "{||Obe? G=bCv<b>Ǘ&uݒc} V ?+ydstChfi␇fEq 4pş'rG4=nړ=-5? '=.O\Sݥ5y0`G9M)SSҋ0{ZH şÒ1ŏAᘇ{L3=Jbş33s e{\Ǡ0 oY8{%+$g-> Ywv3`C~'p=!qs}Ā?t< /i!p%mH\Bc1wQdžs8eGdx(ch'DmӉ;숁mM߰u+qBzcmt]A##jQ +/!sEvL&ve&6oٛCLӎm.TGr2 M1Dgގ{'uzW1=0ФFM]L"/pɃQ]EƗYZ\9~kK͟[u禗`/Ca7I<"-Z+]g&t]C'ӂ}vkOǪwy:~%SF%*4Ѹr A͸hg~<>9~3]FYW&U%:"vD.&knNe瘫{nZuK_&/o7L(~o!}#~Zj)dn&2(T0!|06Xx}Uv} zI,_lM`+ 1ΐ!Asl <>тkV?Ї.SKpR\I >"cƏppF:je} K@fHqR 0/|Gcʖl'ɉe=\2.>9ш,8}O@qObO?"&tvY`2P;tdm>qoM<<1gES|T[?v.ޢُKR? n;pŸ.?_ki_\ Qwۗ⺬.C˛tWeHz{%..yLWg@JHZm=~jY/*OQDb.{ 0̬JcVVEkVFQk#1z4j}Q7 |vޛy$EJM߃xO5]x<x&L3x&RE<˓L=e y+x?j\R}줱9 ?ͧ&ۇAOu&|γxKsGgx '|I<˨ b< f?_$M,W< "g> byɗ g—$oA5[ӓ_O,: g¿N>XX{8XxO"B,=%H,S36XgKµĢDǂCyFL,@ɻCzENEMo>aBx>wxW@bNҫwJbђ|桚&:/uk梞t^>^uafn„sGD v B?m㏹ôM߆o\vŎ>ga/qUggl=z`mXԼܝWtb4!q=k$ye=zKxj*-i]?-w\(|Vwvsq_8}baxꠛ7I 8:X\._\L]\ E3^(牦ff d*/rR!Ъ\yejo[xDxhh^Xΰd=G+ c>lu-/s劅|bA rP̸n*NkXMB "Q(W/5E{K|\UT-ø 5u^mtBv]5lzF59+, wehoNizY+Ҙ< V5<inzՊZI+l5V88Oz;:5R+}$ ѱū:U +lVi:å6(4VRUpJ-3^{v,cXQcq5 p0jkj3qN\/H(V)1& ډj”1zժ{WsTnB<N88ިi0TVeeCAZ (Z+z)WsVL^pVKgf`DC[͞ꫵ(Ni oյQ){Ka]ȮP&5?>8l3md1'wc?1-W20 ~hLlyW]STCG޾gxZͪsuCу<,W1f 8+u،br .Q_1LݱXbvAgE=)%0vAb" 4Z4I3 L]h%Rcf"yXCYuw< 1% bAy+);<Μ A1{%Jh^JI!+Y7O(?Bv BbEPo4vH"yz \6/;|\E^4a[$^k5$"X.x0T { ݼuKplD1t kXbaILt$Xk:?!Z+S4 8Y{g62lYdуNwA} &B .;(EIx#V\q Y sr̡%V\9KAμ0r|ˀ&m@1IG|7Ͷ? U_ /_0d4_U.3dxP-, rs,:l͒|4idḾ"!AϐIiw!Kh,E1 S1hٿm$!$2d 4b9RTE $Khl<4 F8Dt?-mkgA-&Y^a*Ŀf/f cexM#Az)L" ےW刾bq3$2Z.#tRU0Cz+Wu02ylOV)4!܂ Ia .G}]a:=!(VFؖ*OW2oQbK)sbn++(+ '-EiUlJTښ od:Rp#g,4P>&A?;¯ Y_iZݞL"u/{oN)fvf)V~CڮKSu O >KOXM6DMy)= ؾnPը;L1T9dL4zPw1j4(TS Y7hLF쳄m 7-3n@T~S'U$!PɐM~!dP_cI#cNo1ʍ+!*EUJ|Mh\O %I7&Y %A#UDWY*hLAj(!NS6o-:@( DY!ϻZº NG;͋е&YHOk1Vb7?"h?Ylkp6H'WDzNȳ&z9z \ַQ^pxL2tm(lDèm&yd96|cCE8$ >ΐ!` w eM8fлwsCoH21d:h!-,pFa3_[GXJX46 #j-h<RYx&MIoidH$BBf$6s-P8jP:[̃.A[ok{R{qq#KH.B-0Y͏zXA.8D# (9B9E2VLp2Da)uo f;L v%2lG:`I]>uRʧ|!oUHENؤD ]mi I_'o*v+L ^4'(pi1ܥDvxdY?8ݭ{ۮo>TD?\CQفopAذ3O>ILZbQV 8~/`) *h`\+7p*Px;=s{j~* g!sL#$%KCuMq/{eJƭݶ[Lg Mws! XMһ0c !pA0"[PArA\)@^D }|Ʒ1$gþvh+z1rC0AA(eX9h}j*,Ʀ\axt2$_ZkABkQP]I p>tWbp`z~ .8f$sr 30ؔ(O*| ҥӾ,_1~ʧE@Ё"E@$*CVخHb0[J,lyk+I"Gp="k-B!j@B 4} +CD)tߊoaGHQĺ.mZ.^OkA" IakXlELg&WHjܓe­)pcJeu$VLn𷣭7w1ǐ;L LMC HTDXkN|QE.S)eHiДA+J4p0R}U0pAfF&<6pYM&Ӂvamp^#iz ;^WWX kA 4`6QG6C&Mpws{x:28i s5N724KH?u]Cqƿ:ml-˜8b}t;2-ё#80đsX5z=\/;p^vkxN9yno+Ԁ9pOxu؅?R[x߾֌\zXߡ7/}w>K'V'Xxu|tc;q>/E߹ݟ޻rh/~'h{w==W78yoz^/SdFۼQUϛ~Vjww7XPVJXѽ)Sdjx9VJBQ3@j:|R.l`fC9U˻:C@U&Eɚda= w:UK#[¥c!aT H51k:+EU 背NyT@%  [s$0"ZxF3*("U)3#Ju ;ežp HB#,қﵓQ*H,n Mp'|lEVrfjpĿٜ`4Q4V, , rۡY' ψ3HCmX,np;L]OXDM"Avob/t 9KΦK8QiZiAc54A3)5AŋU@܌vOXrVX%6X`:;/蜟LQ`2N~CPA+ .Qf8:34,gq'EeDtyAEŅ6D"r d+2WI!ɫ e + |%( TXa*(UDWJx}Aa5ĿG@,^,1z.B=io֌ kYdjhPXăX&1DĠy : O؀HRBTSu Q/$1'D-pr*˘0l7ąh! NeaL#GhB9D{/s ٲ=7"@BB*?\ a(LY"DR $P_8mb@SHv_o;7H)j.I$1P'f X>+-MiԈd1l= pcB+o'I6x81Rd7_DV4_AB? L8m4mL!YY8Z, 'z*^-3 od:[ Q$(D8DXXk7G7#BWҪMϯ04 #iFeH\x`0,N$~HkL8!kDqRP4`sݰ4.›0JnJf^$y9C $ X TADSA_H2!~X>! Qf DшTlZ!{{ gX^?>p eqa+ӭξ!mЇ}BFDZhIHHeL\ںr[k3a|d-<[j, 58|#Y(F }nrDhFLӝvDa֚2Ob( QB hSQ:ľd|r@F70BF7JIDEXi>u~1ۦjd.C]wy*:A w ÕSӕSP^Z*{u@=. uFlZ%E꒹/a\1\x[\ E]9ߘ. EniWw45nTqWoujPRlmG1=Oںoq?_8_>p3凿z:}7~x}_|Bߞw8ṁߌDyh;3-[?[{cʛguO/qg`<^|~+Pۻ=_5Gz ʫ{r?Uv=Kw;UNvOhMPw_ypжȱ_N>w:cw''Wo8AN ǁ'^/yC~MkGvT 4nםva>ܞwnۋk@aW\+:t,qk 5 x^LgϬ"2?ƾpc=^\2wmE*螙g8 UkhRoQ$SңpFt՘7jmkhͨpuuQ} .̒J5g+tn[,2sJ gz0Y7\m&s4 ϣl\(v|tÄO2'0l_K2?Fyor̬ӽ`Gryy§Wou+'M(/]A!{G#5u7?OEP DȢH(i,,tDEGH?7PKiR&`2rebar/ebin/rebar_compiler.beam|XwGWeTiEC-摲%#^9 yʲ6YR0y2C2B$}dsr~̇@jIli}u=6ܚfQxIu_ݚs|T|9+/{%a6+J5]/3 |fsVlРY{]V*nfb~VoV,^֚|Z~v#?_,+y{Z\^4"Y ZKŞ r7[[‰BWxŲv[`aURf;e-Uz9_> /FnZ,͖UOZ]CÒzZw3Vnժup\zZ.mNXJ~^ϑ{6Ur}+|lZRPn-9`ha l@7[U1j6JeרJQKj^jA Z1@kU %LITmd]7C5/y(gUNR. `Jm6,!rx|y բ_\i*!lshE@p)R9NTpY)$!`z͗m_]W"Ek9Z:٧Jbo~ BK{l\;Gq'īUn!޴V_jUxf;JA@I0ɉ\;*T@xo!7 1_ \+l]~SƆ5oBLߌu"n2A˹;_MMA e͑‡F"Bp$QJ;m=YeP<"PBnN[H$hXd]& ryEbpV&gJ|,f\)w{$raz)UR4%V>K~r˃g0eݼ5?c}zt+NܗԾ+k":Ez~Ds+PTز#5Kݝ#+vk5&Nmd^iEګy>gmJzgeiϦȲ[CJy8a<*9.ٶ֊I"_<GϡyܿxEsbnR6Fߴ1jSGj\)|PtW(~?(Lb)UqGx4:6' &!1ՙP4:59E{C΂O3h(BDŽWj2Ssŗ EoH{tTϘNTUܩ'vf,93E߅WO]O91-37 ~MW" gw2 ŽqXЫ ǛHr_$y^c,8ʕK%xpS nRIjfc͈CGWn:,}!CՑ7灺ČM{@pDII@T% !vOzaBN21EorMC1G˚}mBr ݡбyOipC^UzxAss[B{`69l+&Ma60&}ö1Xs#J{abP>'0-{f{{} %xX7G} 1kużg8##tc ;,}"tQg4k\ᚡ/{60},\w3Դ0M`9-+! Ø,4D[FhnMp`FO ӓ0u3 ;Lr<$ A[`B5e1uNf"XBUhmêL&MYms(G(߆ۆ@@ }SBr 2rus1x}I;TI)BDx/ u]g`HGc!claoώAݚj 'd*`4nF)!F_j#(Xҏ"(3ITxYv؄/öHU*/&i&5m&S&=\ ?2M"nu8qn Gn{!|H3I͜*Xq,'~>,8{a@>r$PAo0Ir? D51ʇf1@a6y#RH8j7awH1&R86 }I'R~%4#aSvA@7A/N+8:SF)Ad-S1-biB?~dy"f\ZH&dLz((Q(gWL៽1ۯ= _a*~ůⷧbk[[vmfh83u G3<^ZF{\'a%axNSp>pg"g'*O G]\{嶅n( CPsf6XYr|@xU%V!Q[;H"h!1&x5}_qII30G2 _0Soqi`ݣJv|R u; 0}@Iz)&ׄ PMϹMzɰOV9Ad'S٤,cLggY=T>s8 xNfΎg 6x5^ dhҹ\ q{-RG) "uE՘8)dC)6o~DatS%ޮ@;9StgౣKUw`0QA c".sn,8S ;F ?DCK,'c0A-a7Yx~U v<LFـ6-nIwW}wM@|Wj궄B.3^L6;~6YSвBG wkq-.cTQ}_PMs".d._X 6K]l_j\׳zHloj~2/:+_ˠ{z4/P)}DC+{Q4 ࿇. iX2DPî@pڗAs ٿ{tAgu+SǩKgqFb瘺~+[@jj.CV#/F+Rw v3 59Ps DlqcS KlM[jk=7 A lѦ_"8Q#عU wY,7_=wHoԼh 7A跐0'8wp0D6UL VI>?6+?ٜHL %ѯ:ܵIwLNH`aV*0i>+*n*i$U2i޺WJ8q00}t#;Vӭ ^& F{iӐTT%?(?C.; M5 4ݑDdq97,|ܟk9T=Q΀T#P1 .?QE.A,:tc^~KʬݎiTM#Ue$ O )JШp?bS$OQr: xʦGXNGeo$y"?/M=nGZᶄ= х.A?E7!B" 'LiVSw)}B}"M_REhK-p6?QY{q~z ]}j+-h ?Y[\>%>}>R؇}vTkI_!>{Jva7gvaw0f)s+fc]/o}'Kٿ׮Ao_-Q~.#P5w¾h-]Ӄ+,+/JF& h|aa{o[xU&}Z>vuhƿQطq~;w 0:(%Vec}@eCV putSt1m"IP~4JZ6E=i(mR7-AA,_*"x"B(ȇUQsx2z'/t0s3d7{oIJ M6) v$,F^LrEѪZjENkfDjWd+ iQSxo.r -1Wt{&w–j6U/4b6(fLׄn3Ho)FQlrAx5R>,6My3Voꍪh&G-QT-G,o26Y | ڐp"x'*y1,yG*.ՅeUC[I|*U%fV\`"oۮQ) Y=q?vnQE;d\xՊN2he{WwpWdIvF%ICf*$H$i֓Rݜd$JdOT@q{=>Ş-Xr8rWGn ^}>d}\/` :Xl pޖMx\ AE~z?k6a.;[n .5.U>flU_WY)E+ڿ#tGT=jwB'eB|sP]fVL SEuU4k>wRbbGo%/èt=;oǑ)v|b/7+y/dmǨGQ[}?-R=2-]wȓDD _QaKOROZTPm*,"68ɭ*XT)E@6@)0;,Qi6 [>! lf9OZs]b|1mGђ7f/A: EyTJV"'=)1;[`O[>ȟ󖿈clGRLu߈h{_nɬVK%v m˫ پT3^g*V;;DYuP0)_\-wCڣ*U'zZo9}JOrJqlxU4"ˆ0 !L$Sd}nq=fzߠC׾qs3¦m.f[|u6xu]Adg":ξKgIBG1i&~8]th]X!\&Bk0#3T&3`fz^7>/c7Y`vLŲ6 cY> A6=e|Y,X9 JX f3l9g+jְZ {d26gس-dR6`Lv>Spc` (r">(?@>q0PKiR`&50"rebar/ebin/rebar_compiler_dag.beamXՕVn.p VZ'n,vEղՒ^f@fqH-a`H82ab%O9+um0>]U{]Ϊ.}WQ +ß>ꮢ[q[nWoM6A/hm4j.QWKsG|cOU˵;rcmY s֘ȷ^'zcnU.͎XRnzhu7cnXn4[o5+暵F[Is,_i\͹Œ w,d%rU^7jxovUIŊ>7DmJnkV̏B;֛V)v&[钻0wwvw]0m\Ou_;Ց5p'FQP+cg,m5:Эq*fMR>nTk;%PthZ W#;hxn [.5GJ"2`֥ dzF@ ReDW {q=.̔Z5zObrx6aˌak 0o$hXO4W{Aj#s-)ao/Ix8pY)0S̰Kɏ~եwCACa0!a3dyY{ zo",C22ԧ^GӪ>PX]~="C1o >feB鰴#0YAHJf72bzGtɣ/kj gᩰwL7Dw"hO/8SVNKdM@T<}]$.qGB ublvT38b`114uA D3y#L&qҖd5~NT,H2D4>˂(XĠ,LRa /"r'_yzn\KԔ$fiXf؉t\X `Ɨ9²}_,6D:)'6J%éQS" ]}vdaPm?;h폋|" -])oN#E[:w'k|9Y{fAFrpT| 56Yf&чL[ ?Z֮v&a/.dIc2/j|j3C ь١R,H1 ҡdy|a:Dz+,_@ؕilx|Qz+)ְ Ib #ҜBY_GzDH$@TICZ\媎f2Dbٽ܆E+h4?o&:=3 'cUplk=d&>5"'<~h#@ТLG(l4_N$02eʬ Gzɢ[BbuUBfJb 06:~Hg`Epڞ ݼ/MBDuP$F\+@OuHMJ&ɝ0#L J \55HPMJO&eBeP V o8e1/^S\Fa iPO _~T(_P<)J~и:dž<*:ĝ0@B 4J{bTj|61$fzMKuS 6J~4m]p)@Qpr|-z@\G FF zBt_K|X2P.*/z|[|!& 32=2 2lR7w{Iq9`ɾX0HD剣)HJ q+f ƭӌ[}b ʲ2 w= Z7 ?:ST0Xf3FEA#k| z|+]yLJ` 쾒7M| *W+\* l?L}0X>Go1x8;识T:_Bm,TA Uox|+7e$8qA<~95v&~^P&֮/A] s3^PǰTB(Cz&k1LYneP$<~53#>z ҈1B ^Moy|Z\-n+mQԷш'Lq֐'Dc#qoD\= yIQq=B{Û==QHDbـ,zd=r8Ų`Q1֥L9r@bͶ 9 VYQG|{|OtfR-[-2ߵR3u b6}T"jR@w+P{{pcjSEmP1GX8"!E.D`wz?@lC=LyNP@"ʨaC((>CC "\|w}D`4]ߍ2 瞭F\ DPD>F.&q(9 {~/"x J=xRs?<@Q~ydR< ~tWcd3#>P쉫$T#%9{@SL؟< ~A` 2~ג)Of`6nL{P|O$2.L8HY4elFg#LDXz1] C(3oK KUpNKÈCp^6aiǟc$`|͈m,Yx-*U6 uPN(< /xS1/pj*(,j4@_'; ]%N!wiJ??ooܴR 9i54}oP!!bWhA * ?bh!̞7F$Ѐi 19m|/~~bT>wz P=LG} ∟B5օ~gpX3%ST14gQSچn7倭(π# Jadsjv.7kH:E>y1%vlncYZL}#(Ԡ0N8UOr #4 0` & 1`.>=W5Eקx{|`7h~h~T"^OaDQЏ#~ێ/5/;wq̻aOIq rTٌ Q> ƚߑQyeȺgDܓQ5tD^!$'HON?OcQǟ(44BӃ,C'#PXP{T (=Xҫ 5cH"|/ߋNK >eRxj'ɓL?pg Oʼn??;oßg? G<O@փP/?n ٦TQ47UkpS][%2T"KA"&k@11#K $($ƄGHDiL(1P 8t6t ͓ 6tEve1D:! .1 10zif)Ǒd//ywD_RRHHl̑ݮ$IoҳodsXU,&"fVveSp5Ks%}Od'9xRNN]uC[DplHdQvZd&9~+KRR+80?ܖ9;l\W-O:G2lbDق tzގsIYq!Y%y{tR_t!KJ̈́ m3dcq\lN*.. [e_sygeGtHKm9 sv\YUfAȓn̅bsH!* Y#'TΦGb6$,u7nueӠVmB\܊WkJM-6b,@fGQClѢRkLj߶Ca8F;4HH+`պl?UȂUg4ߖ.@"!RָF6$G.ujK*: GeSɥDT.eezg;L`QgR{u/ m8*YgCC5[;81LߩҸ7g8øY?#U?J'L/wɋT"(_ X#k/Q'[OTI\9SU{yR/S׫QO?w90_ǸSS%9_F۳*s*>|%֬AQn3b4 k0򸆝;+vggn:)cX|)+%`Ov'ɟ5<ÿRy|YǗp7a(&NhjX{_%fL`]/BnO'V ?pʖ {gJBnK7?O82q~aۥ{M޼lް/&ۖ,廵+}nsOsoL=bkk-Cߛ~z'/T:XXq`028P)bzs+WYvaܷo\׬?`ǁ15M[/;<,txlޏr?!ώq6lL:/q_+LM"s 2?x.C=3X#qBχ]΍4rD˹[ٜQnB8߈g D o@TsMQr|"sGsq=#g9{{G<4Yy?qOCs-1=gwk#rޟ~g^3 Q̙Y)~  BȠ)J;W>xE6]\|gd+Ca%P֠*BWZo,(a+$?iP~PP,\ $ze(f%J !/{+KR~t>Uј ᫄ }Z. ", .J>+\+P(J] {gWYy,X)a&X _ۈyCepm]&DX )ސ,it.:x}&ޫH7>:Xɿ"3Cw!,*KbisJ֤urO _'؏{I[sTi #c!'H&GL4ک:>NKD:ΤOkhΧ%NC&kinowfn-~LAKғ^:dzBoC`( ' !xd("pX%0&$x0`T@% aa6́zX`>< K`)42X+x6[Gha7a|0|GS mp. 72W]ӦsMD3AG.CjT~A4\cMWIweF:-NUGs[3.EI_ +$>xAMkhy]6 B~U yl-6*qM#z ^Ÿ iO;x=z;8x;p7)"h͞'|z˽zYi)7kLú)&4]H }-a\0'US7'q)ݷ14ЯǍpa1-V"[JWDVb ͥ̀ *mdÀN-,  Rc#DO4鑠::53 D$!SůJ;VhSp qym<L‚vOQV;@".PzTY ~EYW#hCz|^q aO*XuqDQJt ld YV_ש%r7Y\ֻtZ}@l2fp0b}mQ%B>7ztJq9lb[$f@k!:#! 5D€ `˛LfةiDcDYe,ok`"+Uv3d>¥Dl]Dt!F5=[,ȯףv^WvTlR:DU5` ԚC܁k @(qKKQ:ڮ(x׿ڨyiGA4OZ@50vBvuKuD mptcHK71bJ(>3>͸;]ԋ#E0ڮ}aygPpLL2NH؇6h&'d_ŚN7[Jt6"lFWQHyqOpDrzӇE]EhХ< /A&eU}Q]ž%- ]ԸrtwDك²'E: v*{Z:ްe1b PviZ6GypcWZW+m\kHt/9P ZUW|t;wᨒ hTFteTtay7,E3 3=JLޫ4J tٔCP1ʗRKc5៲& =3Fbjmip>6ԑ##i< 9?t3d2.+q7WWIAꞯxt ri44g4 i<2 +<U"?GQvýoJ/}Ѡ<Lz*;kr!y!R#ա;bPb(1 :,*rX9")S"=دLw+D:Jc2r,^b("=L/&c$K-5{ע7% !bj_jdOŐV}ƒ=g"qyK<2㽬xO1;{caD5q k ,ݓp⩻h7Z]B'x'^;:`}c.Qُv_MvT@q Q`< r<c.a0Pbx2 4A-45Ƃ1ڍ ,`()2D=7)Il?(fu'Gz:]j6iH3l bEalYXoAm"s0VJ.IPyumti 󪮏iw(c@тKǀZ\DK.'QۥxʏuѲK} hԼ""=EYTDE'A }DSIT9TQDzag('!ʟZ4tDbg\z4sQy(^ӛ&M i^3yxEragpޫP ϊY-Ww*ϑƦxgP%<䱺x/(AOӐs9\#l E<ao*Uom"%h_'Ixw1\b!ĢF ؾ5$Qd<_(*t=a4`1(K\XylR9۸k8˹clr s{|s1|/N3Ρ{t1|2z0asJ>k3Ϯ2Ω]1^)0;?i.'6|˿kOs58S0j1/|8^hOp)nw y O9 ;#0|"Wf?b+/ ?P~oXMs}kN|wGm4 ȍuGh_~ۑtǿa}/i]}Ɔ/{OG?|G4 8;޻}ߛ᯽ޛ,з?K/Ek>xt/s҆'>~7\| }OJC-}+(w^cw'Z/LӬa7tវ>'Wy}}]缾.sR;iÜtsg?3ٻ#!^gal|vm|}\q2א)nĂ*l}m7[}IFX2B7m4exglk֣0}YKx fEb}Cs@X)gr0=W\J`ݧ|9\$g*|d3Lfr^(&Rta[?xiq1T!,gkȲJ}OϗT-Sv!W:ɔS;7/DqI$5)}_H3?Ǿc ƎȎ/9_SvXv0+=CCۚNs~˩{֒[rJǫR]veNr륪 ⍶_f^⡼n-6/Fb-sJ~Qaij4mZמ1-?r/0o ~Qnݨjnˢ@^jzq]/9z/\G/{K"f[-0-o!Qh̹ ΅j݊'@ܙ]sm$htu'ѻF &ȝ`vK7-Ih9rnneȎߞ on8mCa ly]jMt͆t< gHdY?;DE;uo6+fLSI("6ieX1ht.Lf9{U"vH䂩ìmYdӧh̦9k]0 Vcffъn'D2XbU7잔cjR)2fL_M}rHLt|7h) [Ɗ9e,1e+յ>; Qh4SuML.$' [>)^Kw$ ؆b<3vl1&!63 ^N<'0;4f]ad [=nF&49N$p]QS_c#ט9"cLY{fNHȇV=hXBt:M$BbIw)`fv%fVfFm_e#W (i")x/f(#΄ < )@u)3y^ē7~'6w8O9EBn2UhNY#aƳp!8nOiIAc?s̈dȘczxdظDr!m+"'-FA) t= “A2,M(5slg?$v(bdå]Gƨ1>vfN9Y)X^i:fOuPYy'g3MmQn"f j1":!3֎Ev}u}'ҨA 1nﰑYDe+SZx IzQ@z|=0JD4Nv8OoFzNDQ*I ZD Bi&@SN;T{m-ɣ?MjG!m\:~ܱO{RPRy ?KMc ?F׆h0h'Fw E*ʤxw+fF0{J G> [vܶ,GQSx3ȇ0\\[}ְXցov2e_w 0 j`Y0վbN\h,ߛF(G L[q1M<%r7h_ fE;2K>ݕYX(h k"}NƵ'1pE~2]AB|vnhȋ. Qs4y/<ľ`>+k•U>RAIv8hr: +Mq!|9p$ 6$`W9X~sD|uj'bh3L0ܮd=C􌨠O-4 zZԩ9F׻ӄQ/pJd&eS0iZPkӸ L@jHNei?w o 6Sw;R|ާ.(5&Vl0)~zOWoeA@a |PMdAE3o D~מ)g)vr,[ОSǴ,& ҳ|^Km~9K[ B/ ;TX_qeɗjqX8/ ZY&.U[.mlKZ!WOӽw d ]ܻ#V rmQH$e"=|"ODjY o+)qϋ`}G1;I"-ɇh$DfTNLv?)aDL D/cڅ.YUՏjkymYP3ڛ'[ lyև 9KPqeF*ԷWt`)zG"BZ@ ] yL79f+(W W\\ik U|w^ "IyxMLyC~冼Kn ۡr\!B_IVBVqaotZ|/ ҽͮ-ױH^{>LHۓF(V!i絷!@Gٚ湫@;w?+`7?5pVnPRwK|m*Aw5^F0vT$crt \@l- 1k!m}*ºbGMO`O;XY2)St.]yw>΍O9iiH .]S *}]VfwN=78 8> JR_4g+|M>|i:L#i:#"`ufɚsPh[|Iޒ&Sd գ({ץ:Zݎk rZ);9 k?VyxUJu{T4tr$@2Yh D(, tcgGE?PGeD|"kCX!  NDeDsnC%|}ԩs9V\h*,<,bQd"Dh]Rl .T̃Ņq1J D)cI32؆:EdD, *Zl;̣E_ H*h6gYB bI8k m k͑rHb6$+Y$KuW(*WH0E͒lY&gF `9>p3iu\@V ƋE $h[3 $ ^&VC0|E]zzeBECxT®q炕Ud`ѩ9F9QsGG45C9Ԁ:x̲L`C"MOH"joDb@, };<&,`'ʖiw&莾Ep[Frx = z%.Q~Gsxo]Ea?VEdDn;:Xjт)$:$&4 t0[蠖 ΖHB8ax/߀Ep5=h3=Р1=`3ۀ0PG] 33ƑYlj`kۂc>;X- /̇ꝙŠG3ҕٻ2;ջ >?^ W+l=Փ@N8@Rbҵ]AnQG4H <6|i]^0Yƶsk2b7C)=y٠Uf ufMfAנg7p/ 2;U3 =A>a>Y`gL_bK[lm+ 2?۹=ڇՆ1{ Y>4sLg7ς%TG92ţNq: ۃ<0S>hw`vAc/3Z?X~>y1{ uwfowq0O+#`_s:&f_UpYǹװ+}߾]Ƹۗ1;Zf"%kDHkS֊y=q4)]˫bwzEc*_?}F;&uŌOz]!ޟ'_ ݥoo7?s?i e'>q66?GjAG'N/d!&Ya{U A Wk,-kˈM٘ȟyʓ/t:vf//H TSYet?mLn9sJ^0Wiǒ]nD?> ue`yū {-~v/}ƥ|JhE oޖ0aj׋-Oqƀlw~{o;qTR_wș[?Kw{vz/D#mG-&-yKz-mhs!՚Sq䑏V|7`FbYSf_R>vQNlk#W^dG.RD}_NzfYAM۷{ =z<zi[u}qchSّ?/nν>]{O*> 2țK{l8sFo[8._h5;~dOla3̪b>&fjsl on5y5Ψq.he:}Oc:}}L(Nqd 3X./tOy Әbo,x~d3<ۅN1̟s{ekaLO`:Lr~%%OٌB9! ʊ=H\JJKΨ]OpkBv[P+'ۗX.t46ϕ5Qc8obvI|`)ig{J^O15 j}\ މ8_M v%+]Wsts+{YEٓ<pf_`_lO^S:ԓU\[Puqᓲ}g|qVv }Kc}}X'4Ej\\pX[#F1T:ײX|_N1V=0-}y*We1'?q [w]p7Ľp^Ux5ބfox{q>c87 )|/53o|$H "LBII;҅D$t#qN$!OiDJY2$H.y!)d*FfțdM^!$2K^$ R ;I #I=i q9AN3YF*!6{> >akZ\J< ǃPஸ#nPKiR[ "rebar/ebin/rebar_compiler_mib.beamXp߽m lr!] ! , AZrۄ#w]6*ZE2 bQZF%3JG*N;SѱuZKm߷gjg:&~{k(jP권(I657j2OZ8uDU%#e&H*+}*kHRoVJ̈́+"Ecr,L,v5IO_0S4/gQI`$/ZC q6Ot8Ng4/H " DHqJb![N60D4Ɉ͘ *)YJ4. a0qXyr [J3WA NYIg59 76U(kozx!d i%Țjnm$Wp_pBCeWM3[qJSSƶ0/ ifp 薘r ~NՌޤ%lhB(brBa9T%ZNkQfyoʨƲ$U="6MfRd]6 ep(1O`zaӉHм9?חU| >&lOyWVKDk6` 0-T? F>H6->#X7u2T5CJV˯1Ϧ7@lv@^c5_͈*"ZZᩥjL(t/G}@Ҝ-L8]pp'p"c%&QxΈ.:;u $ m${gk_$2\1nz}HEe/=#>ÎvNk `o''#v`NEޮt Oe؀6%䑊Hq <ݥ=EdKvs;wLG aZ=%ӕuoS@6^d/|Cԑvs骟x Pb0TC_ C_,!47eg$[GƴL/vhC%&/C aBASh3$O7R0E>n\:rRPJP<RgK0XYӈgy)o^@qe'3:qP0:Gxl4,9g蒜.)p6 ڈieh2NG35G:; FSa?;9QGeKH]wf:7Ө̖R4*,y9$P2NqfAUqi6]wfCt]P88Tl46d/; ptTaCUYvJ3[ %6Ke(P(x̫Eȣh5)lq 5B`k(0%l˅˅BWyfv&XY❋1oġ2F3A8EqTC ^MA M !Q@0|`Y,t>jXsV 88Y8 p43iTQ+b26PNvX(ZárM)i C! t@Y"BYjFYڐ²&`욁hPG \–&) m S NȅB*"FJ5[NVΘTk5.f@ؼ ᬬ0ݠ4Z- Vmc %vz:&$q ᵞaFSq4av;fp *HMGbT@`r`ӂ9T?CAv) 88Z&vEG`\Na9]B~,O`GفhqͲ|P%df-'M2r˞ȧZӁZPfX3g[rbߐA+Y<_b Cydd_gɫ:Ϧ+_7n-"u4~:-<'FlVIH@֞H%[ď31KmWY)IZ֘wX|ZNkl7e8Lf+WD-޲i޵mRt:W[g+؁kW_a8yܓw<:rb|q@]u/˜K|g;ʞXs\5qڄίi7~y=]Q%ڲOos ۇ 9y'^_N=8CN#_ iqO?==߿馛^:+fϊ _=xe_gߋۋ]yɽ.7ۯ8̜ۢGX={t]wR}__>ZYxXGw9ڮ{:]pw_k꫶-xЭ?g/[?vL:~a\bcآ}Q>NҶO̝7Z1*׾4?ҳ_5<}Yr檉u>mZ*Ԗ'9サk3ϟuq춎sOSw_VRƀ\@~!z_3{pKzqVZf-1g۬^_kr/ͷU@%?xWN¶LFqM 2Z7pl<49fǩy7EjV3b_2&UCgaQ%#B$Bo ro_{A)B&5)Ț ȍ!ܛ'bx)˽q%F)j8lQ.Nt EXXU[s|8D2Z$*(*)]T֢9?C\&19eb1(kXP7OC~؋?_5Zb3֛ΐ{xU\axX=N; r\iXx3qz܈xn"n8;2+Zw r|1^2> oiY|-;xwAVp(oPKiRZskP "rebar/ebin/rebar_compiler_xrl.beamV]lX'=IMLnۤlڂ4Z6h8ĉǶfABUQy@ HO@O}AQ)h ZΝqayAesws9>N Tz IBHEՈّ Zډ< I:.; -_Fd| [뗃4Zw30q!ŕRx%ewtY, c;X ,?`JcmoG/6=fW[izq<04tlyz3:  {Xhu'Sژu:'%l&χD̳*Ӧ k̨bĤdmRXtws]+{ZƢBn{1Sxlߛ5۱o5=;b0)g:@ 4:FhKQ.™!QJy]k4h/i"̗ A=*#[O٧͚fVՂ`VqJT7ŚaURͮC@lC |^5 Zꚰª)bM['S>j tQK(:䝭B1e%<}4/_nH*f9-n9ig{Y bK_ Y,їuNY嶻\ՅXvE4 {i{d"ЌT9 Rڡ Έ*,hш~ D% }Hcws~[  :V*M)CѲ/ZՌT%zgJ9K@2/wyhb;HR΃C@=64FCLBf6Ye{ Y32;OcΡ9e6VI`?:Ს^5gdP 5q|+S^:H 5sxP2HPj<Q!jJ9* ۋ:nFcsgT4I2nf0O*N:V\I~i.B| ]ғ.z/H9z5r g8G@ Gr.Ň''A|tc]\vڵ; puc x\uZ%ӓrsL|w= yqL^rs;_˥ zc?^˓8=Xj_koO~|nGZW-̃g/|+O>|fw76ύ#|>~ѿ_X|;o^/߾?߿O_ +yOyMOvxW]BD-/O'o<1pKsSx6nF0k nmym\̗I0`@̼mtX=J}EkA]o#?zNs::[s} 5ok89FX=o郁dʼnN"zRّϿ,;cF\5xG9X( (o#cfnD1`Z96_%[ha-N%0'; PKiR&D}| "rebar/ebin/rebar_compiler_yrl.beamV]l3wY{׿ 9;a]5^۵W@u:a𵤷cM CzNrX {8pr6X.K)r-{ZMNx%&\8Kq;piG1D,wEHrţ(zZ WyOڑxV+>,4BQv7|;8[[tMSkwmVOrՈ7}=6U&u)I]o]];:շ VLrOyԹ. .;dI bY\C85UB<3hFjx8ej&CFJf:/$k0F1+SV- QfXa%6Ub0JA4MVj0fkx(u/SZ`2R3$sI玴9q5swkƘlț3Da5Sfx<A2U(Wҫ&RyΨrE j, 14F5`\4tIh1)Ocj^P |OªT\׌)]mwJUsn܏}΍Mh_!T+jdII@gE%I+vD(ô7HZ v 25Rf*-PPUK6^3AU0.*itXJq!iG q4{LEWj\7 ;=j[+HD(WP͛faL8>p[&iLpTs{t|Ov|ƀ `XD'T}4!A.֏щ$:)}X$@;^@>9d (e`x<؞qEwdexpY>b!˳kN3eyf|}.bj=zez%<1//=}OoΛ?[/+s7''mH_/}ҳ M|߹Q'y_7V.B&3dd&vO TDzuŵtV98SP{~rm{2U~=5T Բ%(텚ӢL7W}8jLeOPo2csg&r"(Y)1RqX^*gAuG՜YmX덥tƴttDOG3:՜Y 3ϴSx,̤]}j6GU-K1u.b9fֲ?cXՒYM.JzTSS1m "L${3`,$,%B;f5ph/,M-Bƪ^` ܚKX[ag]i!3\,qv$V ]Ct´Ng@2p-[+:;NɴpiꈪLItJO2ƢY⮞e 5CUUMSn8LY{9;KmQx Ni-/ۯeF UϊLCz.L! (n3ܗܸ3?-:ǵPHS3#jԚI K`=:R1 hM$}z^o&HqDh,PE~ `qVẌ7+uԔ:0@= UZ0I C8TI57S&E;CbúA\)a`R9ߧLJ$ r E3 ?8\38GqE ޝ QBS"o(.ڍvGqGJz(8z3j9AA^q~&OƒL7} x.APJУx+{#H/cs kf)2ӧ`GQ<JXKƾ5 }8W37(qS8JW^Ƹ\(@E.nxZ2R^,S<bF`x Sƣƃx$03ENBW" q C Û=H(3o T\q0!'Ä"Ïs ]jt¸ vBfU`RIs5ϵCQ(t{zqys"s "BbH=~DODqB 4&b%,VZ>.TI7 _Mg? <+ TWb#ݯ+x˂\䴃@,y=@lk5snEmNYK]>\Ag vo 3Wxn0D>, T UCO8 rd~o`\[^d!,0av%$i^Yе.uNX)wɌ"@'`̱qx'(o4fFՁa x:˃M@r HANԗxyvFM:QpWtt@tb5^v]]$p#²G!+0 "'*+oVf7u4ZƝp5ss+o^ﵭ9aG(8 k72Jb.Be…e>B$K""IK -"T0j,dʬ0I&8El.+z b!Ww U7b7pՏ@o^û5U4 + ͫh-Ջ#x5"T$Md+@ [F:-WK8kP+l&ϵuZ6mx/ۡ_T2^hc䤴srw#<)cN"pX]*-eww𰭣PIOW Y#B,Zx>  @S ' ¤( v; M~ !2e/q'\|~|{L9Abbِ7_g=d2(  Ý!+w F\r;_֯ppW?1E\VT]`rW.]Ubx&rlzZD4=Ƶ6LqtArJ w(Xcfm7P+0u46-Z XZGu梐ǴU4_<#@(vEݭ0R p@1FX\ p#2[&8Axh6R?VP\jtWHۏ8&FO^g\~@l2pFlVOrՖ Ν\GD3h4`p# Ndf43jä`h  dKЩxova׮ܮ]Yn#I&=K$=8~Nh$ s)`}֭5hM8]#}Va&h3]Ʒ1i^N[XK{}o{KFe8Lh[AV0UxidBu8ÜB4 +eȣ.RqL1R#{rF!x Ac yN RD,YDm:w ,2r ,N{ d)c~nP *p6} R悀fh?.l:Ј" )8iNff!fO~8w6y'a2?[ pg萅4nGf;vz7޳pOzaCDQ8 ˏB5%@ +*^86nh  kPAny}KOz4d,^ᙎIȮ'l 'P݋rc  DD~`ol8KƲ=&z_l]0+m7lW鉮5=m@'n {oP$]70Ūi.EbN=d@m>-165|bc=neːO%yr2(X:lp"k쵻u6`3][<ݧݥϼX/z~h~|Oɾ?i{ԯWnG>|D/NTeϭx zڑ5r#ߜy{6_r=߷g=ywn{ʅ+҉-*d6K6Jp>'_;*m|RnKnpZm}=_ﶾP?4myW|wC:{t3Iϝ_V-?U5ZOKg6GqO_8yh;{[?>=1oNӳ9Ñi_~d%#|^Jr~xi=guCSCke7ṏgh {OWs }?Ͽa( Hœ/s%sJ^\eYoIog @s1qضkf5xn8@:ߑ=M&tVRIO,I }錦Fwij)H18F ^Xlb3Dʕ"ꄁ#?3M$?3i鴪Սf:ZoNuCZ&QJX:,JMOgPlap(R(U]':Db5St&qz6 vZ<xj8P+3i.jk}f̰W(_C_&{VR$-=zdZ\vŬ]ήdClņ^gSΎGأ}IC#cS?/ٗf2>?//ٯ?K$䖊Ri$HJj%$-),ڤ&C*mҠ4*e!t}GiPKiRLnJ\rebar/ebin/rebar_core.beamWl׉kr컐pF%ɺ^qLC@cλFimҲl=@ڭcM]餩+T`]Ticُlt{_L̄4 KO}==@Q{}c\EyMm*i&RelVssI=JZIkd1WXUFYdAzrNɔVaY_4Re%nnagӚiMkDF!1zZm%fD.;U1̹dauŷ@ I6U(s4a`7iV}2LTnF`Ֆ1%@5RpU\Rvuα 8ѪCZO\6]1@*\ֲt+({SɴAL%`Hԓl^f+t՘ɬՕJJ˘YMO.WbNKd_?R^$b5`Q 4gs.QܬFiݼ-zDJkO(Zqt2ѩe]m-Z@C EzRE\s2"NQpLSQQ96,4 0JTH`B#!LzEvmn`hf #W#ԄR &P oKM#(qm-шS<֬H%„VL2h fֹH3[Tg'"Vi5 %x]92Į1:FG0-BͶ̺RBi@WXi l<"e!JY]NRNRK,Xd"" \I}Ftx^Ty*7\Vo'Sec%@{;mC1-F/ xA["T#F&RaVJRIܢC\'-nc|&_:SR0CIf"NR JvFxQts4q\.޻06nN$AʕfMĎqv lD9*@wt%]}pSq&YצCLG6c@_ւL!2 v`gp Bu..W^[n #j`!{pɲr֫&ց g]`V;.Wmwb|cKMiKU~ΚvU@ֽ=@xprqegM~} ݎ~c9$>_^g]d{@xK-Dܓ?zku-ǯD!. \;oO?wȼ3W&=_?r}m?߽P_,u[{/?/__wɆ/qBSB;T~Եƫ';'{ T^}s[y;?\yo"zr_YCٜ;ˏ3s~?擋 ~Kǯ_l^2W\={綾MUq3ׅy iSU>A &\w=#}4-:G87׎ ]!UiS˔Dy\gfqozT%aUˣeoz`mkM;V.4VeU/*V#g7 {zhZ#q\BcrVhLVΕJ57V*[Vna`WŕJaB?lk.W AvvZi[ЉZR&<6h+mp7rrXhm+eۅq+רB 7+*Շ\q"XRd~xP͍JΊ5[jJp*VAQVU)Nxf5ruBZ-;ٿ3g׬bPsS%.Nl]ԯQFqP/" iqӶV9H[T !#"lXvhQDJrww*&'/R1ZZl͚U/0u?X}Vcٲ1>Y]5+w d3#|{#,dd!h'Au'".=ujҴQJ \̍@Mzu<+$PxHuѬWZ ؜tn\H2=:Z;bM`rj8g=KR*l*LZ桻9kzWA.@AP`tBٶtժ6{;oB@؀& '/%+BYyRx',&\B0 J)2ȗhZub Do4>Q!`۪ꅆGw(7@Tu c.k}WJGa{Z'QItr =h%;0d^=áP]aMS7<6$v֟ %O#o,]hT6 U%iq#;yW&Jkt)ٕѤ$zJk3S 5[hPvo%ܞg99Z1ZC0}::_rA/1 ./ %TA/yNM iYMBqF/}I(I)m89`lZv{+ }wK !mEo mK2VNn0JiMz9٠m=ޘщ/ ( + l;xqe$';[q|l\j((z"J˧gypM yE/5Ę4pܡ3T3\µp!wCtA 0|gť=*au\eb y|VK aE%tG2/yx)͇y!>yb t杖z!w/{[ vx,~k[=G?b8D;/j,LX|c:CO+ O~5ns;">-,?p0cznfns= Y" >#Ԗ%tf,S}7lX{$]N j[0Mo'L iz}Aaz]~&:0]K8^=Lw > S." VDŽq~fPQ|ғ(b, M+0ۑ + pGIUȿ%t%+ =fpFk,lR]]mɮ--Y,&;'vб;R$tA8öfK3g8 I6 $K8p |h y:Qo$t+oGIy3}hEZY_Z--Zs> Zy_Z7>ghӉBĭ[Z"ȊqY;@h.]ʳ%rB[] jq5PX=W 8( q8[QfK:,}~=L 5ܢ"+ь$~usT"ȧu?;X*0/o nB 8M9 CȆ1`9KxVlɌEi Aj+趃no}NPWaPd0#H/)E.voطW`¾̾`3t3Y=4ɚ|VY>j&ok1|)`2) з4FI(V{8Tkߞ1y0fa!IClgcqc^vlE;)IN)JjwxvE{@v%oJGobmu~뺯^s?{~Z3!s),g==#0ȥxAr9|Ds[M.wvt&2) @xR=GdB>t9zC&2DJepY B %CJ!EFCrH8#dO`t$2h*'s5se|+:Q`Dc`% O:BaD ؆ߏx9CG n\ ClD> jA^+z|jHRecjYY+VwZ$2-3={, &eMS:r)Pm볚!cZy!DUe3妽 D u3RcODm|՞?kHL_Qdd)1ӗ*͔隟&%wbm(ͭ;UtS?yꊏ^ݎ3FGv}N8XIu̹s:~^UdhMtd[Ķ6^hαf̣x5cgξ4s6rmk;^"oTf|sg{__}߶;/C£ה˝rZ68幱*yhwt+}Sm{x}&w֨iؽ I+ҽ"#;"k4~SvU1xxfaMXP=~7h QL _Ե >ߡx{SpV'w/5{Ln1{_١bóǯʾ2x2 $[ej {ūh׾r/k<~Z@pzJob3Zx7n2Z z>_gj4 'mVWmĎư0`B'm0N[+U-*=[ʽ~qפ3Ŝ!ah6r}o`ulŋuW%':.~o=u7hs:zgؖuݦ褥WȆљߓꅫ nL DJᴬWuktg^e>MeeUt{Ϟ4X\"Gx+e^RPz質O\ L<?߾>{ lI~Uc]ac˹soJSd> Hlڝe j:͹Vj;SromB9aիǑb"H8'kB^?Y_TpgYU^Ď5so;U5,]Dx<~=DIGq8ô[jo5Nc CeFiH_!(L0<,or1_bz߀Gz8YgX>>h0n2p]]ܠ_p3҉us-x|C|AL_PII9tq컳٭9!g>y FLx:67zk~$١ךق~5jږyzw; ?ϞԗT~I+UMfʓGx?njlgβA~2G ή{fpW)pT Шѩ_8}&#=#/'|V$G YVQ`yqbwNv6)-.d{|0fv9E9A}wOF$M6l"dI_Dol"̫sd,)RRs%s8y9c;`G/ESB,8K!4 x-8)8 Ӌ8{pc7Da{s1 bG{ 8cО/Β~L4qxd0Sr2>h%+#^@Un-f$Eơ|@sY07fr1B8°f9 ObP0)/P 2Bqr49 X !osذ%'D'h~Ԣl΍ q!KĖXab? +GGDo|X%90ÍQ%lXD,h/9aߋpoQ( &…EHqvff6f  0䇜e섴tċ7nƅv%?_|x5_cϷo T:t*`1 v`= 6g ̀ 7p8A.Q8 ZAP Cd@&AĄ\!7~BP(P%A0A P  JCP)tP#݀zYPKiR#rebar/ebin/rebar_digraph.beamWMl&=8tLg$VЖVdі[MInO-J"-+I6q[Arv/= -͡)(5mO 9ڥ91kTZ"o:*FWTŶRy];]A:Tyh@z @H:a! w!nx- ePy_-StE<ջ?n!NB$$_3%-Y>{:RGi}F ch!x{>?#Qy!i7`S|0k'=th I3#/C*56 _P" SS0y$RH_q d<..@Aᴨpy2=|(ca.c0*r0ʯPcc$XĚAMdLEhVx j=^Wy|1thAyDkI5q&j@3p6y\DB%g1 QT '^:{cM'rMJyV˃D2EwU<,#UaFD0(_PEہlv`N! @)> G8 3~ 2 0bf|aR~@c _? tS@ <|&!?)Mɋ RFK؜Lˉ/ ]+@/ߡu殳2$9s/o(l@#A+]b0kL3<Y&k,wLzL%]O䴾BB>pϰsT~ny7z[O_Ko.}R篝_mg}`}|rsٙs}4*T-۠z)uK9'LVfE&})~)O񣬇}_f=fx~gm*8 O[jg_}оE N}Ȑ M-Pq6VۉT vpҾ oM8z9W-nնJϵJ%y7Jcg]}ꃷ,NLxպN'7f ̆mqۦ紜&4[\Fz-ZveP7WŨ [!]?zˁr>]%%"4*&tV*J &H[U=)J{RSrCɓ5iMJ%iKHk PKiR [k;rebar/ebin/rebar_dir.beamY p}߽=;N8t`NH>2_B XN= &CILCI0N83u]IS3mfig2;vhM;thjMwLg￧Ӧ:ۘ/AJL$3cYvFt4(D6[U`ɜJH']~I& O)#W$D䅙O^\'MðH,a.=mmX\ME;楓y#MóB>^=/7B8ͻe[3LJ7+&9KfQ'l[)=m' Ǎg?#|!DҜ, c>c#?,sr3JH,E.qJYrY“B @#p=[(R7sIN0 xj~0gCzWX\֮Oc+QI*$OL4 ZZeTOf2QV-ôCf,_(|-&3KH抺ULqJR!Nː ӏFZpS48S@L:eSQ-lY[*ZgV0%&Lc&#aVi4xcO (ikI3ٴ-WXWÍ\+Rl.S2i?H!i~xwː5n~&ȯΘjM&Q\6?{Ze,&S5cdg>Q_Z^5xb>JsɢוV{g~?mZ?5-HV22 S?&{҈H he=rԋH 6J n@Sl 9g@xKnbkH æfvJZӑ0.fCl`i#p*l["4y\Ѥ[&EV`!*44jv35.BlF6a$فi|}/k Rd<\+$Nq*ȵa`{K9о޵nlw),L|PshPȝ 9V62^ڀg\_2FEaR`75{w*%-ǀC\K(s |b?}oÛf!CaX'C-PLhR%"DLE#(ƇAE"/Md@ԠT5-w06uxxM F4_7~0&٬Eb`TwdCYzE!K}\`![HvtV\m(YoT#6CT]ö Y%R8r;d' L!"]{C8*ξq?2^f\\Rl{9snB:h1dѭcyC{>T}CGV##I,9`9XM؇,(SoÁ8Y8dCBf(=%&r|Tp!I<-'cx KV`2cu(4Y*D9q9nFDQPOșXpk 쏉j4$/M")<JvGbx_U{BrifJ{^J/Jg^W)xIE]KCRd3ϙ fo0vs!:2G5HlvZdqt! g X{pߏxAO >9')dq q9^y~34,@n!SlzMC~hF:p V"N@ǭ9#*dHNz\N݃(iɃ8ug#bEc*m01}e8DrK~Xaw\qɻm"f",2K|0"XF\#"9羶l/Gʴ rwɟcf/87a`g}v*s h]a3n:g x@v`#? NEby>3>e L`rzB/a(rNĺ-Xﳒ~&N4ӿNN32z]zN3=e e2@cYs1[g;3XY-J]|<bYND^C&{pY,GGcXIN322e]9Z)a0z+&Oqud90X9Ű8kLtlt)ǽӌ?R3ջӬVs,Tg}V+8ӥ\귔cVa-z^uܷ968]jN3RV+ٝ?2o[fuDzRs8噊ni<S{V嗾QF`|.6v}}A:aF+=gstWM1@Uv2oz┩O@ o> C?% 1*ɚBo;,~0o:Kg{42lcl>#, <27f6LFf2I܃%μb?#=mɴ/MI+0<ٜtICgӐͧsŌ^Ҳ3iq8R?-ַۺ:{N f>6̡)fk[6.~z#LULIu >:@Gv:Jw==HPf5hڴH'Yz>J/wרC>ENӧ3Y-A_ߡߥЛ5:}1 }#MNA7_URjH%Va56FYmQWjV]v^uPRnuMݯPդ:Wj^5ԂZTOgԇՠZ(PKiR?QL rebar/ebin/rebar_dist_utils.beamVlWwsrk4?{횖jgבfa)-&[sZDPE:12`4&t !1j?_z}t=$BfgM{v!BhRv̥S0*WhznV!M04$˶l-{vvөeBxm *MW2` *劖]j$鵸6+:^2BѨgxf:)\5ˬ 4Śk/jeݕggp``6_˶S/zR gLZ)e 4Uñ5q28^-֚f8B#ʇzR"(4krK$mršHFf*VFùofBN觌Zd_{4NMĬֳX4u"M+* Qpz8L%6dA=ϫ2w_QWv_ܚT.Qh# >{[Re5AL,FJ˂ú/h"#(|,/pKrNU+[2nkae۩AxCzSR  iRL9H^=HpL9Q Hxe67C߄z5R7둔p(}Nce-8ܗ05sbSR *!)"pบ l2ɨ @6#/]KEz+AtAĉS/!uAkED4H}1 *|\@k N1!]LWA;%]BV28ݕT|'`U'R^ӻ} /$_Crq(!u]q](wA ).zx"5/Bu^" DWpSa_ރ8 }L9f*&eg14NH8"k 9gi07v 8nל84-!]`GnMtiwy,%_!]}VEm 7 hB) pMb79q4=mТ]mjjgߍE fhӧߟ] !aӳ{(ݧ?y_X*1M,W*6{ط^}\|\FJ2{ ]^xofEs+?z뵫 ?T@=n~"_r^H+o_??4o?]^7N&u~>Zq3ww_Ɂ#WN~zw_Ͽq|_)lϱ~{OK4762d<i⌧=UYMi?tzݠu6y_xFpnO<^,<#'ӷ>N+zj"!2;) 3ǂkF~4JjzTnSfVe[Xp梔jZ ͩ5áH}~BgVg b4Ų2N1MD-pjkr?cM< Y$,H^)'~*H!#zi*9 <~dĴjpB]˜ \@'){LS+Nu=vnvTܫѿݘr\y嶩={Āʷu7y;77];5:O|26mϻ'g|{ߪ}|‹=fN믥{fi{~V{T}lnߧ ܳjw[_-j.I\kXʛVWo7~ 51Gk?8p:vI}?}Ф'f t{g stݘLƄysn$j0:A;jW>dq/ć?7G|_?/%okuH PKiRQ\0W#rebar/ebin/rebar_erlc_compiler.beamWsVz'H< æ8VS&%ǖd:'= ` fw[on:tt= 2}t/~{^vo;)J[P9_~ne2ե+7Dv&35H] Ziw]e ̺-QvMoth2od;t`"E^+F兑Fxڽ k6[V3̆`Հ0H F6}*:ںnV9S[LxY GɴPn[Iw3 ,mtײ7adE.DC!CͣkZ_lhy9{`Zp88f׊6,% zx "/:xn$!eao],n lq[$NkpO8#\"F4rGxkBPFnˋƺ6GV+tǐfZSxg!|amk6zh뇈j{t"HiZeEHhAf 3M]WڲPČJAh:^$]qxo[[ ]mZ4ݜ쵬@!$J49,M8r7`@GN jRdnӛnv)e:6MHt}7N:cO!= PAL% tA'U t5 SAR2 T瘽olԨRH>ӡH'"hlrN:VTȾ{wi9r2JgBu - :|s6ujھ$qjl:2I^sc\{PIL@]olJn xD:IpgJK3vXIN&@Ōvh>nQ!gH4a (jJA j gڱwa׺݋sیzݖ;! #y/R[iI=ND(5:̓w`?Ls"-}Af*-PVJm=5$mu:?. \R&hr%XeEQxd)pnEG֦mu٠{O##'"9¡jLkm7nl;浩9bo7SqvǷ혨Tܱ췐oZ0Bm;* Aya~:ɏ%ݙӐbtec&'S9h‰d'~}Z Z_"&B \{{;ChLwf&1.Ptf7!,7 >\N Њfwn/HB6E<>\ޫ\`Q+gP̠Tʇ{=>{.=:TZ7<+j;[K坚H. ctqΞ"(ig@wH`-C=pNĵ(]{:l:ɤ,0G<(5Ҿs<`;ϡ8Y~ĝ}cH\L6U˻l5eR_pJ FFÃ_HCg _c̳@8ȸ"m×l鍷ձpxx/N2 *iڴ/:!ϟ=! Op:byKR%6pbH|ZX-8!v WO rg!)D^A'agQ_qV=*NJVcZ(̪GM 5DUjp ]HzA=,_ϸ*gVZ߯h?,U5͚j qJ㉠F>!WbH'HP"QN;ݕ !:$tՠyF!1{pƑ X5  >|ڷp% x#V8OJ,F`}CQ7ifjłS Hߥ,g<$( uaX8[(tGqu)y.:q0"N#H="<yߜ&&.)*'aaNoV ho*?00jX{8Zӫav̮x1GMuVH:zadb.0.Ď2'b{ÐɷԒVqEWMĦ+6Y | O&>PHD\PsȐ?& RCj|*u_i ̢8u8[GTtS6o> 0`0:^3šS5?N?ވYdIf>\AFbϭ\*A_ d邰X:zYWH fXy']VbW;޻^E&SaVN!ZF $rZ`#Q`X"gaU`/y gHT^OTV%]G>W/ԺĮa~MW}7g᷎Mu #]'x{/iA]Rcސ/P߯H (]ϸZ꽭ex~ܧ6]H ?w.SS^?H J9@W @dnA&JFdr=?7`0EØmۨ [ܬXQoRnrXF9|,eaåT:xFYUN 7#'$6)CX-?@>ɯq8"2a5s0D)(5gVQ/hҘƎq2~gtۡ)R%_$_6mWƛڗsD }Nf.pY!hKb_`1x}w%U%_ (R څLv uA,EK;5y߈73f#6qk*dl,_9aH-xHGGF/Mt_UmY{6J WTG8K  SLI(k4%;!GdF;F> {CHGM=e/s qQ|튺QTS7D3HmCbi#r?9lj9 r tY |ls؉Ef'qttW /nVE!x!JԈAx\֞ _j_s=wFٳi݊=瘡}k]Lo2SOMHsSikJy;= m.4*> pj6!ƧAY«9N̅đ%^]Έ b۔vf"AࢰF5@j bk?MkfG+D =D$֔JB3;/ \P:V'r Z.*_B)r`4O1o%{%^/bI ZCXѴk)MR)քiE ֒B"F9IWX5C</9 *DP)jmE-udCԍ RjUigWiMVtL0bsi/F -5GjeSKBp$JIԕ}Ԕ%J xUJITZdEJ3%b6EE!2E&-RB=qi1絒p܌(/(p;VQSo>IWLZX_oGF [en5◦e `gno1^a&Y vV0`w/yN=Hyci$74Z^9ëΗI`ޗ NQ&b{$&T_\Rɣ:}!xu+/);i7⯒`賀Zʵ -LFU=ɷB- ?іO7Jܥu!9ٯ1$ X{ 6GoZ]vHnÑi %Mk!yXV D6H^OȬWȼ d1ۛ "2(Xc"7 tlЂ FwJ05`ļ$"^:T's v<n0o1cU͎.zCrƐ]M96vч巏$e3etS˜QT: 1,F+7ʄAxIW>}^4~54~W(Ex2ceF1༸i/ UG! D묽pJU*Q``A'ۊawnDf;&lo> ^]ߣCٌy3296 "`YQ,,`?|T,p'uG}= u@YMxȪ~*DzM~>6[chr0>0Mkr B*y^PI/WNkNUѺk=N&6zP5CC`8@}W fFr־' b:C9ɇWm|>F;jNy3]4w>PGҹ.p.cFNBѵ5ԑ/gtGS'J7?*Ńh\ jnQZ(A-;#Xߪ>%߉E pZ?Gk %aGK`?PU(DZxj(oUxi >(J:Xn D5v^OHI{!l|8{r)ipPZ@L76K+b1Y Ef`{LjNb0Z$"4]M:' "ܠJ)i v018l2jEJ`*:XۦHvؽZ+"nP) -,$]m0Zh =2QA]j@0L&hw< ɫ%T('|l_# >T#D NG+!w|d+%zGW@ozwc1((31d >_Xi%Bאs6 #ؔ+d4@>jH>2A.tR1ҷU nOQ{&TJ7} ݻ"&ܐ v99&!B$7Bav ]t( (=yXQXDGpBF ;9!S1t!^#c򏏗L1(K.w+;9᷋NΖ|6*݁i9u Z*r -J镔m_uyi!B*Sk91SlWtNjitx'T"g>nCu3s2 q0h4F 5:bO1xSQ5S%UP^џ@\&2R[tB:g(۷x DJ ")FlZU@ z|&J>BD])&O <@IATA)b^:. QWnuLO 3&e̘Q$KEX&0h#ۨ Zd*hF0 grA_tQts|RE?(mB륽h5h\XdCMh"d*WPlj6x¥0|X?ZMj`1%H}|"*A4G?)fH񴺵 MG@jJGidQBM/MwH6y.լ7Y1cj:=tq g|`уncz[ W֙!~%Y#ZϱK/ }b*CtmtQ%iTfr#ultp9ߍ 'Lн~cpJ'$D!(DqRGTtiцTu,,?: ΄7ɔƣEՆUaYB ?ûȊRUWr.FI̯pV/9AX~ܴ7 b'~34^BW!wP.TM/ : o%aE W\,tnK*:I~:KKIuƋkλd4e4md`_kEuC :&)!i[m+$l̹R8:LpmdLWEDEF(B,а^ R-(]V\#U\U^/0 *oK T.@ÎP,K *Xy\oJH*4XJc)):RFŘ uz[ͽEF$kel3ȅ0e6Fd "Y;}t|}=Y󜓙F.LsMgV.tmg*"$Ii~t&L̘0`bDV3il4űLpLN &-su8 rEܖI;/w`̤=On׏r1|!ߝ7s#`s}/F(u>CL>-@ !`|BaL„ᓿ|.%b5y̱$IA#`YqB z!x 解_$!w?Ag $W[ vdXG:bSa=1mMRyٯͩL ThI""^JܮԋyK+.iIS=Sܭ|8;[^}KlMmE,rri|hYn{ݹό>{Y5Be9kt٤{xCȨ갧ۛ5/u|ȕ/51eӔ7 + ]hC/%kOOv~` 3QԣgOgdm X|QrsZjA!lsq]9}FՑ?oNqF>K=%,pQR>vN\<9MըO'ު\6IAcW>ap^.:g J S?*Wi* M3յ{_2b}/{b#'-|XbgS/}GO*;9(~|v+2imz{ ((u+^{heE^\=տ=8;#}\QplYZW<ᄉ{pULt]7 s%$sma=;Xuotg}tmxݢ]}XLΘ{$xY4+]eUx<*!{}W`aoߍ:70bK܆Gs>#=D\bQ4FΨ kDx&V w)b9ɷgpCz=G3|g"}Cb{Ľ_GŽb>~n!sۏz!> ~A2y~V9~HO"N g_P">uyCw&|c3ٌc2C~ ጴ.bJi H; D%j*:q<9ptptgip ׏f&>9d9 ?1/M^!h`̥' RݝvE=KĜoϤ귕?%*DtJ*JIhdMDS1 ):eZ%NP4&V0^.8.hY51*ъϿYWl$˞]==>L^Q'H}eIg=[/D,kCKL =m+2D1DITd,'2I qDK4$$'d<ё$Id Yd!YNrJ[Gmd7'E 9D~&1 9ANӤk)% r$1"O3RM5yEޒw +hv`ZB+h m-C]O a  Q0 c! ! 02a>,%ra6@fAl} pN8 py nC9 }Oh}J_Yl![ d+^n.[ʭegEn#nr{] wrW[{}d?9@ Sy܌ޠӛt tNGH:FQ%M*Ki"Mh0mMi[ڎODA%<'\RA@!`PpaztAjmRF"J.d-YGidIPKiRVjrP rebar/ebin/rebar_fetch.beamVolG߽9{w|o|nrosvX>iY=vT!R!.@PRJ* @TAP[Vj **=RNz7o7o=S \,=;u IƲUï&-RS=Vt(wp=a[Gjh]un]:nZ寺F̩Ǯ+,^N͆JyYVH*κU֩$MTm]3-m2q[ihlWN֓q+zVV1\nj+fn-guFv]*8#jZ1mϨ4]Ctߨ76}8Q-]kLWmԔF_sLӖUz}u lB.X}6 STj^eɺ7vi-EǾN} L q, H,3.R VXUzI&4(! *2Ī 8E4{ IEkB#ED PΔaAVYqDbrϔ 48"+HG MG{yII#$|n@W#WHIBK =}bp#&e e!! "KO%&Py}̶0-E) A%R* RMg"mRd٢V3ۥT,1bDuل,HXSV@B ^R-ELo(͋TftAO82nc$=,{=3LB=+-1ib >PPv@RaA ~NO8L 'i>.9 > t˥Hx*%nڐ$tCT) 6͌,<ER磐,|H 3!(ʠ6a8H~95,D@c@j$gcR!Vx~,T@?+@$ =WDRaif!L3,?dP"<ı7B4{/x7`eB@qP7X TS~OD<C#@"@~h"SۢRH)\Ġ"N>R\9"|#z2'#|4♨S֫+w{ЦO1_8u X9sQyp5f6ԍ۴mo8n-~O 4-=p=Of(0u0C0Cx mÆ٥ހoשG}`Wıv%զ1kLfCsm^ 3skyneĭ!vj.ʑ[Bi7̓<9=9.8'՘C\c$ks˞O? s7g܇0{A<%gmx!24>?g1|ş xgY|cPKiRwG8+ rebar/ebin/rebar_file_utils.beam|YյxdZuΞII'J$؊#(-Ō8!,5Pa m e/ {BۚזҖ;#ہs޳|g瞫lo utU.]].-ifb(3l4lNxiQmfѬ*Q(T2(f^0f.Y.ʼnz ekmb,SH Ul:c+dⰌw T˖%P.Y'ꆊf>YN`5b"MsKla4Q4!`lwU.3'|rHdwdyNL9\5 jߒgM3Fcz$i&RJIY!3HkDVU%s%]6Ǎ9BQ 0 e[+|3J䘑SŲa[Y6 Uٶ-k uz@|0Tq3fq,KCf1/.eFXXCnMdstT9;=0=pL6f0\I&IUl&P> wdU&Y*YC?4 Mf1U$$;BQ5id %#=n0>S6c5:b#/J-YY#g!T,/0]5D 7)CC) xy])T4wNM{rU4!/.D9?4#Ha,gN4Jwxz~Jb$vb1Ly >؝3aogB6J墩~y*QNHUO2U1]̍= *T2ALU08@6!U#ryj (Ai];\$r0ʁ9 r_$Ǭ]"YK mbbox.i2Q5+_̌;Mg|ڦ]64^h Ջɩg.2v60[v7%4>4rP\yߊg?!l{u^,6j⺐_Dk(7bOp=xrRThL}wkK R} C8)[Vx%l~AȆ/?!]f@6խ~D$~ފ)"h  kaW,[c awKm59 \\bAQ&Cgmw+\Q[aEоVֹxGa Q 8VUBJMQzDy߁D%yP:!֋gHx/b􄡧-5BRל_k2&Bq#95DVnM6߄7! z&z'-Cr`kA0>cVrV`:h۲y-#H1B̯yuAAcfJo{) D&AojFx >puIk`plF12:& ϐf$ްg u?u7wK@hL\ErZxK:%.| Sb .8Nn[ϔILj /OaD"S~~&Nݍ? ֍u`=obr] )t._3]#wCc)9618~5u.X]{$z@z|}Ķի<'<K,.V;`:1SU8^?CT[ =ϒ-lت&Cg zUCʐ Ho;{5l G/or?pT򋱼]@xKs!ssnPw$v.i"f#VŶhqG}ih;a|m"=PO%I"K4ۥ)/S -C$mtj>[bRJ>DOo >/v$F/z̧?RIs<.4/K*p.Qճ6dr9ps)'5Av^D2݈\_ WT^!v\.)ܪ-[Am8Ӣ lP* ;G9! ظw@j@'{/BYN]`+`7bsWHjHKđ.q|_FbR*F:/41I2NNf~Ep+M+oBUb*$l$Ԓ)%a(;S }ʱzF)3ea3d3D {2ǐXb>' ӱRFNר(CKx-pD(B<:8J=J6ůX4F: ~U  ׂ*u:Ůocrtɲl(]]ɨNu5/ME}N^gsIͥh `S+RIEx9gдX8.jIkB|\bei8ejpmI ?ВCǀvTML~j]3!bh=ҳDFg4)~MNb~w)6~5EYNwf@ }ve e'Qb{-6Axr`X+|GCgsqøIb0Q7Ժf]9W`x ɉh2#VxZ:]W뱻h#쁮`^?ףnQsGd^lڰs&e=C#B\!Cg_-45A>tz6w]o袗n,v#vxQ;gOF߰Ma@N/v_RcA?U=,׀ZbwF~9BRo퍛޸w jnp+/ ݊˽*S#XlXztWbqPylXA<ۜvn9C<EẕV?(;zҚiXV{3n,{ȊA ̓Q7c{Dm/u?..xa@{<\E=j-Ev}@% )eڏ@;` ÍBؙYN$j$B_e:vʹǁ!ONڸ߲]p,p7q[n*1?MQ~>>2ly }E@&w( /VZ#LƝRE#Ķoٖn{[-(X' 8xLɏ(j1uhh^kPAM.~h;%h'=-D}OyR`yièzJ<-D4@IO33ɏJmxԞ4U:焪**,vHL*"RCjm[-#{ {߁JAS+>m.:) ? 5Q򃙿|@B'!TPZuJ?bc?؏AIXƭb/Vn_νEPm@9ӏ\b#BGLVчjrɿ`~)~*8b-=K?ڟKgR^.}. FD/ ȏbLӂ UH|Vb{3-:$SyƜG"K ߷C Ej>;~r`qqz]j=ǚ"ڬNzoPJ} 77@@Rzr!ӮG7wNN=) ":Efh_` v= k;m1CZOXl 1:/0CtZ?OkZ)~2!*'G*^X $ rMh3JDzW._+C8S"Ϥ3cb@`M\ʩꮴKλԙ!9)\ߡh9=v !IP_ΏAF >ftϡs,ϔcxn u<2KxjxqSO3Ǚ&\g~>E3;x9~<zW8xV9c1UkPTGDBD=I`AfxD@ҙb0f.0pEy+ڭhDR#q5QQDHJip%>˦x?V}u;Owq qcT D蔿aO"^ey4,Dbb]>_+ıXM` ̍<&t^ ֐<4"-/a:iMKeceK`bS֜#[\\d`%<,)˙b<+e{2Ts3|t*)O('&\IPƼOl G`|x A>XBx=RCje/gcsH? 鴷}`2i0ݓqwD@N` cM,nT_ќTY$ 9ۊT.JOXy)aܱVvG g|4;{ꖥ^_a>yq_uES[|n -Hrq2Шu=`̓/4{NQb wzq~ߦ ~L1'i}ğү zַnv$NW wNV]xs;wrzm"W]yu':SG>x-Yo6|^VWm ޛqwnS膋 JS?f4FmuCF~ri'Vh7??Os_KQmۚV{A=T.M#t:mNkn|JGWoV7toysC߼SԞ_4t?vǙBmoݟR\Std-ߺ!&J*Z[mkq[?J*K(im@ԜU:Ͱ)/[E<r9"Hg7dg]ޟkY/a3* (aA1me|3_3Ns$\vNV-;O>x`,A@ GkmTu1sc %PU IiOzgYN .b6Rq\]3: (Eb9@槸<{ u Q cN"ZHU=WNAEU]D>(}Y/*%kDVY)A7 =/bExIc(fFl Qe|S0H&yE"H42h<_qh|,4YŸ+DʹE /dw&ʩ/35d58ċ6 $mt<3lG(PJF Q&2 +!ّh%*EehBmBu}Ԁ>@;N G :vԁNiԆ.t ]Bt]GOL >GC&! a DB DC,&hA R-C ;adB! Pe ʡ*OjalC3|a|gpZ 68'+hN p.98C\pnx aWv{ao<OJ"<_Xp uuXSp2#b4.2"32=VsPKiRvX&"rebar/ebin/rebar_git_resource.beamX tՙhnw41u5ۦ볡D" H2__4ˏăH_@<9JE#t6 jl?DS5%`<U3񴚲SLzWByd4ݎ~/3 œ`<TxҚbBj*@Y\ST0U.ӡd(T4>H P%3j~T l) ѭD(3(A'ˆ5L,BVQ2O-'3PpIFR!uT2t*#3CTIC?3 FJlXUc `c^v8C ^rCQ E~+M(:,R8,~JlxюT,2 % ґx6jl-ל28:8VZTh8MӅdd0jj ; D4tw(J g:JY8P&|_&'t%UVmiĩt%h l !OJ٢h(DC}BOxKʇ q@(RAp\S1 t"6ypU&oCJ9kFB /+\,hM. JB]l(I}"%L3X2Ck6< @S^Tk->)Rna]*KqJ:p*l8΀4 M aipw6S4䀽U"xe _vV ]a7WU0 a]"S^ E-]>?`CKWR*"WVs 儝{Ir%{5Tw芫!Tͤ*fNJxD!D픕K:A6锛s0.;̘lP#rb^#afiL @.rpW+N켰+*k ʁ@IAv&ಂTXt*AF,GY\yt Bb$вߡIXOJ@SijIf =rf(ρ7 lj-x ,-:1֫8 yP 5;3(3\!e2܅T4ȈRZNEvn`$z]XW/\=ru %i3ztIB7l$zBEFc MТ|`F 5H+rRY%n1!{\.W< A.Z .=3u>P]%q0\VrK /D%R8CNzx4͵НZ /k _#teځ:ef@D@dtV T hqd?8U]sԋr~@"Fra鐖$t}:$?2!9R 4O2P[A.\/bQC+#|p 06)z/ 4 жwP]t J? j[>B\nYnn*ZY(wjP١oF`\J°lY>@04˜lضtH6+o -dC USvL1`s2mMv_rkt%]3)oܣ x͓r7^;Sޖppd^w2u\7`:~WGd;x {J0$B |Β6A=8٩5xuH©\?U)Dr] 0ݖ#@:l`* k 0 AC1&-[dv555L/`;dХYECݹ샴 a;K個,j8MPxm^=^)7nV[r-8 R*wrݐDnXg'.%zR6{&#)!Ip`#sP)Ot[;̾a8$nIf}a9DYC# FHn1]| l5%*ߊ/pqtq(t+^k K;(V2XˌO' YqT ubaKAc4;̛4t?xp).O"l[fV.wfC^,y4BKG! "DmҕNnW '><(x1j2F+Pq}n(3xM.Ra㳌qZcC6ư؋9v=ɠc/MPd>^2hXs0whR[ ]cn͑붜9z6~ =#{$glk ~Cmc zjc Րq^dU+6#kb5XͲ6 By  \}uu.4Zu}sA)__|'Gy 詻[u?q.;yy;?kM>h6J?V]WGџ}bymsv;TW~_>2zhOncήަ~tЫ{|VDGpҟ\ˏG?>6==o?=P'Wu?Tvӟ>nX~G5F>C|7:]OiGh> [{uhL?Y궞Mbmoy3G{#_/nZU19wpf?>/}_i;^Rt]"n3ᄑ/MG.7̤We_"E^^WsOq4u92tnO ׌{?+sC{׽l/碲s'~_|Wmf_-}a_^uW^GIR㴻g0{.pg/?%>whwGۇ_|{ck]/V4.gj>{PW⫴҂mCh}T,`dK.l| "〶S(Zm*T*TJ#VE:Vn ;vg~s~9{؟ϖ'95JǢn膱G\Ҫ6 ,iSj|C9{ԣ₇_Ę5W맢uŊ^֯[Ў _7`d7=6 0 Z NԔ<ٻgrȗ iy!A*T\N-~z6|9Y` z+h8> ci /h=}\a`*iZV+K˩/%)\'$ZF>%әkXSyNڧ V8NO+5zV2*jh -p,Ӻ6zf5ߛmN'(2L ,TL_7si6Ɍ^CLxJ&Izf.屬ߑ06WL8VۨO^ ŻWa<<0R`2030@|9 !C8"OK ,4HHFV`%V#됉/-؃؇؅#0 JP8rT$=~iA*q83jP4W\4ZqWq„Kh︌?p qЎct PĖ "d8A3q!năH'D|F,H'$#a$'$D( C4$DGRJN2H*"PKiR'XP )rebar/ebin/rebar_git_subdir_resource.beamVhwd˧oǖsd[١W[qHc:仓Ζ}I'vʲ4,KE[LGi73aиle݆ [GҺ`tߧ >z}'wbjzbGGueHQ}^NjU:cT;'!S_ YKL*.5Nj%+\1`eT1"[/bƘe٩um[v(t̘뚣V+Z]/D!԰kR`rN%o;Z\ЋKV媦 S4Pj!yZ6 e/ݺNTλnZ}Me',%E0#dQtB ŠDKȘ Dk٧ÆC6u|׮VѰn|ꗨ~ҪeAYK)ӂ'{%ZQV$)A&$QVQiٓCDY( hDGx Qb.^CBb I8p6"$`aBJBaI$yx"לf43óٌ4}[*I )"*L\7wsgūj M F4F Lyc$$.oA7ގ{T28ăgRZD _C#H"JDZ8s%A'$DJ2Ez[Ps+TALy'`#*b& 6Lmio#*tmW7Kw#&$zʍɓ)/ "y2Ʉx۬ThV&p7-&8+R4+zm&Բ<| ԇoiL8Dmzl;s4vo]4cA<3 JbEqt| 'DR#dmad DpSXrPM)x2$GXPVkH7S/XPOnCX5".ɔ9xaH.ʜCI2$^F{D2v$gI q5|:2bD#5!7t@g]}CH(-yh `'>D@7`;cK|S39q)4| km8"R%}ڙv_{V~::{|xgk _[bzbhfLgS?~ڇo</k/Vft.gWzxqz~\oO^\O_kj^<ebGy܏_ծ~-ۙ7 =+_M< @>XJ3AH  M Dr!Q M={PE"I_L>9mzR4%Ks=ov*@|o1 >q &S%s|8ˬ9]XZ,,s1S,eKaRM\:59}~#l&KEߜnfYUf)3g\fƼt2Ͳ5Qaa>2vJ=ixoI/3Ŭ @D h9]|%BTuU]YZgfE-4ggxq4sf> 9@1_Ζt("Ӳ4 P=eGg+[y:kDXI5U6|Î8.Ap}^7g9\e5q?5r7B-̜z9 ͙l L 7uGW-j05Tʔ@ll%=_ggLhiP3^ծ<̊ -YZI5liW,(IS3.]sjRhy-s3u{iSbg\T:2lTN ջW$͝h$BւXX˖^-4Btͧ3wV'`_,-kB@^IUq%Ӻ$uVgcgx*--?u6I:3K]gfrQk|php`.a3q\0ȹ'mE[ ʼ\!"jUTSRQ̖ g+>QO![ UThO!"gF(4qb–̫5VU@1䳗 <8)^n8訌7Ѱ,2)^{Ȩ,iDW7Ӓ}- *ZQ$d_$5("{5 O<|5AI~yD Ճfߪ #"9dߞ n[u[UMH"RUqT8i/K{iFP|-7mD r|jpdc1 _S#(q'~FP|]e* 2ɑݱqdLhz֬8FUR"q %NBvX"(hP/ )C Ѡ4$I!IFBH_ MhrZ7U0mt̋/VA6ax3Āmb*2̄|lm27mȮ&omyEhFOA}B L0IBsX8 R-MZADCDa7;!wG$mAm:w)x<"R 0;P)8~ZE`gjD Dm oZS*};VV s`l&`O`lJB[* ۡζdR.>5 ;hܡWK=Un8ɮQkw:z6Zw /{=(=#Q:gsJNwQj9dV,A96V3ԉ :H zX4"1zBMa'h*zդ͝+K:Y&mcyJN聀QUPn\|L@=wxt7QKƻ!-UÈea8q`ؿְ1w-X$3hOF1z+=nv/n 膍@vxmtʻҫFbHIߒP(vU79m-a@^tÙӽ kB`gs vF67%f২;gC-<;c 8{>WQNN=$X:7C\YkrYLjhtiUӚ^#Lƒps)zhEcN$`UTDjUBfP#᳆0xh٠rL~bymq&?qL>UzڑO%:P+#[|_#;fG}gVhnn 7s߸/3"6O8t/; /o7Wo]ozk}_v=7_G ~݋YzA4[~:qM}륟=\_|Cz]q׫G>5|i9^wfzqė'XMn\7{lߗrӿ+Vzq٧..u^Ƕ|7Y8صG6!œM:#?_~tFk?ܵBW{oמ>s'O4p,m==~ ֳ룷w~OnocsF1νo ۣicFGmTb<}63̡Td#y\UC]zM +3ǭO 5aZZ梥9To,De0'yR$NPKiRS8!rebar/ebin/rebar_hg_resource.beamXpי5kla1lŰ!MT-, q= JmCISIK4H2pI(Z&Uy{{~e5Ͻ~OR4L5 ',aI}8=\?!4`I'Jf|sp-^\VbraWFz<1#2^1&RNC-.a)1nO_@m0((_l_aRֈn"V̈́0+5Q+(_u邞w@}*;7XQl M n`t{kg *ژ{%ͲxPo[h0eDvζЅFTaD mAЃ0.2Z5<-߆[7^MPPɖSQ\<@Aχ`ew|{>L]F}rRKahD%QC DSX^x0VIˈ&g F)h2LXҢ"\È NsHAM>B53Ūk<=8u|Aut5Ӻ.e{EWteQI<' \aDVr"2ĝt *I`aaF.;NAf' jav#L7Lj)IM~Cą1 faN0=G 2@&VW MRE6CG6=ٌ1Ȝff 0"5Y]ҐUEV"s\j ̭"s n0UՎE6.j5dja˭;A}cUZ]݄UzL`UTVFv܁aSfV7\ܱZBkFk:$&)uBI;`vAyJZ|2ppd1`D ܺ8F^ sbIFNPiŬn/}.@7N?h qeM@\V ރJ9,\7虭=Nz6 T2 -.45bN"h͍S`M-ɪųe^vb'8`m׈jlE$n-!p1B݌ Ek&Agu} }kznl4GA`z؝:ZDM耐f<\Tn$܎ȫ\{=Ľ>;މ.$KH@`5@@ " aɕcCQ= o;zPE4 VMd"n9: țysy?U4"]`&5Un1.\ilުM`J6+>U* [\ XWf,}E"Z&5|5*y@y^;(͚n|B|.y7IEjy‹!x~ #8ȫ<]_U6oXF&2q4 ]Wq27;wndڏV#^O鋸 SS\\ erP?yZ|. BHN^(Ē>XZy&[,V=v<28|3|xc?&MxSE)8F5tk3Pz !,$ڀi.Vj0dL9%TN!y#|s$b15B-M'R6W ƃ2J&Ø&-S L:x2؈k^ J+XAkCk$ Yo- ĨHvݞA&@&L9)z ġAي iNmHfY FS 2'æNiim<}5?c]^'z302d[3"}!ѯw}2CQR㚬zK69 z߬GAӆa֬=[ћkKlΟYƏ.+ o<ʟ[nܷaq7\1r?Ͻ:|`C-y|Z~3_Y;w0J흻v\͡=/eOS'fa4|n[~ϼMqռxkꙛ먉'NV?ҭ\CE{v_>ޱ^o5|' ^8}޷_#W ޞ\/86l$..im+{m ]מˏ^m|jGoUro=~MctCyS%#1]_e=}Swk]ugY3EW3Rg+6͟]l,[߿o{G{_ȩf|ewO9WlۿݼݜC`[EWABKa[ArAǭ8x%1,b ^8{qUx5nXI 8x#ބ -x3~?mA?O'Si]YnK|+~Q|_5|/PKiR*! rebar/ebin/rebar_hooks.beamW eu7ln}wIw%}&M76޻KBUH*h6OmB?(J"~#( i*ھY*x߼|{7 ª&;Y3%Av4[tΚ3ͅdSR1kmHrTt1C׊XY9wͪ\jxgJZ(M6eX#|,:׼h%)*̊2n>-3Yy)C͠ "ē/ Y0QMYhlл$ʶ fE%(Cc4(D(U9+'2HEP"4ٺWiHnwԫi)`F5b4 if@"tLDhPHJ,SYCdJRJMXFT Qt'B.Gv9#WNhA(3wSL!xf<1%3锘mɦn 2greT iYqI#rʾJ#4/kbKsqDJ <#Fh F&=;SCDڅy$pZT"Z!JHa6>V @Vϰҁ ۼQ dէZ۽qA(R̾ڸgc4o` giqW=dDY :P !YbW>b(0ZqW}B/.2ȟqfXnn@.Wx-i4:]QIAc0TV-V)]RSz")MR|)OFUY%3qJfRU$TQҷ>xW`,@5(l,ch1СWR |Rۨo@if17p%(vDA4: (kǕ#D" \AhrDo#l^ 0 JJpVn#xN!܎er|nt[3 mYo*^MEeAH<{TҶiRW;ե5NkZ> M NhgCl.h+T ֊GօIUr R7Y$D!uM!v> t/vYmI^tij$2JD}U2jcHP!>V "d ɸSZ+8@t*qm`j{5A"qD㪈7JChHUi7.4e\&hTă Yۺ'c{wk6uڕKGzӷ? 8p>SwFқ>^`Jϯ?+p퇟~r4Cv/O~gnxW7OWfO<^ڍK]ͯ[ɟ}'Fv=oíϜ9Ge>_X;{>|=dn;酋cJÜnk[a9ݼV%fmUR&bY:/4طSO=xٛLr8G1wcɉ *π96bZ^ٶ1 wa,K0 a5acX.esZCX)zyZe>BA ;reN+%-Pry r< Wj#0zyU-_b9_e+[.ˊS4\{Ո>_5VX"2^q5jm=2Qo (C1"$~ 6N:H'&kHYK֑^>2HN6(mdl';N#q2IvKFrdI)} PKiR-#rebar/ebin/rebar_httpc_adapter.beamUOhUoI۝mb Cg-촻d!;D]{ivvgLvvf?CA$" (KXV =x* A[)(ɬMA{3ߴx Bw#ow!t\ Qu4 z;H0u#v"u۱xÊ&v|:G!f,cVx^l;;dnLl鑧;vM>csV]jY0㡯oZi!9f s1p3cmw;䩹^3s;M2wV~5؏Dh3-rmAO4 $r$!҉6'h:*T)%UhȂ(ѲDyV+D՘bR%!MZvSk9jF+-/:.b=i my,71(jG us9u4:TPmSktjTJFI'TIkXdѤ8JT5}sϕU9 T⡱d[ *+d'TVSwen;ɕƉ,|4+2i@K-@VIiKTwxrKN)x;JW b~v^{up{0Fa^mQ@?<%IAK}.N\pX l ɞ@;~G姞3sbRzJe\%oieq1C@Hq.'d8(T32ǘմSt/eݵ-f MAjfjP`wKcb"=7d@82`MohUuz tF*cؽ}j 3ьo\q7a[:0d98j =׹6B&aadQ`-knzZx ?Eݾ(ʹ犒^p:~|}L:no Ѕ Tw9^{~]p gv>EqV<&(xJoPKiR| rebar/ebin/rebar_log.beamV]lw{cfwlc1& 85z3klgfmLF%IQ$VQPm**RI*5O}@H@;;&VZ#|ss>/!rgfl)pW>F{e]o;*zϩȧYl뤗̠l:k wìZi$\ x'1ro8'- )\/e /Wt6Y)WV ;:x\3mhi~`UjSV e[~৫&ӳug)Q(g ˥Ojr˶T]LV-DasR hJh`31:/ޢ뛩nfs#01ѼۖpƫW$tl\^Hٷ.@_1ˋS~ȦEѽdŭmͫQ2oޠmOP5j\!,XVB)A*!vDf38T œ72\!{3X9I |!jiIՁ<%=K)B]o9{8ք36y6BB&Cr\XK(­V0dH1m}/K YFӢ$elL.YF·$pe2MWfIUM 5a 0M1,[DS$dJrD.܂$ B삘^ɇwOxFՂP y6<)mmXu}#$2m@vkbIW-;G&(^HԽ[$E'dȗgs $VKxəü%'G!Aνos2|7$Vch5lQ4 BB3]q.|Q)MܭdpNb]pl : ۸R)w9푻 ( ||TC=yG!p&s6NJ؇j2 *\|)RH}(7a I'D<0,8}9.`>`C L?0EVn`  B7Ğ- sC07wk fzm.ED aFB|FgE9G;$0$}IEm6C&c!9aB XxAT8%X D&+H^DMBCg%"&+`ԸxB.*×Sdqa9j-EIqlTVToQSe#=S BBJS &3Q5Uw'F1QKN*ʞFΰ <{M(D;Ϭ,kKQ `l'خu Xw̫ކ9@Ƹ<Αxxݖk,`mǶ'f/Fl**yo(\@9s1ܡ7Ƕݱq|9_y+ şV`{^ksG"k'NwБW&{W/ig~t ԡеOFw gO~ vW|']Y4V:qm +ZNNH/nӿ~uą 'q0Q䏹ڭ ‰&&Y3czu[)F᭡XlX\']4,c-5})W2UL9@ԃ#\u-Zwxz4P2-g/~Xm'{Or*v0>Q Q@CWPzlPubj)>>BE?xN0}v*G y%eU5 MjOt^LC7UFD_? }Iߢ?{PKiR«rebar/ebin/rebar_opts.beamY[lyP9=z}{,K-z$Y/,[&P9iQ$wHlr&b 4@@@оm m F>}SE;gű 8?GcUEEŜT5m٭G |=T,"6*Vsmzsgŭȕ#mb p۹T,]/[pbyovŽr%ľ)Wng+ nmR*6 lzD>kDU8@\%` Iaܺ_ue7XIX.kт[ <ƭ`hz7ڨ/ o!ܡb9ݎu͛x^>nZ-.ߣ[E9^+ Ab7نUz(|e IEōUXG^m[ʕjfWE۸'MRTV%+ֲ9/M-tm?[a1V+^DxC}H/}v(!/nxK[8,Y;d^B:h[^**i}OWͦ4hl4W~W}~m !x>mt`A6-}W/%^ڕJUMثcfb:hci3̐62b5]R S6luC#RcE}KHCJ6n mщ ;g4?lWafA0kzvwcD ndg-'0?5i̴1FO(SHg,ia1 7͌Ա3oRu6eEg1sOF5-6]=[m3nuu֥N?+lB':J-A:CiI׋D4gZ@n'llm脃\v$ftB<YB -hK9 mB[>yMO]~$,^0U8ͺpNtgNdO&R =fW.>|:`>8Z+=D,q~Sfj:1Lif du ^iIDu”-l8P;ۦNnzcԦ;0õ'flJIllEϕ킍粄иq׋wAcI:hKh]6r>ak]ğ^ D~w=%%>b=> קRI.&kOzeu(:>p%A Ƙa vAo&-7#If: N##| m6뇎k`iv(zĸev+htikZЧt2L Jz&ݴ'@Ur$yME(it&.\Mh$ _/DIJ7')`FEaޒ6vJԎi{X*~19'HUI;avNҳ&[@odJ,?"Gb c>QQz,XP^>yiGdu&&<9*qtl>9zmpqҦa~{!mAQI&!V);y;m68ئٱ{C%l4!\|#`,`b63Q8FP sd,v%d q(rO$$&)G|Li؎dve<3IW/4_)R:d2JZi0̝f `؅nfg6+D<GQB؆ Ű<ʼ sN%32eZcӉI+*Z*Nixp ͆;MqL L\&. abN%e&HZnsoD 3*fy|29a+sy'Sq0 e˖=-| yL6k&AQg"LDy֦MZBON$I^kog&䣄&$''-X~A2@5%J NOμc /YS\u 3=:iSI £ؼo;]$/(6iou1a*Y fN`&7򮶈ڢahu-9nz 3*ZΌzx}b:bN\^nkǔJi/d8pͰ%,zvCҩixtk δ&8|t-Ö"E"Shw9)wi*)KFqB$du(VP_vߘ'ǒ}o{w]>D. DK^@3lʡyyYOp%٥+\WWN*NMyP%@N.ͺf㫗Sp>M F݄ͦTl xs]R@ ;M;@κ~ [5gfػ|7h/{J  s2.6ZC݀ۃeךkpL H7okqC3[ZMwro1eA~\Bw%Ͱ`}I}d7AJ4^CCGp_w#)P*z??fk5Vkx?H΀&h iH34E{ӫU< _ňHȧ6`twQs+oal&1CIH}!|@< ?8KtXCU.;yP*FcTJCt3[BqJHc4WL46I1(<@o˜KJĿ)M|/K${JP/MĿ&k1j3RmFUg$~C jxOK)i,'%OJLwd\%w/,,,.,ſW~_ҿ][ AITs5/04R6 }.-"i1BĎAZkhzbLkWJƛ6N;inIjѽt=@:MOӳJߦyZPh6-z~Kߣߠ{'?BOпC>?NoC{{Kw'}Dt ?PKiR^ rebar/ebin/rebar_otp_app.beamXypy-](J" jIXB)Q$uZrAj%vuqKfb'J&m#KvIJq̴S5N:ȮIcKnq:hڙ9GbdD'5`f`l,p\4qܴZ97K\$8 Bj YG,ȳ;fBL*vޚ-V>Vu܅\9k +s٪F} R` [Hr(bJlQ[\8uE )frcGyV?+EC'<\TGr*VVEa^,択u+B[Z1hh^ŵ#清+Zs<+|-"T]N{eǵ5]zįF@١D+ٞ'f3]곳[W>:'ȹTe(A @b+m^#h1501ZQ+êw 6vPSݴ[OrET"SQڴpMWq|١}ST5d(fUy/`̭s*=s8kAk۬X!jx=  alT, g sӹ|ΝzpbƣϺ44eQظ ۻ(hkjHV6@16cs[qzG'h, z;B'vCN81g̅?>5/G*T@ICA s 0YNunDiDեU 4jTȏH:%?$\/ZhUOO"NIYò*"Yh09*^`uL:2x6e?H.0GHG9ArELZȿU0_$^U+1]QKPxҿ"m, J2Ri .J"K Q~Y#ڀ447I1%CiAojԿUTGHbD5RBo$t 2qnWl`GDY( jPZo!K 020k5jH{DMb3iAEOx`\q3GFt0[x4@E'8ZAu| Ż>nҌa ͔BZޅ7*aa*jhp;@]0f `zಥA @JNIkȊ`F4<  : m^iz]8딴NBwح>]0=4tԪ74ɓI lԡ`CV= Fwn}B#>dܸA"d&7#5t%LZ m>N^ppq :^Їj7C|q0*h (/C;||,)",P@KņBY |hX>W؃c@%M5 q 4WfA5υn^i+\.%*uwcQVM(FDHd'a  " NۈDQ8{ Ji HEBl(J{|<YAֽ48* q_x Is_A+*1/2Y?Q|< Bϙy"zUWl'QφGxׄ9鷛k 8Կ ѢK&U1Ke2< TPeU *(`؇5Sݘ{!*1afupJCxwA>ݐ:}(tޘjm@z]31@+lQ%ͱ6p5%C `/x}$ sdB)9AC *I@:Ѧh4؈Zxo+p R1r émvkWuo "{G:`sj'OT:I𐃤qЂ4fbnGt.hN?0w/Ex3@ HѹW/ZWdrk@-Lh˃2@OldTRZ\.Sm6uZKɷm祝1y?6&ynw5|h?\Swё0Ct3~h&AhqӜfjz?PSigNQp]l^ ӟq6^+]9.|b>&G ex硷;>{p|k}b/ix64qu}꺵_೗M|_|hߕo=̢K>*2/>Du=/xW_x}WS_۱w$<6w~ܵ/~Jίye# o)Iя=dž_lSStZN囶|}~x߽ǯ֗_ˍ73Ƕ,yKs+bMoo?gt~ v϶=duC̹_h5kAwߺ{Hӫ?,Ǎ:3J1,;P65`sc|/x: CN&יD;P7S5XY֨>=7Tg#/cӕB1Qr|G0;跄A$"&!ɟɗE,D#/7UrDEMy|yN ?#?'lPKiR 0Z,+rebar/ebin/rebar_packages.beamXwGvP[ƼihqDbC²Pdm dIQK&Hy3dw2ɐxٝ$39r8g~se>!RwWݺ޺#oHxpxj)Ij!IťU{,[MWc SZڮVۦ S(M4V t0,vk^+ l2a:}2[[ZX}U,O(B07t<[(t\*ٹtXLSC}VB)oTE Ni;j-̷4UP.9!Okٱݕ+Z>T*Xdu<[tl$jZ>+9Չ6Z̖&4iCBη5^*eLws f %Ǯֺl>-Ӿz\lnz\coKz% +R-llvzQZkP՞êBU-Wjv\"PJDtc.}|.>7! * ֤ҒBxXs&\(rR@dR1[MZR9mٕ4iV&j6RDMAk V'V9-U[K݁RY`E[%7UQ1KzgxYXGTB^6\r|&+f뎽w#+zwX~˕m\zJ1շM.A_i5ۨ\vPQs\5=|]+Ty(OAkTztz0v۔sXhv^x9oKR84}j-4RQAE#]v6[t8hH2k4*( hSt*LW]'iJ2b6)!irMt1LH3&_aԁKK!_ǵKK5hJAӈ^^f7}QhVrP3x,a("4T%,}ekWYsV7u)Ϡf;g`\JEfS7Zn`6s6g $'25H &4ֹ0${3ء߆$ͽj4;Z`5%%;:ȺwU7B$3% STfm@˰ _i¯]XTWv)T4ՂiL'n)֢ȤS#ڈe&bDH&IڹM(5!,)]CO%P%Z@I%}}F&] n`f[vdY(aK),x[\͈oUc%t0%`ЄĆ􎞎XǐKb?V>E7I]&1\aE<-wI}{KAɍLV$%{! jR,?@ bjML,4D7 N t@.Y0_jhP nYIZеJW & 45N ߣu.YоɺjDWB%ÿ^\ .YmLPuLh3c:=DF^K1^S$EgB&ۇ].;9⧞ʂ#~9ە:W=CUk@;쒝v/nG4L.مF+e3j$*ӆ xڍ 5`(wKAK&.+؉1C*w.*= )v a8pY?@o'zeGXď$K, ~dʌ(Yu~UˠE!94aòxln@QUc< 9c) ^+чn- #`poFv5vPKЄc c.ys#gzEEǕ6F?.9'PA2Q~Mq)t*y;s)W0CL"!cqqڳpb*z<#G#fCёhp͠c:n ^d9z>8@mp3>,F, ShW?&[-:д\dqᚔԢ$7aA ؙ]c%Q|2B3JpDk5Ǣ=d1^Eec" @kqDscI2~z\ A~t~q?PU7OWe5k.@ܧ`O]ů`tOϽ|l%'r.4}vVeh z'ͳ 6#g ^鬒0gg3TP'9`t@-xt#5!*kt${ߡ‰D,gL Nk;91%/?y KxE?mr6]iw~__ikW!+L4ŗ&8>&%aXN70a>lB? |ozs /P5d2,pc&h>5/Y_G/eǭ+ +f(m_+| D t.oxqÑ/.yp+EWqrK^^͛ȊJYq芖t )+$a g+&/ߐ-zp-ҳj-3= >פ =iw#> Br.VSbaCT(=-}^\&:}9X859`Nu % 1,H)$jQ\= iT-Oy;ътц11? |?0|,XsztcK|{tӟ&(F̿o;0ﹽ{ta 7h}>t{?X4UAxa([#Pj2~"8s8p>Q STsq'Eit *JrHwh cPI{Vg?wZ><}_w(ʎ(fqJj0"߀XIla1bXHQzfFvRw_^}k̊c3Ib 蒳1~3 cYIfRocDid/ZDWo-wg `bt07^k!$hd5d| 3S)uRH}!Y;ɼ~2S=)7ace?ag|dl|lV׺cNY\MEL.Bdnom/7UnGϼ %O<(qYCQ+Iўy{ r~ $n;rTuD(56qi䐢pZQxIp2zucm@/,l ]꺓=1Թ/l5rZ^LDž}>"(vN^e0r;)U3hP]33ev/=S9_4] 2} ŧt]Ñ6IbZU_CWw֗*ϩcUuOy8䭐w=*oN8140{_v.eV:YSFN o[5{P]S߶kJٰ3w/ t֙ 'ƟDVO뾕,Щ';;jˊ՞>޼GhS[˷^u`a#%8yCy/^Tavrg$_ MM?H"1?'$<O|K1$IjxokLu UHtx$ݩ uxw޼W֐!|3̚KeEzqS*w.9UKBi+1v 3ZSLE2,G9ZON\X)9Ă(2y2m*=6V)cJ)qJV6r*9aE 9:\}^D*\"XP. H p#V'06&V&9 ~@"3ȓ)h%'%1ȘHH8D,rz))ȘJBU_^<)GSzm:ڽ~"h)LX|eHZaKs(-\|d JMSϿ`:̆9`0l#+7XbA  !>tYpaȆ<8  **\z~'<>0%(! i#]"CdfY-Ev} ЧHh$EQH.e:Q.:P!*FgQ9j Ԁb(mF# 9#K4\d# W6?SPKiRhÞrebar/ebin/rebar_parallel.beamVMlEM܌=v~t[jDI.4I$4zN׻m*UH8āCB\{T )Nn̬7ԨXzo̾K,.-\X61z ݭutW,joti&kQk-aQ{P5uoifklbxٔ{l-==azon][ z]ˏv}kŜxҵ qۺ0E}=oZ^\o8-Z:k}k<T<1c5 ׵܌6b>ܲm;ֵصw=:`ڠx /`rIR@eT@*UO<&^BR)Ry8BE5T1aZd UC*"P\cy0ZR#Dۈ4 HR+- $|h_"'KXw"UXI b:H)$%@`9 .|[}$t!]6fpwn7_#J< :Y~b!$50cWLJIDppóWt'L-1z|>Gq9T>ۡo6OD"Xglk_W#{g{y: LNYx fϼw?wygN*Uѿ+=͔˫_fPihw/>G8fr |e,3|!<~W>Fg+ ӻvۥk"xkaօMQ9cBnȵN7YN7LN76-]ۦnbK{C5-qh\ 6z;&{A՘^z?C0 mC^|]I i{5ѳ6[`~yns1vtľX¢%o6K} $ߚf˳sa=6i ,?f&Xhx>M FwM,#9-g<*S1|X.GI(k\s.PKiRrebar/ebin/rebar_paths.beamYilGvf9U#dDjkriICZG(DKeK!I8왝C6kk\ H 7`$A,#, ?_UאMZB (ͫWw֫΁'eï_)(JC(%%g2Sn\MP_ _0r.9Ri MN$3!5׹\W^RuQ3<2Y'&;D-f2N)Mawd!/-V1KgܬF'K4 nX 6)6>zzr}E2tLh  H0 HTu'3y8uNidaKSgܩzT*V̌S&x4 D+awzT=$$\Wrn9ukխfe/>h.ڕr x\ܝ+E'Ksey.ﲱ{Z޹NH/C΃ϓp]Kߗ-S5\U7PN+T EyER&_9;bGr^wA Ս-&ۢ,AZF5 Jmay͇_t+ N$W~ECW`U74m#!1m-4Cawk^"Pa4l7هHX7@SEHw-ضnw-di5+fTm"źPbS6݆b++#'x.FtS[ KI7#uo'7*CF}բGФunДbbvpb#++GZ ijXfflBMѾZmXvr#+)#0nkQ%נn<G(Z0f0Y#42#H>C䞰^@S?7ՏBbzd#^aA!Y_ Vv#&D[G6c*SQ-FcE%л bkL4S@NOu4{ @~t=o' ^h#X4b*z#D ˛uC%+~4"4ޯU8P7%T2VdkIb #M}Id$&2ҏtMmՉXGX'MepJ' L`|V }FĆC`ٖfZ7HB7:%* aXe?Rc:Y nҚPZg;8Sz Gf}X8'6H'u$Q#`|FH4 G:ۇ < $[,Ʊc{p7J6{L'HfqB5Q (~żş7IV]{ex1&}MPH帯qvDX-ٟ`ۓ{XJ`IZU.n'e[v UևeO7>뙗lƓ @w{keZRBdyzGz1>M4(!CC1x70n'7N1NKX-i:,<CQ׿bZ v@X 倩`w'`]muA-fؑphP0X²>ܓb:~M q'P"C&ARe^$ @؝b{jvd31"RDc{9^P=o ¾ maGaNX֘0&+%$NK,*6fb ~ pI1;l#Ǡ)6X1`n&=pMC;5\阯i-#*RNaQ?Q?z¯Z F0 ņu@0[UrIz1̾ҏ ttmL٣<v2j)NuJ'pgT zI(솩{*h(Jin);w! CX|!b/;/}8}8MF|܇4|0ك!<8YPq3}8pV%-6}Hgb'`xM9GXXeI5Rcvd!zplJ"'S*$f*mMcŲ\[hSp~:fDay|G#\%. Mxh(:WyNVɌ\O8G%T b}gaIBzbW+%PN8 G5_w*hWj pK*`E)wķlX՗քk,$N! /ZR wd(Q,*cQT#uAL1yRALI6ξg ;CB9}N󨈛 locվ7`U]^x{~xD%߷m7RXDyb?uN='/+*y&?9v)*s|Ө[) yUST0uMt!Pn9W: ,eo؛%$3^oy_`o;s_a߶;{Kһ 8ҷ{ [yP{}#}Sc>Z$DGP%[F+2gach a UD#"%o"L\Yph@~9Fi Wk%pcRo|ss3tw@) #% t+i#0Lk?  ?Qs{2b<9i1%xyq9 % R[ҊRI.JE^b(?&?U+}U+/Oa/&>3[μӦz.Uܭrl6~KkÓp|}'Nώwz%R&WbQ𡴅bHmE }Ř0&e,p$1ǹ#H26|s2".Q-#rtXyKcH2 r=S06Io.uRk06ɽ+7iX%iVHI9NGKW*%fUVt' 3\)!7\*_pWkU71.tJL͹StM+=θri!ȟk(;HZ@O+]K\&i%!×ON)qxsqT^c6괉6SFrkZI[&nt8@/҇e$gM3}B_[m} SgS_ҿ?K+ 7[5;?_пA_/ҧ (.-,c4C{>[o8PKiRC ("rebar/ebin/rebar_pkg_resource.beamX}pq;w : RE$"!R EG$Kڮ#y$pQSR\i:c>Gf8nfL[v>8i+Q<+J5ȪnfjIku(ȸYb߾}}F_AQu(ED҈f→ FJaTM5*\FM%MUSUr$7P1M̙J<~FALfʹiⅼbēSf'TUUðjN{& =ϛISHxJ&)=-*?؉4SU!0jzyQA4B. 6+uC2q8`sIUSb+g~LR5|:YӅb*XzZ(O THK(mhR0c""KrKTv#)([4H@Kƴx%I(Ā 7ĬK%[穛]Sօy QH1#bXcnI B =؏KpXI1E'eKڅhl|Q*)w c*(aj!!(Q$FC1Ou, cXšc9kH| {P={ `ct H$"*\ B>A%0d-u>Y)c38΀}sabS?xޠugEz PDg }!rN='&"5rnS]9?FLDER:*p@GKni! K^XsK^JC(;\Mȝz,oŬD nz1'/ASĒ)cl5 >(@﷡d}niDLeeK C9"DvyVE.H,z!!Hcl\LcCpЈܢ%߼Hu rz-$@LF0"Zl Gx0XS>TÁ24<8P(Q0ftfW[l-X܌i߀dw.pi bXOu[/_sIzOkx:(h G`4 o׆_Z]s_;T#R΍Zo<"X®Bva ~堎 un4ꄄl|MmQ/U[7$iX,GT$44D[`pHRmz#|# XP#A   0'Aԇ` t&C46l\\wSyn؍6pb$-6͆"|394[<4Bq ab=A<؅CXn@y䠴d1H41h;jo\̶bl$jAm bafdsɼ,l5 6aӲ=V*D&Eyrb732?@<öׅ\ߑV0< 49XFݫय़xySZ=;bboo,Hb-v `'hɕllt  ce;'`< U tZl;X`);!v6-|g)C lmR&'l#}d w;A r"!!C]'H`G˜,sQVf adP 61AsQ!; =M6'dSea{ MMٔKΔ3(vuv7]2bwAE^ ǠZȳ o R4o@ycpB.I{pb#09D*XFap#Rp"eRyL6 loQ0i6jטXK2}Ȣ UbEԸi^]}1frOu\2ltTTq@{rt@> Z~:W9sI%km( rV-κ@Ó 0Pwm-G;$uy\Kt3l09x]猏:<ĀJ&حsprx]ӦIr>L:|uq@F׎oz?[z}/|n|T~ox{76\z׏~'3nޑS8}$TOد/N~нW~M?~~7_@_ą3V7X7XK1]~Fնwu7|WK?WN^2pi=3oqտ=/ߗEؕ߼]8yz6;>{~eS8=ǩarzԩN=NcGN' vznSs;ffi;!yC~К@>7Oz_j4nBNTah*))>l(&Izf46 ï~[ϑFy"`ɧUcmֳJQ lK:DAͤsnϨEq2jiV\A׳95qK)sy#bYPTVVt,+v#R6Æٿ~ï#3C LMU5Q|n'5FN\kڹ\?&n⦹ w[n-rq/r/p/qpW׸poq?prq !_hm.. B-PKiRV rebar/ebin/rebar_plugins.beamXlw9暳ύӤM/q3u|6?qӤ]N6.gv Ơ肘t$&Ə! PULbC U1M}\KP-}u{>{n>r~<=vbjpǝ-c!geRT-󌑯0bq!ke@2 EZ5̳([-膕5W ˂YXT+ fQ/6,#T5٢j\+ 0!]`h KFѰҒG(WHs@T)औ[1*r3,Pl%W|:WҋF_41 xb cZG`0%(W3iPҳ@))_R ԍ`N׳\t%R+@}XCLڮ&+3@; `U+7tcD]բ$Y%qV(E'쪙u 1@^mU =*Y> \l \`NUQZncik@';`+VҸb,V3*{7'%}d _R(śky@q D# AZ_nPhOq48`'.Rb@< 4i* vcO"sK"uɨ6'kbOT:$]߷.BiD8CcB#Y\"1nZ?vm:8 ڠkMt9gա؏9npP`Kȸ,B$ĔNl2Р8M6O{>Q7~8kNsz 4%-bi!p: =D5Gݨ4I~lhAqx8߹V׍(p3U՜>`'2<.|$>4NmP_+p;ڤ:0'툘<ñS5rdʯ̓y 1N 4܈ki}C`t'M9F2l_q5Nm؝NNUOJa gF>p41f.C0ef-Gꑷyic0:X8tpt`<̝9fryF'^m3W=*;1w8z=tsVȏzzc0q>5tVa9Vf6Udxhbq׳,93~sw2O2zn>&acZןbz#̮.ϰ{33,GPŘ s\tU=of}R#־W?_goNwp/qRlk?~W^ aRؽ57>ֽ_Ż^;?2]/ԏurϏ߿_>KGWþO4ya_'vz{G|S܌\^ܽ˹/^z6wcL(mcř-m {9pcժq?TDi %{~5 q6JedYƢ¦EZ>^3g0٩",` 0.^AAAY278m V*Vi-'Wd~HK(\q?֒sgK  u~0Ksar' ׺ ^p+_G=B)_FPV /sCjoϡb֬ C.>1VH/ "}~^5 }^ߦOѧ3t:5"$UiM~I[o3qPKiR rebar/ebin/rebar_prv_alias.beamXl_z9i8IvRumdc{/λ-MciJ$б"XUE#:L`l߻;'4>~{sPThlK#=GQKEM3H#I%QL&e Z<2 JAEsjWd3fճ鬏X:NU9ɂ$V.?ɜޔg*89Ii*+LFScLZ5S>nD J.U+szUMɩ8oG2i0+S Qt:Yk@,i@W?:^HE-MhF|hj)EMɚ*sT14l>!%sa)y)7FFc $cLӛ֓E-Ťl2U5OZ9e^Mɖ4,ež9¨N0B3a{oV]PsYd6fu$Se`x;'`RX.A5s}7T$ pHfeX$\&\5~[+%0$t +(ɡ,.kˋ2@%22ċ? R 2\["^1ڏ#qw>rrJxna ][Cߓ)|pF}$E'_Sw"bD<^?,Ig\̍o>+o4>s?v܇~y'vP]<|޸c^Mw}XPKiR}մ 'rebar/ebin/rebar_prv_app_discovery.beamVlUvowCQZ`6^`(x][wڱP4(g$.M!.I0D11:0%&^{Ę%>x[YD[-5+YbɪR$*-k%ˬhjٍJTS$EY)&]3+е,nmFMBf_,ED878}9РZi5{rI.Hy՞uat;3U-s֬L;Z ᬤjC*:<0EKVQQ4Ϗ퍚)ZkQB-M̀QtMV-MlAlǦrȥMES)<4#g6"dg蔩v} tY .!O4 ]'YO8pǦ4ɳ$X"}̋94-?P\P1S3KWWَqXX1.0>MqB )(G{fP O!20Sނ0-4t zأO`G`L:5se c $& IVC@V a4f5,6$v ą(*%$C@"0z F>!MyO3;Ӵwf.i!yEY}]۪y^^=ȴ8!/G«}4 7m{n2ڽuēTr% K_~\߾}P{,;u=~:)v^VNz gmƆ6nGߠ~L#C尕,YŸ.Τ;:"0=-1Y+34vtrF@@c畔" !ኂ#obÒ dV% ?(Đ:|M,{XV@N!A w8Y6\BK C>#fK Q0 M9֝B|"8!"_#G![Qp#907G<ѼNs28vy樋9+s?s[w86}`bG X, hZѽd-pncQK09{S g⑹bEvus| `ur?%G 9#Ma"ϘK%X@8tEyXj9xI5)ȶ%)o7G^.@fF8gJԋ@7z8ř~8>i@8gIa Dxs ܣRaEIUB 8UeR>'nyxΉ`T&YmD6@T_@XRȏ-qř*T .np v9 zA?`R~ Jo1+8bCVjA S7*hgvW+8^b qE)k-s;<\,-^)4 u&-I2ai R†OIj5N 6w3SET= ΀0*LGLZp5vִ1-Dcyb GJA\ՁaP^sZYƙ6Y!݉dx!1OՐTppn&r@@H%NU bGNT(I$΄q9 ~OG7Y [R8Enhl`**r<]"we@[Ums>s%:~ujjmJ oj*_]\kM y-mm汎Dg-؉sHv[w_eNCi`yԶKPhyƭ#u/+޺tv~k󟆷}{a[GN>ޒͺ>ՇO?_/W?%xcZӱ~v?|c݋/U__^{Oyuwg ]{b_3;O6^~gٞ.->]<_[:s [F·_"ڻ[x/'΋Fy胥t5ziɿooC>a LԞ#69pߎ w_g6 TvrnƉ/|w~}zsy]Ur\P⧝ջWћgB'3Yمկy9RS].N/6k{-hvvy׾s_kp4 Ia5s~1մLXRLx\&,Fdn悪hzCA6Cz٘2)+,%fajXzt=JY&QwwJg)6i1)Lit15"Y|+i(9aFAn%+~UpL`q]NƔtn"Z=0@mM,R#k큤g@ɐ*! AOoAO <6 - D7%!AV}G>&h.~/a.ضb zq w$ؘMI,`ÞJ; J"~6.hdN-$`Ѕ`R+4Rtbu{ǞpyK'q3?$ +㣬p Aˆ!,I Z+`U`r0 JZ3||i:0wcf<8i x1"mdR03XҨ!M{ńՌ&Xy+CXVu]@ €{d;y 'c>y> )q,)sE Rl4o,:ց\ ڛ³i^,) =by4+'GةEȉoU_xqC7!>Y];G _ V"/6V[WyE:]u^{J/Kz'fN] \='_>=qqr8yzڸ7Cۧ j~USJtW32|??UuRJcM'#|ɝ`݀;coz}S7&5Ƶ/A;V.Ӟ/vVlY}1zwdM0AXӱDmg>Jຕ]R'4MFL^MI:h(=icHL__:lݍ :EL x( (j JHjbt#HϦDzK; 3!i8tjz&݀ot4i Fz :u-;$4"Qˀ)#~@24fX235dvQxəF$ayPH,)+Y/ߗt~GE첥KkoB#J=mZcԩj0,r J\-W=5p˸J n'rkuqOɠPKiR 7b rebar/ebin/rebar_prv_clean.beamWk]7wm'lٵ7}؛un!w_q 1dChg=]z=ff -HRKTP4vQH!TJ_TQKh{xܸ̽9;sftgǾyilrQK_OA\fhӊ)G2B`)a6 Miدjf=ެ;8/5TՂѬLnQmpQ٨10*R,VgeT4jeĜ,hҜ^(yXƋS ` |3 /T yhj~iNY$uucA2aFcAkc9A:$/av2O+ƣU@*\o-WJ958:oS_d~U~Ōg޲㺪 ,- Y&Rz]T2$lKBLBQf2 Cb!BbNxwS5gaٔͼTfhM!;-@x55CBr ɱ1DE?B-Sv+'ʲW`lbeSB}̛NHhc1{ih>pǘoE^C؉3ɑV棚/R3¶eC3T<Ƥ(%֒l$N׀^z̒5c œ|x0!X6KSS&2 IAlܲ+)_Fy=A_/RN"Odd^yIl0(0S?clPb0 /s< O[;lH:+`Y'q/@ ^zaO%K/˂C6m7؆emRLVjbH x7[x/l^:@m 0 ^ IiHRR*\9$Wx 8=6c6X@#0P|(A( DBl2ynN`$x.$ٸ `0E[Y-uU,la/m"^]EAgQ8"b1 E9!9""4a/`O0ڸ{7M tCE]>Ţ,9ȳ41KUqõ&̿w-}$ڇAx+H[Y].9QX]lk:n||wmz];torژ# _rymM-'<׃xXxOpp\5XpsQǍݱrgGyxP<1wFW/;ȱSx}G {uzO ۥwts]ע.=tD[ŏo CR۝z`P_A7@zB<0?d.tտ:c{ߛ:n_yek}~ޅK/Ulŏhóhx~OEYy.o/=Q׾]޺c"X?6hx7rK+d_|''Mo~yl$ܾ'g6_ӓ>{7>{C>.bn@Pwo'fcOz/|ԟwϿ֡ӧNfGI۟x;O7_D6d#^*QChDpg>7wq.W ~%FvyΪ]VBAҭAW= 9mAOZOmb~oɪN' :mT\ NPnUN2JHG^56`A=EEKF2p^52cj4dtLEgYN]w:L-սϫ{VѮJnydZ9Y^O>i8j B}nPqW"}WjeiWGZ>跭`_VڵBzˇ߬[sl +!5$ZVYV%$I ;[]o97ɢ^ZJJѵ5ѵ$(20Xzs`8x8Q@4~ʮ vg|  l7) W ȶ{]­ٕfIR2Rh#6elM7pdEPV [6xNoVR#d,"\}VzvU'HUN;D RRvI8BWw88Bh0..b0WRf!Rלu~E슞`*rLZW@T,QS #M9J!Xo8)2².Q|oVL}B) O*VTDž*`=8 Q#YDAB޲(c>ek= D98,D;Z'C U\T@J{@du G8JI;;ݚ1LQ(ȉ舡 VBRQ@;J8;c=`KC;ZQՇ}Q)6k+TRqŋ /_{N8@ifY+; \|Qxl$bEVh/U-hcpƉRR Pj6)˻SC:DPyfcMqDݙzI-PEV@uj]C5vím+&F)2ȡfʀs*-hCdq“-1L'hG;Z5Ŏjج8`mAXf :7S땭≄'Q/'b{aA5i©5)+:T:hSVz8W+KBgQ[Jht /E֍Z KY|@l$)J7Hjmuh"\:֪av + VrĞhpwv3chh`#2#]oZu* M(*XQh頞@z:,F+6=i>ڵ kA:[ozLYVI[*T]ߧ[E"pV۶:IaOt1#\'ZW $ -^L)㶀nIh@vl{8~"kdwgӍM]t}ۧSK)Y`nuʱ+a=ms|2>9}XBCه|Lq')Pl OY"Q=O|fdɶMt[FfIIrG]}3;;5Wu79-E1UNߛ9?m~lٕCߝ\e{qϑWylVGF/ .ԋN"1m"P_-3tXr2ԓjWյSy9r.ˠ/kEr&FJMf&j"4SBF^65y(IF3*:t +I?fa< NRn3MYxSۘU컗R䙯#s3cO|JhMGZRY]䐮I+0Y֫.5hNx, 0K0uS413G3{2)4'53OLKRWȁD0ь9`*52;?4Ϗ%1U'dl0ičzȆL!HaR$F`TMvq"ɃÃP6B,`24G}k!esv )uolORQRB:fU^2Dkr)JDxw33Aא$\JDY>$xxZbC[`UÈݰLid"J HßGTN3e >w+^0ߧd~eP=df3l,jLn9p0sI'=fff˽9#[rBvvY՗r2? ߧeW3}*~U7rTGՁ{TLd9!c){* ,%$HȞ@(r t)q~HV#ă)g#$vH $R:͏"GJGE&ȫFY<$?,Q8-p B:T-Mj„؅8)6ёQS >&( EIR7&!rK-p0Asn 3,pX:9 #ql5 HWN1"4<>`/ Z[;SX,1xw4'krߐؓ!HMJT1qLZ<6mA} tdpnSܔ4? (\ ~z?=*9~B9ȟVF9O*V3E~JğU%Sܲgi I+%>)i?O6sy6Q`MD'd4:qt陴b.'̉SC[3t*` C(Wae0-9! j">xx/l'&>Ivn\'ۉOu$n)yq;񯺉w/m'>MtdҜF^R^ͅ 4Zx)jωqJ;_y-)@(w}l DzH3Hb@%$ybm˩K kɜO;@[j>r9: /E@A빐-PË}(K:pr'8y_A^G"ZLI c-K4Vc~ASGR$/l25[K)'9r4$2Kh!UFVke=/h2a! ]|y7їKW/]]Ayue!W*;lgsy Y%_[+2xQW;Mg;# M 6#]o'͝7*oM#0]*7 }3s1(*%t0mmUYGg-U;Wo?h>  P&`&F"Yq;@VZEm8)uuE)p&'g29jPD=Z|-`Z+e T'c+ؖf[B$>dMxY+J\%^ 1'g.d-dZQsxU2OkyW2fxeU_U8,@5sBC:\U'mBfCd ^( ߖXTDM^Ȋ LKh|GLŞ?V ѸHzM4~L/.Q${{mL?9f`LwcL x"B ]X 4-aÉh,%2K dPkw%1ؔΓ%{"fwʍŞp" _D,bvWu*hH w3LLǧh*͹ gѶlrvӮ_)FϨY̠c4fAMP\=9+j, -+ɤ~3Ar=7ߊn-W6̭P-BYj.?- xp%Ŏ=ir^v^9X^Q;ƃbsq<3&sVתUj&a{za#)w֙xP|t5YKqmM[K7z;oD`"$"H|Z q(ǿo1pꝊŚz]_ ڄCH$J`c=Fwc8hCZ@0kLL;Щ [l v6V{$6 u눠ha %:>5J9(̦9E];[ۓR3m&^TXϖ:gޠ5V6Hgk0驃Mڊ>qgե4(r.H ޏ‡OPgR9;;; ieAe(JwU4*ڐ,US΢,,s.ecٞ9)JlR3d'$3GqN*PR\nvأeߣvB ]M~籼an| ? o^ QZYMEI$9 8`p%aj IqY3?@fFExtS[n>˧:{-͝OـV|ڪ@5dTC9_Xӊ-m}Ub[E(!x*ϨKfF-mbb|&z #JAdZ?ɰ xo2'uU"vm_64@K'FJOU?p+?;ޣE=ڷXD%-mƥe MCԏxIcd-dWт^yר ;`Qw~(0;;{M;;.{@bYn[ةPwɠJ)pǝq/ræ8$-{c+TӷZ,Q* ]2'E|AP~p`~@nxnMx9ċ({Xn+c! \] %Fjy4Ïccצ4'0{=(r/w%Qvy%B]bjpgҗ0q^ឆ];Rvto ~Jrc@Z|+jC_0&ϰ._w_lQX~i eꖇw#,qvɗ 6~<oG,vH?Tb X,B6[dtiNwtCGgTQ<h@ZDofmˁo N:D}O%"@jA Jܹi96`Rs;RtA5[fc\}U(џ1쇓!,-ZTi;py4 r|C& EV*7|ߌ%E)?uW'{8/Cs---BD[t< S*3ݧ<GSA3tc D3EZBfĠ:eU(P0?`f0CbPbҥ@ .1*zPh3#6K0C,bm(ѿbQ/=/᨜Aex5{}:GҋK[V I^.l LZ*OXT\Q沉Օ* rr@S\s]Ć qS _`\TD/{ %#X(Qpi+WQ/ӺX"T>`6HY`reQآr4"G[bTaWA1cѪ l2Y9Q*f3Pp^!B`Nj%F~;经beKs)s5@zj#H͸sR* 0h} 8n 6؛|ӑ,*̅M`5/Е Hk,,%,hFN΄3gK)OΐʪE%\Cc1 _srl,}e'-=,=H,"sEY l-A9 :3Z繱B/L>oV#Q5K-g[FѭhybI"٠ VD7t {F6  <WT npU~4NKطOk ܪG)QԳ wZKMoYZK"<'i"OW ^'T"UO͛/'_yzGFxϐ7r> oY&oo-o[elKQϦ =8gXZDI<s1$#Đ 1"PbX@! A aHGưA,0B!| $`X`L"XaE~AzK0l@a#6!HưA -~ưA* edZئiqGIeh K($9]Z!%.E]b4JC#jޠ\GNZO(ti>JsQN{~F2ٿS:b7%٣G?edX&  H·("btDFDäD?3/-Da`0iuB(HuBxȓIˌUQ/1%O)>5I+-QzĮ9_&202(οcP,G-KHCX@Ii$ Ȁt! h$dCIdֱ̎ZRBU\u]iHD'qDBZfG(xL8*L'CDӝ(:kP7Q7'ŴOi fbgx|EK'5VԶ-qOOl(D!uS]qPR{IWՁ| 74@TLLHXTe Hr0ub.Cdh~%9E_y'9d{Th!2!g^·FpUSh+m,-3GFȂo#"l0m#-GKx`Cpk}\v"ZhdZhp+I$䣐 xB%(V&04C3zM#PF:ch7 ji/IdMKMd׋7(-\:ѹTUqfUQ[k8jY4v* $5sQZEhR{ml˂A:J! s;'i3˗١-#wZԤNqP4w(:+AJ{ $γ JҠQ}(mD,Wt.i$JX6,5,=*FI!ߺ}>, %$%a~OE2{¸H~aG$Nh܈<Ӈ#< ?GoNx_̳89"}IȜt 1s,!$pxs͙;oNI~2$ >:rx1k3qĿcəݜ8pƲ0~N_^c9y8ፄ!9`'֟؜W /?&H98}]p%t8Ar`ˉ㿅zDZyasx\q7=m\9[(tP}9O\D<[<s?j|voџAY䓾1kW"?/=QٛyEG2$]^Hωn}Oo&qwȘ8('1QWx$N]/uĞϣR͞ϒG|1eԣ ugf_n{^kОtx"Vp '.9DqC"*lmKDWWǞ#I8o6MF5Ep|b=Il"ɕҴ稯?$|)6ӁmdVGcW-U~EnHRdttC;cF*#dh]St#]?lw?aD[Ў73Sͫ},6yI1&l!ϗ/x-l"/`j⫍tIrXbUIS߂~p_׿v&g57Ofê'ș굊JkH|sG}m~{Lez`] /h]7A^]Ińҧٯ[TC}hu7f}Q붋^Yڟ2ˢԅ.ս{&^_[_~FVӣ7Y^\c˯Еt@ޭutf?XuC燬lTЯQǎX_Qb[))0v,>`I–L7|4gWq8d -*ЙgAWĺi,* Os'EXF^͖i{=l^VZbFp뛟S7Yk vTWVm[v9bep8/ھ<ȨdjH)Ei~q;3p|soۀARу`λ;y OL.x[r;{^}Fl?yȦvO\53,YPˬeMٷhRҦ0ۖ/]Td2.,|g86q{?b>NZPfBgcL$ݍW߿ȃ_P֘U2ԽS6k9r(ѲhF-&_ګ˛c!LIpyNs\Nk_yϗ&w(?WOzS^izDAϑ^IfOl\r/ATܭ''LٱM?V{|7ә'o;7D'^la֮.Jw><if>n\dwj2gңv3<&w붻%e߸/۞az[fuͭV~?OD4-=y̺3_9Ԯ-Դ+Rj*'j?UܻĽ3Q}G=睉b";HtW.Ž?QӔĽ3qIܻQ;T~&mĽ'={7zC&'pw 㜒{;@e59E87 7 "5 w@TVw} *ϒ}TJN<7K F"PH xs"~<=ٙܮeߙ2-鳉miz$[o"mq6w_ ~#Z82 /^3鄏&N#cGX;c8$K>󾽩=nH'K57%K$p`<" $$㷻;a8b#}{?~\KH#OL}I; c 6zV['ֹ؀Ϫ՝f4.DRqV u\8ߌx?Jtl!-c^z1!@f(c c3RƆeF3cFƸ2f#3f0~L32ٌ f&Q21L3Ib2L*Yd05zfh,&2;L>s)`2L)S3Ǚ"e{Lid'L3¼ad> gB` Cp` F;37|4f@ "AP5`>X*Vu`@6`'|pcg9pT7@5wA-Ax@h/A x ށ tԅ 8 C pB+h m3t?@Wx O@_C9 p 8F( Pcx&p\J k`&\ p `6p< Ixga ހ5{6)|_Ww~aKZkd>l_۟a-X XK֚ڳ(֙úcX7֝`dzDv;bb)Nei?; `g3@v;Al0; aCYƆ,e#(6U*6O3 ^c&PKiR Oj,9!rebar/ebin/rebar_prv_compile.beamXypUW;ѳ-C%`DH!qClj#pr[j²$ Gq3s{؝aaݭEKe?^,ۙT};j](ԛvٶFm"$Cϣ3lPMG+Wˍκ[W.97:vY ]vT:j]c^4J/9uШJٕJNN8SFh t=qTU+& PeS*ٺ% 땲:Go]i:+vSlzSKe@ʣ]} 5΄Zwjq4^5=qd]lxD(1]Fr*] vhs(a!T+ˋ[2$QH6X/gxP7NV Ega"IT$ђSqyy3DIJ hj}N+=bUJqVѼWsk7(+nݚpA=SsˎG.:m&{$]Fq3icirqt%L \) ŊXA>}u[;%sVw-KL֯HIlS l־ڥFKOVętΰ'>BRϗ"Ծ}- Ŋ7C項86ӭ%;=XLؓZӉ:@5/ O7tÜNssN896T&SiÚblJzfB"Z̟p&W股ЊfE#$PHrFGl$ R5+Ry"l,մ4.acg̻y2XT2<PrҌ-5eL+o|0HX9iY)`7-5GG4K_F:sj4ǏCfjKmM]}Q*F?%>S 9C}F&3F-fJ;_u" Q}]WC5"13eKU$gQ,J(,p 1ba6s'{U7+|kY%I+"KM+Vs(< A%- 8SɅ<8'Ԥ$-r5; 倎d[s8gK^6RoqĀYI&')*+&g7=_ !fW[U DR¢"u/J豌nZz\a1I_dFFBHv :س"YmX_Md@it$H3ZjfvgUe, ɲjTR9ÕLV5º,jd 3~x' 8K,ї[T}IuJ_<bK8;P e@ٲZI"Yl5ۊV&A\g ;5saFa˻+%*A`\0v1KLtLMh@)jMW~%gi"UdOO),D rv!]I?Gݦɜ~NүzT9 8$&[f ӣ"O_`%|NC2 Y)~R ghJQl=6*y~e QS ͡7l"bE .7br@ &8 = tU`6I Dl&.EBpĊ 2m2e\u3˞17]3ٝO>[QI#Ŝ]UW_]ң[jd-.-ή`g+lgJ?'zן&zmF0_973>gy/;ٌ.D £ Sf)C 39w=0D^ vP#PWz{]@oW.~=Ol7gw83rgQߙr6밼L0Erh]/ f-Ko5UP\0 n _B"H9@Ȓ+2&0 1(hCQ^zLbOW)lP 5h_au:D#M&^}@GQ9@~+)H#WYvdRqz]͙Wn5]C(h|[r9k){;T[Y?Ɍ~9w9w):]K|FzuRub:rvAd\tvr|n LÓ`u?e;{[mѻv#(D8MaF]`Z3t7'35bC2$/̓HJs'`LsՀCٽ xoD9ReԂ,>^ff` @8}?oŨ@f -=HŒ|\zg?ATT:,V>C\hEsIOAWaM`Eâ!NTHݠZQI>ڬڣP?e;ɼ73٣@r-WMig ?;3hj^#v Ü=q9;DnH`QG!v3z VR~!4fwWHI\CgO!!Kq4zCYƁzƸ?hgHǧ,¿}"ň+„ensPq> g!,ؤWUXM/) El{W!5՟Qt{~NDlӊ _ϯ$x Fm/O=@}5WTՠҶqw;nobxEuu)̥ #Xkr* g,g?>~_$y }FAhwu9Y^gp C|;go@ M*ބz}GЗ[o=Z|qOl6DmNtɭqv:@25(ZJʦCpm?^WDh gl?`9G0G8wMw+."O9g?9AhyIga Ձ82b0 )t+ygqɮ{OȖF/ރS= ;S $%l'ϺMv}$# |b0 O?/Ͻ ,~L@ σBy?hVSWv@B/@&͔Es/ST/#qK{¾:a+@$xXZۇ S# {X >%]G3!쏠A(t?kW'RQR~?>QR9Ǔ|$| 4)O (?CTџU] ?m\f`Fu%1$~>G ?<-_>q 2(@4> 9+Xzz.h eVF|~|~~JAaϋl_ =4E%ξø{$Y7~r{+`9߁QBjpa//ŞF/#^Z*`5ū#o?B(|Mz;:$`'ԍ9gM G~b'NG7{<[ҧfF_ 뉐~dT‡Ҁ~,O'䁸H )gS9_= {wwH24enMqyǀUTX4_Q$?9/n!uϩÜyQW39 gEϢ>?V{tLWɝ$$zF-:9dbF3JHW5IgBK)Ji*ѢUx "(o35|߬>~ϽE ]9c$N5%?W⌒FtR,hA+tp\ 7&~+ϭRbB(MhKPdlP23m`-@9o`_ߚv{=->dT A330m)T&Җף-Q.Ѫfv4y B+PSQΉ\ ='q:' UF&~7U]OȧGF^{#-,+J(i8yG-9`>3e8`z 2ԟkrݧu?@s=P/ץ\B]=h"jrI$(MFg Ҟ?u&s*14R7FB IFg!) C0"`'DIg5 M2:ڃlЄIN>}"JXJX(:Ze#\k@訌.B@[A8 T AkQP6>b׀"ߩɨ t[;u̟"S\wWC ' 䤉?Er^}%Z-QE 2$o͟>HL<]қ K;++0 `>o+h/q߆HZV H8T=@.t -ao ˨VK?{4Dz mjLGID)6J^)T5BH)W T.B2ZH⒉L\/j_2WdTɬdVP rTK S4. 2" @W#^0s| lm:mNeL )HG!B藜"ؒS)lF3] ٔu$, u3ʉ_+v&/RZMFh)w΢(R !0F7L5Z:ՂFhfVMe 2)zUU%נu iс;J&պWO+ɧځ&кojiсJ&!zXOO둌LU ZZ&LERhũLz5hũiZqA2j Z%ugkk@Z0Z>jj Q"hmPJ@Z0Uj!S-\O+ѧ&VU Zf}D`VpVIS-V+VŁz¬kŁV@Z0B% VzZq>Hou^u+^"#,ۛ%')80 ,,, 11 "!`Ql>?h 7k1~gYs`:`1Z3t)t)~a3XtCOgCROwTEi }s1ks8^f1bF+3[M)ThE(ELbNiqRV)֞Xob/)x+t;5i&3<uPԄ <7bOgTvóQ9KQSq꟞\(RW}(pSWWUOgܻ}aRGݒaO`lnCƥfsJwR->o|~tTY6ʞ~ZW8/s,/5f+<|!j_iBR _~e y 8W'.%& =;gѳQdd م߳zz醺ueƗl(ٗpݲW̊XeCcni>>ޜnfӐM"8u՜̵]u'\Rٌ i#Η>cOus"z&C+6ej_z[@[ y=%hU욵jQMܚmj7;1 ݓU7Di;(tw=mûH_Z^47FӺh[b_dx¨wdkf&&aWf )UGE?`ӚmEGO+vJJzpsyֵHty}CN;)N j/V~ћT5k7Rww5Vk!ϙvʥC{Tzyel\7W\Ҽ;1J]),sd~gݮ$~^]qn&O;|E\Wpʛ&' z :٭ٌ.2ykLoe7cgs^n^Kµ%k{,6&19~3~+)/dm}{ry\eaH}l x2LZ0bֳmb> M L\\* Was+;ʳO`aK3lôn`Nf\4.lh ;I̟T094> lz0p36N؈i]]$!+Ȯy)Nk3I}%V ͣipdv廽avuenpx(Np;2rYn˝=:{PWn^D!<{{CB+Ǒw:.##m?c =alvϝvU.'7}jsfugsx>*|W| _ǵw!~DT#LB "4"OgHҔ&+IYE>#_dYO6MdN. ?7r'')rT3\ *En?r< #Kh,f DXSƖHH$)uJƑ d"A,I| ?r /ãhPKiR|M 5rebar/ebin/rebar_prv_cover.beam|XkUNu%ӡ] yYbB@b+GIW'@:IAcc| {E:s]r}NQְ}ϳO\}mϷho窞zy { L7-uBP+ZKZ[9ZsugdjrCSyOSgccv dZsrFb TS'ybyk:caGXPtvRᢓSkdZ{rvX97^tj#]8jJ;sͅrXn|0x"M+* ;;KvqNxPi,Iv8 d9B)H_PrJ;bF"W]-a&֜X\qv)WR3VQŮjr "9HpإJX&l/D"n,CH?Hz\ dk}ƞL NGpN+ᩌlBPRP Pf`m|s+}٢g7/E!8g-Te #ڥvE~ǰc1LA8/dDTwDkcSW%NtLm3v9|Y?S\\"M1Z5RӵCOI)=SSiMŏA?&X3H?CJWt5Ru˯|24B&Ҫ>]1]QM<]AHQ7Q^է['i 9nTY_wQ!3y2$hJڦ*Ueҭ44M9fzr=HC9j'ɡvR#dдB?!ۓQ0քnw$3=9CaaO$]XYXY$_s^U9jhuFT_UvcHas4CKM MYA~~f5bVL[L pYexDjq00T߮X. 9Am2E ;0C%_l~ l^2[\y$cKOL~~?K}iCR]A+1nİ_֙SFFD.DߕJ. "`ގ`A,z^2DaIkd?OޥA=˄3\vQP;@@!GlEy ]eұ by:({>>#e&QWltv1RB| `Jv.k-솴juc /цSOϦHJWesJf*<aޟK!1a5NE@W"7V(vPK7u}PލZ b٠ ӭ(Wn\ jh K]>dju0aN_J91¾KB&dzwY c))k,o52~Dkf܈4 kEI*I .ћ퍭ISl3fi)Ru5  i؂(X20&߬oyp5,oReKED0txYD$ Vև4{PE1&eBzZ5bV laPD9b~/@N>k~pEkp O3MC!I, =@0@#r8dv`ɭ)Xep.ۊ)^86mu6"CJGv'Vd֨CRH+6ۧecbur`^tF_wV}zu1]q p-9Ut<6`DjUO2҉rٕ@&|y<${zA> W!&55I;W5@ߎ: ] fv vQxu m+pNK5TX%\,R7MT~c(;fJ˻uz2M%ȢD#JL>h Q"!t 6]KN4 !(:c'$ގKXOX9=sBYr%JNLl*=@]׫kB:xz3T .ˀ};oMp (FB-e)D(:A9O\fCՐv$


bN*H<ɋWCQ"߃^OV4jRjAKG5 [uqEJrWa۸Wৢ/@=wC ==I1g ҳ첽.ߪ.he4|˰ Hjh yIQ'$'l3%mZB#8Ɠb+8s5valjD~Ʀ"DrjBvB~&iU9}{K5˺lU0j@:s`v6I}p=]CvLmn aP[&1AuI>Qbth~E%S8];";: D t Ot0uV &;]vd e0*g ~L?(^Ь:,ieG݁Cah<uY]Vxe?v4i@۪1f5{8S Apa}HޑpG{dayG:1ۛN`h:яX,|ΛC =lxoiҡ$fC?|@vqGOvTh#D P}p^Cci?-jKa?wP^@IQ؃[I> LΜ&iCa=iOzS̬@d2RI#.s dN&$ V0(=[t@-J/%eρ'f?']<ɧgVQΨN$L993 sEŵ`yb{؞lO,܈wzR3bxNM'Uqz^^ǫbP}AyW^pdn.{wuI~?>Dž_/)E0#*.`ǻCcf >q/͘8T]'29};馯/:A>d.":SU~QEz WqUR޿^@z/uumq |L^ϝl tF߆$j`{ X z{o!} UmqS. }W迒oLy ȱo)}eO_ 8!`Ȏp kwhvIKnJIP? !y7HpCc=5t:)=U.{ڎ@ۇ m}pF  m m6EH~[}m@'KϸPoa;|?i,}N~O$ [i%"ۤ t^PthOC`4}:RTTeA}PQoliy?:¿PH^:O㥏^K?a0K 7zZF}%X0e #!V@r Wo8@yoSލ7;Vm]E?p`Msڜ ^h/U؟Mutc $K~Fa*U0iTz镴\qᗇ_qG{[z8Ch/}GMůp}8ETj:|r]{㤏ʞe;)E {M?<  3q/ wr5$7YgȲb` +UYyI%q+fYW"%A"2s+U OCsy ; fE]3w $wY0bYˋX9$Ach2=6XƲ4WΨ\(8ȣ/s^oB{ rp\5کd9՚CC`HdTCլRzI[:RjUyԘ]Ԁhy(E!  ) ?(Hʌ 3w.E2n/kAR^"WYT 1s!HmPr>\-j\$| <<@Iy .rFʩ-%*ka- ɣ!ܓH뎪3s#!&^'pu͵ayI{jYK7sÞXqX O8rn!u9.5++W[a58+a {6qTv4s\hdjf}U2.93*_Y./7s~}aӾh*H]u QJ2XJʺa ^p/!$y4s&ӆ)ڹyөX+971bev`k\y%",DvT!ߚkఐua|o!~E?u ERGHL#X `f^P24 r"ANׂI rƁ-16Q#1]x)&rmFLlfX:ʋOT#&yQBߛw~+S}Q~3?y(_X{H>fgw?bƦ_OuT_E` Z$ЯMk X{M?Xπ(]TX#1HFA^cEY~ޝh;—J*?~^U"0彋IgnGFoZTq(*gUC]eW- umC֐y;ђ^Z.TzxPe+L gBQ`)4B] s~@}ƈ“wNjHQ{塉Az/,]hpѮOo۴=并{n%g/<{샫2*ڹϛuyCv;qѪK83fx.oXV .Q̡O'f0}WN˝;ȋgs.=Ⓟeͬ?W;uæ>bɓ :gǽ_q_r _d(_8|LIoKnW$;W^W Y6#pڳϼ,Y#WM?~wu[s9>jNUO,mtlK„α]sj~R7_ػ߾MkOh٩]uϫesޱOnI圵׎э|T,9UmtDZU<}^]C{=O089/ŇtNa)=M3b͈GN]7z>{p`e6_N_tTKY>lzV&s`mI])uY׹ݮOu\v|dCȹ&ˎf<]p~1qfGɥENugLt{-}=Rf-tc޲I'ƞzKD67:kwN۟V/c[߻\Ƭ4=+Ys'[s"]u]zG̜z˹Sm#;lUzK,*m eQCl..^-ž,++쫫)_^_]N5^*_(> 6C`s)>M>͢xX&O7m`GJ\M.5`'b{z{,ՐJ4H1I1cEP=C|I1S™wwáߡTOj6PSR_Uv%~TO=ܳmqɩDC !̻b2?g LM%wlrd'i6=@^Dqw5&MIL2-%zIo0tQ&]\qFSRLjd2bD'bou0&ꍆBp7jg1IcJ3t&)FSIc=bZHC듨>XA˸Q)1S QcR>QgFBb0SAe4$N1- Q)1TSFe\Ę zC|JTLuÒK1il>̃ޠILl^zsHיR C_ow/O3 Pfp3%NB<3Tokg'">&rE@ ^GAh AC0H4EhAZ%$Fi(MAh&y((-ArVUCmGTvh*B?}~FaT:.t ]FWP9 t@.!AG=Au)zP#z;b' `W,2nݱq';. wݱ7J``ƃ'8p'q N8x.s|/yx1^W[q^7xށ N.p1 q_x <jB_&n.=ڄ6-h*@#Ѩ _PKiR#-T rebar/ebin/rebar_prv_deps.beamilyf߮'l#'uw;6 &j z;'Yff;((TDmzXDQ R+6 JS*?=I[ߛZ)+}}o%0<326|m1 `S3]49h5]I4浜jZ5YSjmYȲr銀r-۔<Tn02vZ5MìьtA)ӂ9*2:)lfDKE(QjA6!St-k  \ iꡪussyǻc%=&ǂKv[싺 lkx"ՃV =-dJpTAQ:6($05 Y}6x^A~% G ŏyƑ"(RfVv_~Uj /*~/3"i؃N.N"?|⑳4!lY_٥Χw9_P5,* ‰19gDD9Ws9| x]u0B`R')Qt6Xj c!mA%H yt\APWJJ%S5PiK!.  pB`%:mAx2EBN4 '0';R[IBaC!4kG<#%F:Ȓa$%3,n[)cDPMa AVp|$|8cwu1֠~Okk&e^@z!]J9#,LtR.CY80&:z'M\ u8ם>:%~2n.6,㸃llyK a*$Cg*#%΁;Q:I"_f4?*iӺ+eh)78 zAeq׿=֪]p}UeW$x p4$QO?^>9p`7D U[6ob+ʰn4? y8u oWG<{M͞ oVӥQo; Vl8))SUU9Xes/[uc/^Oo@ȣܱ=ۼ:uNj1_wh6ON͝cGwZikkkoN\Nmi} Gkz{~̉Ks<[/tr5;(Nԧ6.:?t/'t|x|G?}~nؿ~_"ڳ߿6_{o+qݫwү6w[6V[9m9g{๿6'>aYZeoL{-C.1?[n<ǣǣt[cҗ+ pӦl63Y5DŽgL]gt*Pgي}WgAӳRNX*+nǂd`ӕp_~噾gr_G!fxʲq^r/0vF$Av$FL>2@}dqr9@n$w er'|&3$O r7Y {1r#i(br]T2nԤu9eʵeaIe2 eMHk|JsJ7L2Tΐ]>2X)ipcL2J=OIY5Fђ9el/jV(ig)sTer>!dP0]LfTQ3tY[WWr]VƘ ^˦tV+5Kc.9'eDcE6e99Z=C/ G:sГRS+ȕMW5KN]ݢ d Ѣ6MMs!7}I @C+ij|k! 'A22E1?Y ywy%x #3 OYKy@X@~J% ;O4aYtbϣQ$#J']DUpU`yd@Z=lM9/qpG֑ =ew~>80 a!:dkE $15 B֬!A^.HI{L|yAG|؇p0dKl{[@m8Jm)Rp OL_ N oP@Tˣ^߮_*PBэB Ibr)a%)3%qh"` 4|P"V&QHۈ#oc#v{Eo.߀LssƇ+ NB"-6l/J YXBmVhC(5<8[K^H"f["4jQ@p EYv~fFfIAJvqfEk`7뼋>(iW߶ ųKjBanͱ޶@X[gDw U~ﲹTb5\*&kl[?poسkOS>u[QػG1$ցU=wn4>wZ{뻓U;'>&]ء.~ls&/|fꅟ[Rh7`|hw|čK xk8g"s/ppyX:ww6iϮ5a#0wsjlorδi}r/tpo_okиʙ_o>Xj掶/ыGwܽM/ũ; ,\}g_ .>^|8жw!gRV-]}v5_VGmχ{o>Y؟#qGB0 Z婅jOL?9|> nDOM*0`Lg%w2jܫˣ%1~nJ(ui2C LGERq-/zxgå &Ry$G0eF& \G`(T-iR{`%'TKˆAr؏NFIMX`!g:kꩴ{RcF2eP^d#gJ AQӹbFDY@QJ'FfJ\khWw9/K_a//`1Lت0R? 9[ĵqȅ%r[En-6p\m=}p+U\'Ņ9IPKiRq ^(@"rebar/ebin/rebar_prv_dialyzer.beamX[tEVVvZn9=J$XbB㛀0/,e%mn x53ٝnv9süpya}}J-+9VWU2ԙsgw)S_b5e.sSzTmhK}.6q0NRǽg{NQ驺`ݎ[>,V÷1Tnkj`sn+hBݱK繞^m5z8JKn&b=ٙ.FQVb]nMq6"1ďRq-/@RڪU[tl> p\pblM,R9nɩ BG5 TA dw`/e{i/Dn۸4KԻPv|[ =6*my[8l']Vܦ8oU{rnYjV|+͝`~: YFJ5Ers|f?gCG4+8ΗTdKйՈ/8`dѩm >+;]ГqpJiUmT@n*V\Һ8ҷd/۞`E[[~wU F${POŭq9\,ڕ|1N(>_!`t/YA[;J٫*'MZl&ctil|( k?Жm.Dm@8H%R!<`py׫aQszEDFEՂ~a2<6D!r`Zr/ٝS3Bq$jk5h8N RhFpi̷`LKZO_ys2`{'DAHvQ9|Az.ƥJx@g;7fbQF)۸@b BxI)nBIqnBaWJҸD&P["|B/rJw EHgȨ}A} ]+W.wl]PT_,J_PE ꃋ,i}8TTlX!"P՗0F˨PeZ+bpw*m]RqrU7edUܱW 'K^{;%%>R;ko҉qNSɾtf-[Ϙ8s2?(9Z6s=uw_'Axˆx؛0AM؇&۬C537* wm~nD:J^?p=unw:-*U] (`baWT,vSa,&PNg@6 Ɯ!d=Hq+zz Z^HheRm/D*+1#AYb%{}1 W`<ִ7 WɡouױgG lځṴY~턭qYϘ㔁UuKݣVwk{]^Tԣݡ}~mИ0'##%tq9՝qnSr<*, .wP915yxę\=M̈h! IK`L'd62zK=ÎC|JS쾸m+EYqRs!;7 {D;$9!K,^:ҫ!1Ʉ,D'dfx{2hN"% ݆IzHh‰ØOڱaKyC*ĎX )GKkL;י.avB6d1(0br)1%}s!ce ˓EQ,x$Τt)_<O1mG7(%(,W37;aej ' pԡ[ CEI%XAp!+޽;ww ]rʒ] B% }WxeԈv#ܠw[!kic`sO%f,IA@v-cLC{0)zP/i>}FSN݋EY(OO⾐TS`>JEEu;w?R/[24 ?[gLWgCd, Cc$̓z! BnM0?IQZdeR&<>X*3Fq_ אL/ݣi1P35)~D'\[$_[ N\`jy,(C@J5S$HȖ-jv`m q{,Dfms5(EI:+cXQC^Sԩ /"Ͷ Km-_e9dyzXmD[fa)aiX Bv 7efƴ&M 氵]DՍE[@b>kabm) 2[N{=+WJ^ҫð)E=@Iqnu`^h}5kx^JAo{5z#d0|E[\?'Vَkط};d!m )hn.Įo{[Gz$-b .s-13;;aM? t4|Q])*STݞO'Z1555~(w<B/!C ?XIkbWLOp3|LyRda2|$ĪB/ƴ<D*lD2$j#h¸/Hm[h$ȍiZwFQJC5soZ`JnL >N)5]WUMN&gUfss{A6ӓ7?So:SN]TP4__P"?.~__vh>__'o9ο >1e>=Itx(sjߋL Q$$PӐ| .AcPS'$yK! gc(U_FϷ/! x̭oʯK0%*d UA:߶zVODntˬހR -qCVdwP4C)(+N2o/Go_~k7[j d`o٩) 5jxwXw؍,5(Y$*^W)1VvH h(b/W۵{WVEvXQTD,473gʙ9{@A_V"R-W &JZTejjI)feRVE*,L k!6 e2B`VjÊUi.!mUH5_-f|r;&]Ĭ-$8BV*VnO"3V&N SY,Bh3Qp9P8^Oa;ĐjBd!\SFXЭލ 7LϣA)lV3\"?f>`0~?e2C~&d&xBCTɴƑ! >yЂUXNOኹh;N'xИ7jDNJ W@A!<H+V ]|+3k(Lu9&0 2=֏[ﻞ+ V8os kCb-W@,t ( )XMQG{pD‘<:OV ةc( EXG P) G9JW'Y@Ğ>NI728z ,}PL &ab B܁*܅dqBp Rs]SIYA"G?ҦCNs䃹<;ep҂J]OX)|OC4Ut RЧRl}Fx>'Is}i<}a0#l| an!e5. dÝ΁54+adE GB-PK909@ק hEsS@?y q#8$wBb(*ˑmO}RT@l8%e@ė-aIz&jR$B%p6Q@q7:| a%N OPkfJa5rd5> YJߚHwSd4%<$Tv]!Y rd-tS/]ҕ Z'o}A-N߂մĿm kpԁ}w66JX9mh@&t!|'!v6(z? аH n$@%<(cD<+DĺӥQiSD( h hs~R}dz4mJ1o;*'X?⻀)ohǑ!hTlD-?6.QmڵP8僧2֚xCE'|hBs,[›RJ073LNnje : !7D{ W,ńo _?dr}r2rS)BrEDҿ!A7y3@ԏzqr $KrH| DX rrOyS3..F)9-\hHSdP/1"F7?ʹp%Ҁ d d2"L@0o Ya91lPG'u쀤 @2lB8NXr(:Hnyf>4Gc@:j2 ~@#p<e?HO9 ɵF`L3(H#(463c|/0/4mQ W~zޯ`?'m3aTh⼐77\NTsg`o69,\FsZ_1x-~3#}[̣u$⑑r# ;ɽXa^#̣_ |<#'6r$gpyh˰> x >snzOugؓa9y|pX b~I zpeV3"K6ދ:ʯXo=\a{/+\f׹אan3|ݬOzUɕg{>i}fGo#zyMW6za~}챭 |q_5 #6891uo5}]:n@bdX^::zl)zfO5lߺ yO>aeƪ =6I{tX;;U6cgNHrRޏxܩgn#pͅפֿLJ[۸ҦLȅ}7Gi} )z6vߜV1ُ|Cuz;=؄5#okyҶ~uJ3ot4>Tv3OK)2~ᙽMs*tIcM8;t3eͻ̵Қ)w];jhٰVm]h7cτۖos .0d݁9.\]äc|{Rye^8j?%q+"[&LuR?[8R慟p =āۼ<#S$Z^Kj8Gܘ'XZPc{Q F_{|NT'2sSu_ێU*mW\1bU$'V_Ou!yc;Zǩc7i}Md"3mM79cDvOm=f܀.Wp&ÈK-R-ZY}뎥$"~EO_~{zoN;ޛw#͐Or]y߆ѣ.-3zbF^%tþsݐ߭[/ufXkH^z"iKa'd2|4*[=zGELi49#VuxVa9󂧻|[3ydnA]jFgSQ`sτ"Ym\7\ֲ!!Y~CPܷKyS?už# ]zD=#oAyԥ_uhݱ^s}fF@ul?uu4+~ _k֨P)*n~q{{pjśҷBwDqdcmYc0QnX&,`aW_ݸv_F{&W?{_̓91*[/la,K{ѡke"L_jFwCGmt 2V h1V8!+=ϘC,~q'v䑔- cYm{U͞'S?YSܻrM&Һq̫E{l]kuqK鷬Am*TZ?[6֫|K6ͫRM=잃:v jaYe ˸2(.Ys=|mgpLjҗ4t$/.4#nW ۮ(7>w}.;V&Woslubu]?Ad4¼o^=?ym\眳3{!q HkQ󊼽j)SY{UUSK=*];IVkS}V~O~vtB8kyӁۤ [$mj[.b;Ӗm_ % &9}qLdAːGN/[fqev^ĉz^nl>Gzj8#)Goga|0>_`y-P櫁 0_t/n`o3eG'4ֹy><;mYJǶz C$pnFLX0vbA0h 0byt߄l#oh%p~[@K0N/ZXw,_b/ ,G>Ӱ<(狾°Nޯw(֙! @1?#a[}7lj}y(v0li/*X}Z8ѷ3sǃcc"0/5$b## ?R+vu|"*P;% .,*VW uQqaZq(?^ + EHnZ@VND7*_1aQH@"YTlLp.yJ4.62Rs witʦ`lXD3h;k"ym"̌6=,B mt4|tQ$!1A1 m0zf.2,24? ڐ:, jB!EEkPO(3ꢡ0A`Ɨ1d& fBf31qL&f0sy|fY,cV0f#If1ۙNff24&9aҙ1&dN2Y,1+uS̔0=yvJ5˯*Q+˧wUҲU+feyՒs4^ okD |ť\+G.i-`yJzV/īX_/^v]6G)wdҩSzWSsF*ΒˋX]BH% @@BK D#12ALԘ$#Vڼ7/2ML"M0B#ޔ%n(Yg-5 ?βpX?mŕz` tor٭Y^q,oL6OͩXnUs0'l'hqp"pZH4KC0 \z-پp]xp(y9Wߪm{K*mT j|PW_ADvbO>LbCZ&;hp':S-ߔmKh뷫8pgN~ ?>9xǟe\^6a{KPKiRcϻ< Hrebar/ebin/rebar_prv_edoc.beamiWyo7ٌ=3G=uvbnY[B3;wf)&jjP1BA$RCiZğ6⨈h߳g[ !ҧ]ӻdr'\ka*0>[K5{kV3L5Z14v*^v.kZiC!`-4mbaPZ+n\9/Y5Aۦղ9LEasTjZrDnj V usVU:,\٨W7۶e,{Dal[[(;eu[p!Z6J+j]Gq\75}0uk5yńeݞ A]nV7dRSC(rښKX+jV龨}Uds&G ީY];NhTƙe=,TZ ME>[SmRlUJ5,6U Ειv]Lt ^<).JXHAd¡c"IIiRvEMz@~%5%h ) ѐ7):A2 Q % J{*gP?eI5?Ehe yUү2R$[RTIZGґw:&I+aQrAY4n>c2h {iAHM:dT&ΨR!' ]ϣB2Ka8"*1NC(*,-tW;rQ<+Nc w!$Y1])a>KP @\P2# Z q$H, %2J$ 9E앦+ (9DЗjsW/Gy- vM 8ӛh/-naݼO i\q@PAETe-VV# H`sK}$I=ҼYZhz򧊤B3|U!v]d8xrix!ڻA|y5tx&(.<)MNR kb 3it'x!1ˑ%|/T!fAvs##ň4Qq]%[ #a,J2`yx?X~D\D& q(A5cǡyw~spX~aUλ#?)1.G|j+K-`m 8巷\M-|eﭾvA!{X_g@/C P;|<cHI=>~#-i ;im{/㌟fO8@d8J96}p9gw>o,O;ړB|F 7e2_^P} Eoz_~]fm~O'}G_x3r~6ũ==p cS)/ח;_9sKz>f;GE9`UhjYܯm/ZFq|qumXkkUz A+I ֔Sz]sj̫o}o^] xV_(Vni 127ƼizN[k(PGf22@~C0POmV݅ zK,XzfS-{)јRKinT%`(najLiZ r FjA`Utǡ>X ˦V680/ʒk+Dm0ٍ%fʂsʆ+`j]7"+ ~ IGv5 W%grI|Sk>Yǥ|kM^aQÝx;Vq?x8'qO[p߆?o3xWw`ZN8x4`?PKiR<#$rebar/ebin/rebar_prv_escriptize.beamYyxhdG`21 s$c !i01,m, I8 I Ii7nҖnRشfmg$izl4 OM;Ɉ^{noX#-͹TRbvfH& l4O4[<b@R\$I cZ&[h6Fbp: A"Mkp"Yc)1(T__$ H&‘LOV"d8N'cZw<-&P&]DV ~-#zEpLKgL"qsD$ wœd bf1>HdRǵ(LT@V e]SOvlFPIHzD\o&5hK9; !?8‚XOhx(u w9d?sL:-F"id4aȈ`Q6YdKWqw*ٻ\NKN΋e{abgҧ%s)#ho|@ciX\rB%UR< wGZ̚ri֟ ƓF0#?a.ĮTfR{hTLW<0RRǰ^;eLD҆rHu1PEݩD,HY6OĢLd{;Ӯh*t岓 ĵ p(V`hhÔR2"Jj}p 6P8m<x'Gmn?a^]ǘCe;FTfE#Nj 3<a"^ߣڑ~ i2DsGBO:LNϮ.mO{_3piC}"Zσ~vaEoeRA1Fw|HLrEݑDV2dl5 >gk`557Q_,^_PQ_+)g Sh[~/CrVl0Fyq BM /q;EvjS  L( eK 9fhVᙦŕ/rGuE"c[cmWkD5Y2ĉ . huqEuԈF{moL$%n[#kmuWEǂʫExz-hXy5j X뮶WPH nI!ѼQN0oTt*Iv *t6ZR։NMɔOViߦr]-jTb(vȟIΡj-Zk53o @8*ꤙF:iw1l@6Y5e2EUvS˝&b!,^ V xZlĺհmp\<'$EqA^\,yHEᆱS):y$=d)rF~n'0Arw>D+Mhy>!}OndCf)#/t[hSmdc 'UYkP7$`^/"(xCCΐ5z׹!@i^фixaDMPQ):a fxZفV*`N.Y#U Hc+xY4^{dDTgQKOg0 !\'u*!6(,Ҿr+# !V0z.:#4x9<`w>7*X.( { 3[aV n z 5 frt.),:>#R| EQFmyO>**†PKi%Lw^[BؒII>h }3~T^Swb)za0.P>Y40a;rQH*QU6)] ,K7N=U3ZNTz}cc#%SgO6C5/&2[!!w ~X*"0F0 79䓆( 1a 43̝P` BG}aRpLgGK'T6I#J,㋌>K#[@C p4C *mTs.h1|'=N({n]=ʿYGV®b26tݮcPw2?NW)o ?oD@ƅyG,NJHH0\pSG167n4!#e{<"TnMt3}7#0^T=M8>]ف#f5Ccļ܋o Hx/34 +.IV=, 0zihq39vOWTD=AiKi&XEXXaY9\T/- ,ÄwLTJtU.9ܦXn3nl-/ªp={ lhX>mp* px-7/a߾6`m,fO-&; X7迥0 <$<@gOl_?]`.@HG8>oޗ<&dO~1FszGjYX8oq=\8x Q\k߻Uivg[{t3_P?ɫ?xDM`^RY{j𭗳?9ze~͏|ks d舘Wj>w'Uviʆ/qޞ\{O=_Kj}xËK?eʘPe髖o8>W8VϳZϩCVOpYW?+=2=/?ӃW.~J^:ĻoߔM~G/+GGyxeE<_ё'6^_7Of;7.]3;|ʕoqKvF.5u%xUW+޳:}ҽC\sqÿm}7 h\ޔw>Y]?ֻeS*->z.>wrS>5awpfp>§qr49+`elX۰~ ?X#3q΋.O3>^ܾY r #YQŜ~l͹+}+y$Zcq !)[.ʾ؃-VcPҥF R[MQ^g0g>9MEKgMӸN#>3'~~SDj4Qfl94Oh-DSVbN3iR ^PKiR)($=rebar/ebin/rebar_prv_eunit.beamXYpךVe1-tj m$0 frC"R[Ȓd$t6vnp2e*USS5/SKjg,S5Toy@`wo'V _ϯcjRNSB=Xr^TӅ[udnΓ:Ù\.`h+m˕qr)y弊J>Q묹uMRsj(Zv]V+ՆryomquD:z:Sʥ3 Vʵz'mZ֞sǦAzowrc,Tru2SO Z'hri̻%ICPPKaaxkrXڋpJe3u>U[*/In nNUҥ̤7zX -KPpNW2UW*7ݭTjiȖ'' p\Q恭X ]B,禊n{mPw;j1Sʷ|ڊ"0)NS \*@ [,nh}IIwHގzy[Ir^N9`k*Ԇkn\ݙb+A4O$X Luy+Źj򪦷P=BIg4؃T#D4$bsnn>U+Wmثp](`Ie.4*դDgJIaw]*O-k?'[(>lSˌit }k;;[u= Y0sӾcm?I 9=^-O0E\Ͽt{^ͤ G:OB"r@/?J2A,I YaVGFM p0(\kPFo.&QU(d'¥2 MfGnԬW{ !757eNUT+{ ҫ֓KHν<%9 2嵎R9],dixLnT)KT]r3.hZX[ڔZ.[5S,߶aÂn&o&dϵrc.C##['oSfeŽC^ lE+u 뺣=3w7FjmJ6Ru81Ǧ~FgT5SCpT GL]ٔjwPloE]q|]A/g#U&;Ӊ]]9]쐪Nh7te[7Q$$J15NYY;M'z i7I;2m"Gz?#N4hˉ' .FF^#KLA&NNb348;{I!s!­dQX7ɍ:fS:nP:%L=:V{,JUrw¶nmpxJ7uG{Qb(%>zl)7vzH4;- B!x9r,zl<' m>R;e&q"'OU[OC[~P(Zr P߆gc+R]a H]#R-yXoHm<- |(H9dbC! B@ %;zv8f 'w-AEuNHY:Vݴ6Gp]!6i@RzºX|L5¬yqN1zC 3iW9$EpcWp`\%RT)?T8q-CR0b~%6Q*D[ZM~ c!fȾ_jú IyY㱛ɺvO5uC"):݂Ug9ߧټ_ T6DcB1WUhŒq*I\108!E6bQѨ-7ҹ8CR*(dmy*(k]m:ܯfJ)ñTV5)fK^张3k<  A݅>%pf n*fiŀ~A$O)2ǂYo%TX *<[P,xh p9plgÍ݀NFM̦йMZ6#2aӤ6T;Pq_Q%u'T.d\KxnHuzBoVzm!"<7Q%.#:`\z0;AQ_h!7* H|)CtC Dhhq`&$wG(QxA^A&[,݋-MA1dXG= PTSl*KźM < .ܸWTv+іDSK${pŹιj۾Qi,IS0aӲFd/~QAhҨvߎ'= ,Pf {A"̓<8Eh,CiyK_{G r2ê<ĝx{߆~JwC ]<6 'VEW6͏ b;<6AA Xky:dAB-KsF79tm ʷN4ѷ8. vb ÂO5(UlJWKFm|vF5FM\)S@eޟav:÷&A7 "A wG6q>bM[E$& J<ܝ!w'%{w Y'F{q3C6[wylO[Zxu;ۋ_uW6{m1y@%c.S݆p#AD ' n8n)N5q'F?k˃Bg:j:kMz)3 tCgZiufscs :3a}O<@=6eVS~8<fpأ`y;-f쎡;4u\VV,|Cy˰CIHy<YErSaBl%'܉i'Iq^Ņ]PFW{ E?2#F+[lNhz0M ?.A־v!?:Sأ}(E6~u/?20/ŵ4W0厠RG]W^1^:lm8 N&>&='̒'“'f9>VME0^X $|aRC"xG*YXA'=v(#Sj"s`! P=a=1tcG}!H? Oc`8`8 -AQGGs 2^BU&qF)|i>1 d Xv\s{ G먫3)FAC_SOa K_qS|tn\<~۰9od's2Ο3 2C?u=v M)n$WȰa+ ^i5EK2O z*_luJuRL# _^%,&iꨧz6Kal~Zoa댶o*5'Fw|\aoȷ*Mޒ1=mi~I4}-Aam~>sǸUxgwip+g+9 ljRqf)_ޅ8W vKj5@׉@0? QuhjUq h}!8bK LefH?L_{-h|,?t=vZܥQ*&RRR >R* Sl9[aK3)YNeV<6Lx &|0L8+MGͿ"&|T&Mx&|0+$~wa0;eƯlu/K]C=>>Af߿xPBKc|^h-'N>@ 77a#菍s*$|"%|K _§_v^H+%.|ij9~FiXjtF#!FP?:o0 #o|/$dWb\ ]Df1i$> lV%wt53 +b> IéĒCi=(W*'JJoo3ܭ uBl}`JE Ęs2EY&QdD wL:A---1t[mpyq!YXƟ ܌hFW^b,A1 us,ɣ[yع}9L2lmUr =D\"Bi8=ii2܃ c4Q)@֫x#8:9 ,'vܠ/r4zj5pE~9s݌\uKBx;,!&S6VpOy3.",B7.G4Ԑlv\R?d<b-ع#3/UuM@==y8"hnZ#ոj !P9ܜDN߰ cNBCZb"* l44i4PN ,)f` f50zF4PǒΝ4\ѓ0 AOf 6v$,dH9$.Q*xwpw<ZU7䂬Y㽠jD~9h<)иih$D;H!=eиQO! 2x|G8X;Nfw4 4' GЮB'|p^v D%ȇ:Df5r-o5YV]n?77Bܚ ZVm-ȭ!"Vڹr$ɭ_ rG>䖫ȯCp5)Uf>l|yp.|5Y`evꉯO++,&l@+;یaWM6MjeCͨ؞62` [(leK(X#Ѱ].E|Wflo⻹ }Cn`TȬ5:(i@x80>F@ RC~^W:/7t{:eh‡rCWFb9 D@~   % 8D'] Xb:ћ1d -ׇBQo{AƳVk&C eﴌG'xB Ah0Vo.)b(TRĒ,C i2ȧ-PFo 9TI]*ڗ۹z" :PuuT4@j> F8pEX{o#=4ZG}&O1K>po%cXe`VM`MyxÑ/s)0`>QX(2+!$Kfk`O0KS\c}&ͮh\,xoxea\':O;:*#me?VVHFcUBILskƀsss*5'1ĿF% `>4K_ ֕n`:qy\A mM`a4NX$`1ǁIt~$5)GHHA`)b:`t<3TC|^Wdyf1<3# ~bgbl;l"60D}&MVhF}.{bgΝ*SCC9 = \yRkEZ L1ZE[;*bmB󏊘[NL*[#2PLLϢ7:QȽ+%%Jj*idsmOv9I(G8}gԟܧ6 7PJNʇ9͔ts I]:&/3ռN !fP)5-ܺ~uO\I<8(uzdA[Iw?:g۾/Ok[ _D6~ijVv,ظ 7س|ɟ81dAuˎNoI ڛJw ?5ʣ{4|ස;L  Ψڶ~xmZhƤ}E1V'.Zqg5,mҲ* yti~|ZƇGVt[֗4/+q:xʒ6OftPN.ٕX﴿ ψ)e-u6!C ?lcҨ tɕuOj4Voԅj#[~fܽI};{v`{5K?ubOG}/ʃW 6..u7[Ωw=Xzsl;t^vە_xf᡽람Zmח &'@֓HXՋ6-? ml[\`% ƪQ6}[⋃#hܼ"-[s-6G{Xk͒1fq8#'610b2q%k{1[^~an!fƫCΛh Y[Ȥ<eL-1(wBv;FC,cr٪ּI֌, ˲9lX-Ӛg0xY3 32,*I6kulAFfytxUԿW`rFwrY-(a*c܊-Hx[D1ˑ,xU"ClqȚ[lM?s\+SrCbި3 @(@A(0"QbQ_$R^Eih0JGCP4 F(M@V4CȂ$4ME .f٨Gc h!ר-A }֢o: DOF:hڏtBgP-:n[6z5Q VᶘOZ|_u*>{>~~3XEj#<$)yIޒ HRGO$uHZC zJ)X R/*0)\$7<]DT.CHTPKiR%d]X"rebar/ebin/rebar_prv_get_deps.beamTmhGݽeKEQZ*Plb#h_$Yg.WMi͏DZEPQTZ ~`?'4iBqn+tay}gv޷.{CـSyc8$=Ϊa]@I!ᠲXrؘs%׹2C eqK[!c`3CXsFZBx*$r@ ,-eŴ!34؟NxNRRJ&TXpX5a[i j\!Bfj TNX52mi">"({rdr+&D븀*Em#}L'DzǣS#[$Թ(xMW3v|~IbXugcG;..~Nܡ7`3ŵm񱁅掜qfﵩ_0wW;flv o[Ԯݝ.ud{S|55ѯ&';O[}U/Ƴ/YݍWm 2w$:SHIj\E@T. ?o]N|4x; <wUx -:*?jztO0/ hDUDxa26yR ƋfoLvBJN8O(k(}ϙ)aY\ž/c(HlFKLhrw\1pJo;~_"x}ym(lZnpC{Y*";3vvծOq,mS@ABe?y<-Q(ޡԟ՟_pB!PKiR* rebar/ebin/rebar_prv_help.beamVklU3(v`7v郂R+/0ݙ,3DEP$<B#)T B@&  ;SZ~07{ι̙Y9iE%W<{U†:vYDIȐ"׉>E4!>jjHNx3e? >9$5VPTUdCPͅZB9EVbE UQbDE96*7& WqVi,jHAC\LXjRT$D?Id1V٪5HAV92/;Q >:B2:# F'@ Bc!Bf4nXJS^K0aݸ}5ĘD-K+ CP9#8/\l\^a;%;>Ccya2k:yaI&nPJ(d;Ma! CX iXB ffA0f(݄~JWV xJ2/!}D^H 9ZUwcD"t[XnH)n+6"9Tr6 -8#۷%[qiJ V  J8 *DXi,|\Ɠ_JBnSQ ⦰Iεmi5B6Pْ;r؎Qb6/]k2۸ 3!c܇@ X0XܔIC7ec`&ilG;hܿ!3fpjM?Fq /̘zʔ 7򶙲kx!?.$XeCs`R?wDx.~n,=J?-/~o\Wį^>kb}{t8֯#ްsw9M- v5~Qع:|r OH|\noƕV6*YbFrma-fXV4L>pts?af>yws79ZmxLơqϮ^{NgM/ߎ[ϛ{mZp\<~DE .=M7-D7$lj[pwD)`077䗋=-ݭ9[,̇x=K:3j<~>hjۃ'߾W,չ7x4ٟg^GoQBnn/Β5]7=-tkCJxdGH1ɠw(ޣi Co "D*L@xuu8?u5 *aW'a%b[7k$YzES]jkgHHw?5TDxQɮErPp)aIYo`bw4xj7Q%T%nĢeF GQ&K}LbuX2:ϯ)S$QQYN!ZIϖ~fe7/- i+b!/T}ZշRBT-ھ;}R%P b6^tރ3!]w:Ї3@!<%0anAf6)ÜD-a;uj"Q=3GjM]#x3uM#UaC<6nDD2A٩ۺ2MttLoJgӛP"z. SYZ$8I5Lu{S5GԈ1P!].b!T!j5%M&T)jx7Rưstq%s[㦚Sdrw^;ݲ^ׅf$4R_4KUh~ϔkl0糃?C?-\6GX?xjP~nRݱݺA[== oWXb h%v͍m Mk,azyMa̐LmEvmuX~-D53!=cGlK] A2X Ov-= ;$|SgN ߭R5. cẴPq!@0"jcw!##)5щ>ny-1Cc{ oP3"P ]*h𔥰=3 )5"݀SpT$=2\D$@.}Q YJ MCofP-4)VcBAd/ADIJm]D8y~>PXjߧPA²ڈýT)$DzC9avVa݊|{JgeucήA"Ai7H!;=6g\$G(~Q* IS5}cԤ5 D6g*͟tScS0qɬ,kT|%t6[;~gnzX(Bađ<p<.VeHIjUE))r9WcqڥOwP  'ZUMIQJRFS=DJ2B2"Ӆ„͉줘P*c|U*u7@Ce1#v̿xl ښ.@$OdH X XސÇjZY19*rS"SIi C>^?F?&Q8NJ^7P1.Ryj(F,{<y = Vd|}₡.Qix'")H"S?}߇hd)sئ #$u|}M/^y;y0/t'"e+Hatg@?\9K8Op\jdM4ɢ& nLRtc;$4s엥hsa4f`RS_XzK†rGDE*e3R{ڠb[J>gT|.Pa*JxbdԵi*yH}vE/y qݔ|Z˧5ذ?6a+zt% >2UMҞ)+$;K5rSV0KT)w[oC0):Ǖ^ }I=y^Q<"[E_}$hk&BLP_MuLi%p׏X4׫V iwH #| /n{ e|gt- F;8{ Ў7vI!!36Pm K`"t/Cg(c"f_%qvAa!]S- a" %5-ļZd-Johl_I[mKo;do|ʷaHa[;T+Й"*NޅG.9o!ŸݔEr_%cW]o74F#銛<0?"l87Ep~m`J>%g|RJ &1˄^oQVQ'JySd̸Ҿ|2 o {[:{tPN} ;jd@ݨڳgI.~_ĖT_}Ϡܛ0H[ >b5*> Z;'`8~J"e*邓u/+Ǣ["u!?ʧT@dB7IZpsSk?/tas>5U%Da_# /CW]:0\__i #Wb7ɷ FNsuW¾m;;@}>Sj_eSʏͯۿyH#F\X*#~3"&6FNѯ a!k$<`uKZ\jHAﱽ~~7SL~ߓ?xl8P%[48vc,z c)@ :p{2r<"{?&j^ bŠ5L]^aMz3dzM5HdLя'x1,D4, +98'8l ! k{+2pp~?p`+ΑNCpHV܆C\>hs t~2d]d!=۾dg**t@ ֳ! |>oӶ/h tHB/c,\,P :V#7CV玐[[#7sbچu>̿ohEkVV{8эbe}yPWgAeވ-DmDVDT<Q@$FtcYefT (j0H99_ɟJ&oOD)}p*$0LfiF#OaXy2XFT,nw[#*9VS0JDyRT<˰JCLi 3e`Xޤ;}Ry,}e1yxgk(2tdec KVNWjiLz5M-FdӨW-![l^0 fC<`XEe8hX @(X ` A:l= Y 9pq8%P*"\+p *:܄T=L ; ^:)X>$M\3>$`/v{ hz" M5P$tBnM}7r4nq[vۇ@d7lc-*(_BdX4!q>-MX/D"%!t0-ss.`h,j5 d3\ H85MKJ6lj'Wdc*Q`N@MԈI+S5K2i&_7hLUݸ:%-ږyBQm BE^ [Od1<@i@. ԧLIa(#[9eShT4f t4ۑ*u<*W+'5j? '"G\,p';C9;9u9Q1Q I_?}屆]?]s(KfHCN5{/7~tO:y\{sC`WՎ9f8̾_]U6h~;U3yϏ\3:aL_g\V\o{OG"g}}iySX^~%1p}||r*HdH+/:_E[x\ͫ%?}V𑘼G=O^wf wsF~3}aq|S>yl5^+FSUcRN]< ׿nO[Ս+lH=^gFͭxc vzsEĉ{Q_M'~_={7)ixiW?uMk}N󙗏VM_H?ȵ[￱ ^[sucm ?2[o ӼXmu~q.sN5MĖ]. C *x؜.]b%u&ax'g/+H.w$LĻW}Ih 2B4-iBOӃГ*>M * DJL ]l]$l FBC]_<jX5$=p EU4560.ԑTQA,a>QWD#CJ[9C’;XRǭ̿#+ХC{0rayN'{e2W6b5BS'SGӅt=. ]IЋ4Ot]KJzD{}z~vZU PKiR{=2 'rebar/ebin/rebar_prv_local_upgrade.beamWp߽vlxw{!$Xr\1$n$R^6%w^XԈq -82:FPuH j N߻ 8Ng|}~{o6_ZIQYjlK)Z)AY $@T @2ѥa#ԾHXi!M ezgp` `S{maզ1P2#UHH$dKJg$3:娮dr"qP M˅#j LRT =$`fPBJWOƦE@4ػ# W3hL02ݲ=MѢr++ FyΨlJ7_#I@.J}dsmf)յ#QBiǂdHaj`\X[cTA,9hy001wލ-wÂՀ0IŊ-*Fo;F^1##GHGM>d;1s `y̤9iljڋ9y.,mJSL>gND4M0:Dm`n8&! ZlFȗʻaHnd3dJɁ3Kyb;\~Kh%;PJ#Aɟb|"ͺ%U$r9ɑZD6d 2Hz&Mx 4 >/G,͑W!LyAkƷ,.v7-?E;,z*\pC(̃ms!rH}NOt2d16VS'pB2L@Ckv8 L1^`Y& =?w7 6Yc~r~.l OVIGrT7:[r\]sW2WH+$;{hv`<6B2^y}u(#-L,fLzoJE*u [}T:Rg]E+R)dCM^B2+\- iK P˨+P^R#)"dB&94[B)!P aCB )]/B ^_|՜Pk~5p7[&~+ljͨ ;Ǝpϛ;\t.5~s=;vo4kCjX͆\ھf=w_軹dh,ʧ.^|m-xsƿ9{WGWO-hKZ/[ o9,ܿJY޺o*p2>jϥx{'Wly۾9gԑujF6]K?;g#|xxĞH￵yuvgލ?{qfKgfȵ[ {uw[ݹxŷ^~{'n/S5c6}GQCW JڶY̲,t'Hgm(n[B}z|qL Y5wu7|T很R0NݼS#]qnҙn2Ӹ* }.ǵM2ub`a4VcJ_z+٩&LF H#\7촹B;sʍ=!!5-Ϋ!EI : x8ƣSdXz-_ S~YZzzё^%k)˿H<M)/Kj}WRr_:pH饮_a.׭~_zꃺA5{Skpb+ʹ\5WI\#VruZnw=µrm M\'s!.Ÿ8r\ r)PKiRd rebar/ebin/rebar_prv_lock.beamVkl&uiDJ_б\IdQ8IPlag^-ЕDʦE*Iɯk?0o^E.@dl]e:`eX`?\Jǀ8.{>@Q'ϖJӠEQ\j]S-ZgL묙zkhhy>IMՎA̩tW[k>LвX{l89tv5skFXJlg)ͲQ;-_Ս7\Kc\TV7-/i5Gmy5mhjMGJ-KTZ9K8l|`a2 S2bJ @p)P`|K T# Lo]ތc!Eol(Fu_EN ui.5#KHdfE@(*PD9|KL2H+\(`p.+\fFDIEJ1%7b.% C()naL܀RC`&*f%N$e vLdSH,rH, ä!7ڥp< T3؂6d@edFE}D7s7Ea'+86#q nEaq p<3$pm:ȇOa Ax軏FV: Q1@4PP(|7MFlvDr>"{b> F0ή]?=7ğ+!=h_dOLE| V2x*(@\3Q,MlO_0x,?/9zჟɥ_~fglfucS"u9Ρ0޾.].]zKx'n_^]~|Gybs?^W"5CO{I/uJO[qu`U?=2MoYC{'_|^z?N1GKǯp7O_x\.rnj.ٿlCNXqO#~(ۅ+ﻰ6bS_ZlmLM^^x;ν*1+Ɗf5Bu\POF-4 n{^]=]oe& =" 7,$uH~iyeۆ_wz>hv5P1-֩aQhB) AЏٺ֙.f2IU뾫U Jkm˞yf:|gyn;`U((ቱk0Fu=4-O !٥N}E^򷏨0R瓗z޴\4u'G%:͋|͏|{|?e~s|?ΟӒ PKiRLMe rebar/ebin/rebar_prv_new.beamX}tdm20|A C6kIv݄a6Tlvg%n*D-(S =Z?*VUTp= ,aϹxkkm~J_Z E-bU7P0!bXSaTXDVSaU¬b* |Q( N()y( `HKct< h(bJATQCY<pQv)(J" bJ0D.̪<nQ%Q0Vc`4KX*`a9O ᙾDK>>9 32$"2+GLi5#[MA%N^b&ghL@bJnB@VD_Qc%LRpjNL`(*&fFjrA2%LX\6)Aiso&OVC/bcJbbg*L\M& șWq650u+Xf\^w1]\4R8{qQKLQG9*р*9˳VsPq2iby)>@~>@!CC$<H%!^2-IfD}xdxZԺs4I35Zǖx~|{2w-qd\ߓY[vdɄ}&G&&ߦ{8v1wT6- xR1/!dC 0{蔔K$F͂t 43v}qia1?H ¨<$f&iG/%v6vM6ɬQy+x6iN$5.*lR0HonqD@NwLlL@QgOjLqb3s;6ې)InDpcfG])1sI9J0RFV)z`\GiL,UV-˞?[-ԅ$;*ڠ"Nnm ) !+L!5f*M" |4sT$%K%XfdNUL,B LL5NG!D1U&_ Mwġ*ڊ<@fȬWpUZc. ddw9;^ /r@2 4SfQ%@э;,; EJ \󨁀&@}UpcyѪ]i8:a{Ԃِ$k;IJLpi,r[eYC]ET0ekC * ͆5dsp=ɨFj$he Xz l 4lYypX9umzBus65YCLN0)Ҍ1" "+F-lɺ,"4ni]R0M@fm"M:|@43?OF hw&Ь99fC>B[G%#&C#~ڈ "|^{&Ap܆k+ =Ny ƨWҰ7)_bojw'Tj K|U?ڶqncׇ^{=G=?Nsϯ:މ8E>Yy?}._wJMKϔV9[퍃~yٝSo#UK_V(aRK⯨oFF 8O&ad}YXm@3rztWD}^nO]b_:˜' џP*A&t*\NwpG\Kp)UWPT_.%UrTW !z%KQX{]ߣRDAIGbWeI$d9NrZq'\bb b"Sp"W$\ ˩AG \\D|݄dܦ;܏3x6߁›xނxމ]xUDQPKiR "rebar/ebin/rebar_prv_packages.beamW}pJ֝W:ي#;_eؐpȗ[')YΎbNs'9I8-pŤP0 Lj tifBKI'Pзҙ$0Лy~w;k]PxK[6m&B+=Q=#h?ڧHR6+ӕhZiix<2`mqMiQn(HT3WzwZT2a OIGɌ銡ebT'f `!~ЕfX!e҉HOBY8LzFqF©Z:ҫA@E5=rjxݫt$+Khd'/93jmQ Tmzͯ ' DSf߬%'),#LSF0 3jZ< i}Ϫe:((UU_S7jWJ4R.U+9~ =BPTTؼs # ]#lv—CE!\mBධ%їL_ՍpJ$HƂlǷdN);8e{Hf{7a \UN"6M؂  /b3唤^I`:d+9$2u"q}b.~'8$Ӈs)i7ˮ$t.Ⱛ41z893,P)HqC5a̐9v fF̑ov{̜y]DfMDl)V"̄ys3!n̥Z5)JHZ0 l&Kֈ#YiBPy{X`Xd=`3@Vh (k @@l PKJ?tya1owǍD,<e*d[@ 9Vo^6/'ca Xba#k{/"Tf&[oM/8i6V]]s¬YqaVZ=Mc@6c8ưbK M6dKhE-Y8KC&;sAh(Z[m'9̢ 38C,o=!sCȞ=/&;.r bsd:!l-Š1g)?;Y#?Vqvv[a;2U9w8;́80jEw2m%[`MK@—v}_OV4ٹ|5Q KHh7sMİًML1lE9%xaA_@Bbr[D]B?]<va=?."s J [$Qt:nS `-J!N7?_n%`^`/1g7 9^5b;hv R#_ h-GP av"U+JL`?5]=1 /MT]a /pe,oᔟm--]/Zh١:UOWP>4ͷm([X*->9Ns,n٢=R;`ewoCt W%0yDOns\6/L{ X}SպO<м}5߽T>;JYrRHpy{:%WlPK~tb_U~mʇ߶g&'[vM7wjs[>źѓǿu\#eC?v|d6W→^v`TZ=3zS`UU|kx߸erCe*|76u~{oWnޛ{<|΃m{w|0~7=vnmh~twl/xEs"沭o{鲱O:5'O6ܷwñ'\G螽u}z]_ڇOnYGˢ_N: Kw3^{Qg@}ou=(}Z{{^g|o}XAO㕟[q!kVpWsp+Ujw-w]n#Ep\*PKiRe JW rebar/ebin/rebar_prv_path.beamX p߽[qs{I |A l.0@"rmK.]Bgh-Xֱ[2mRѱڱv̼]QT2AQ EQ<R u2%Gx4iWI 'Te2U-;ʡD㶨2GTU&T !)jM9&OEw䀵d.DRw47Ѹq )JLųFXbƄI`4>\C"͘{2cyb᪭>x<_N=u3-籓Yg[gGpz= q%ԂAԽLMa :UapZ'L'k8N%"SuSc3rysO9]9oi)>/L̈ HUĸAx$x# 'U%[d..v'/Ѓz_7C'6yz/~&H=~WQEp(J1qG\ yv78#,H5a`.OHyq Ou}\GyHO5sL;rdI;:yw28|,[[![lǖsNXsgkNjle4(g# URR;{95[jd%—S2TJE+}R>(NR-=r ^]Ʃ`pANU_/VU3A"?sZƓL/I m;vA!Kf4*Q\ 2 H(Q1NA,$bI,Q  8WX,1:*b5 biţ@,5:*@Ё)D]*tl~7|o8ep }\ "k–pЏC?^}x)[˵y2-[3Dha&ZA("b",m aDt !u.9/QB$h9#tNķ3md|3SX,O0u5rˉd;ꄎ>75F5FA_khtLeS:MkAa{E4Z=k3;fAw~st]-%=2)eі#vnFeUJ_}6/Bk|W}U2ײi|ڲi|ï v<(!?Aw@:]5:ҟ;ؚ% :ez=~6p)o2$,M!iQbHlJF`!b"OW#  #͒Y #sa\cN% cgXB33,N w<7Cw~/e3!~3t,]CKޚ!NN[~WXg%Xh$D׿3%Y+Y띖LY>8 G0%-DuΛzolPӻYg|ZO|7WYxG{n~uVi]]r^ ~w[{6hr9Zyǟ:mV}[)?Tk/?zxᮡCVN ;uSq+{p^uc}~IټT#?n=q.r?{~/uϪ.[ܯi>4tvrgďwzӓg~¥;5Fo^35}|# ;.RGoYn~[z"Ucɕ0YrCtI_c%>mCZsRc=T#;E(fR ؽsj>K[ۿz1BChh2TP <#:WT9] x:Wća~J"UT2UǩV2!{T<.m:5x4T4Bo,:d^$BŕmD"KD%,kAG`?*yc;Hp*N^ Ǔj(,G(綐G`H3`jGǩ NSi KB4"rJUY1nA^0яSE:5WPkZȧ`jeIȰEpC-y;[4aM۵Pujݹ:E;--#cg &Tڰ4gQM=[M3LGZ{?DͩҐa1 5ZAY=]̱yvjp^9E]49D_{jOBv)aBT^PG+"A*/ ?)*$~EqFxUcH撾K8F~\D?%$SP\~AL|ZEdu^NeLѿQ0?RpjU}\Sh"4xCIy!4uq\s_J5<2p7?x7QHΫ# rVi H p UUOB5RTTit4)!O*D1Ȅ$~ 1"s<7Kr(VpBЄ)Я;H[zn&2m>K 妵g6(J48A^C/5eCJC$V|t&܅$x* t Aut AJK l@U_H})Z(`#AM wG ݆81GTIJ8pE$jN+KFa] hlD8~ (RcH&NjA6j@4"Ͳ'HY(-jM(hXjNǥ&Hd!6R$| HZˆ DZA%'Z"NK[$"B/6(?9C7/v#$q@@@! LKVD 1@uojd0{c5]Of |0bqoxj}n%~71#@X Ňh6cf9P3=fX7Gѵ {q1X8;p'ނmx;;p7}x{G|'>ćqIxHPKiRE{JN3 p)rebar/ebin/rebar_prv_plugins_upgrade.beamW}lfur~wI/ss)e]$Klm.- }}hAb6d1R4cB GTѴMG@lӘ&>zBH'=ԍ7(4>9vd̵HQ\+vb*FuQ7Bh+EMZы4ڦUB]!d-Zb؎V,(Ø5׬{Cw\/jnaE1Z?BaQEM Q4 ]V܂fۖM-,XUج[CoIAZmtI 9lQ8n#Gs׮jWObZD S7iA*eZ5.Va$_f6s10U"~(M~5 XM-5;b|U7p|Cɗ(TÚ!pԉZRlEu*bflr7׌*Jꆃl[Jtsjvjc[CV?Blb9MnɶN1β^iGʢx fA TSV*aN)kj6bBbL'd3UH USuu,Ru-Vds- Q" `Ucj@4-(1ܲԝ\4S[&=g`OTY=>i12bi ybd$ Z)O . U¼ШF4xfzfBfVY9!4ϊIroBH襚%!""ɐ O{x~IDyQ 0*OȢF aBL%3H"Pk_/Ã^Ͽ)rqA(ĸ ȑ6X('X>,!P.ccȵ"5%eF#90KFVycay-(_Cy dBt<63?%,>xlkLP,ʋh%jJDPt&. @ʸazt$>Դ!hP3mC]wH4JbIEzl+4c;A_֨O凲t 9$"96feͭms ٶjRl{Ti!D)ux,L-`()&Uz. IVСDA)ᱽ5kPN!jM&MpxKE~=U!dv(7-j5I\c)Pd4ut "H\$C25\||WUUqru"4)GAq JQF)}A]mQVrHggXb9E@Wa'GBA/݀&g!q"a%#6"n(;IlGug;h6YCA1Er"I;n'JʥHgsiLͳMF92 1\jUk>K{k|'u!αtk p۱sˏ؛ 䩀: x&KT<NDm ;Ļu mkko|Z͞80%̿8Qۧ[_g_~_*p8vF~r1tί-=soLL={|ϕKv߸e{z+_}:jwϥ'{wu>+W~\kOZw?܎w.}m|[?(hgO>xQ8?}ٲ๗Aιu[k~ο0wOw9qt _B15ǎ7K߽fTRJЃ|0od kۀhuuxtumAPB+I^h=uCxu`DVRVt$Պ[/MiZ6[[B/-M̘ {߰U!_X4[U/l*kYj=eYk5W F(fTHuƩ(59,wrg:c_]vឡ8bEP[/Sµ͂nFխHVW&_{~kejY\=%8u?r&N8I}x'9p_Óx 30>gOcckx^elb Ʒ;qߌ?!PKiRE!rebar/ebin/rebar_prv_release.beamTmhEݽtZ.~]G l.g4&GZH(\2evrbPAeі"kK)mQ(:sK+̼̻μ365u_m+<<&m}쀵m|( z4:ia\aP mcPur"﹇ϳS6df:eM'crŚM|dcR*bC6,ȾC|_r@b<: Podb3˽r3tE)R7P4hJDSUrePRVnXY4ajE ET-Y?%KTY yh²"V%P`Ҵ +1UB˚ "f YP82IjI9CN|=!'-;]}K-ɥ"B{}«CNlyGB5:xT}(g~>>Q}ϱOopf5:^8ӭ_K 7$=Xnѡ/~竗~:=ũtFu]G؅wfos;:N^=|Ǯϝ|Kᛜ4#Qx<-P!@0Ȋ/VCNy t/ʐau?n#--:Y^QP9Ay5}EDE"2ebL;¾Wzt0Մuf}$( alJGSf"Fy@_vd!Coms!sS)`-*CkRX_0](*l?'!%~ja$&z\ֵUڍM-ڭcAPKiRorebar/ebin/rebar_prv_relup.beamT_hEݽ&&vX﮴V7&3?}lv.ۛYfw/,J >(uQ1&RE*/(>X)ۣ! oof9xnמHpAnRã"7RvɈ- C:~bQqlDE|]0m;%$ @qӫgz\ME$kPm4 6b1lVPRe^9ѪTQERk o4ޔy&*E,)iHj,kUIW%B9PйU)B-D+(A V$Qիt @ )3&أ%bVAM=ޟ``A!ěI8! 3O>~+K䒏ܖW%3CLlq [ 7v/ ^-&ӣ;Sޞ^Y>Ě=;6m앍cb&\1?_ZͅAҳ#/oϭ?\OUة>x/6>>ՙq<^UM6u^oY^o׻?̓lէ/48&~ǃ{Ym u| uam훕w#wh8$ r&&K5bN릛noneus"lj ۡ-HiQ#ZZ$UkO3% JGP6V"6,&}`xm`dl i!(&nbvZ6Sl*oJ%>l[,-W,w [2'Ȟ;6`N͂[_!)AM!wmz9PKiRumD rebar/ebin/rebar_prv_report.beamV{p;$g>`c"gcߙ3Q6Ƽ1g,K-|']%PIKC!UBNJi@«IZIó $iWg\OgOow9 "mbUMJCy "Ijm!M٠&' RꀙnƋb聁CmujF""xYGmOѰ,*fJ+dzRX ݩC\F_R<-! FL%-+ͩJVtoSPET ˍ>,)"ٲ!CHi.hrEy!M፴4MՒe5n-–5Yiv5yÐ( V&%"FT16 E_\  : d@ 『+SF!!t))\YhVyj@h;'&0Jb;<`MØCN@&ΉaXx jc`Ȁpx`KeL΅Y=o9a d6@*$V!) aF-T.kD݉8e.|.ŋ̾8?<"8Tb(̛.ݤ!/ T o6h9bSY9P(L<X  Ĭ\- .), 9=!TD6 TÓ2 혵0\mV Lj(G`UtdP _yp}*V1a|PzuIA8Zd>xS$AT6Dɾ4q I uᝒtOIY>Asd1ɛ2LRꍸ2H*jQT.Ly0.bZc@E1% kv1+=q_V;!I@ q^d,!c$U*QܝUA]\]U \FzEPc[bPgs(d$5ĝc݋dQyp)qorvȫv5= C2!F4;膗szCo]i(P2 pH@ yږ[n[>Ⱦ`l>GdۼshB,d9\ފ6|+n^B~wv Ѷyk_EMr&go6o=aAڵsؼclX[ Sc/T'O~{j׮c޺b3gJZ)8JkϚ3U(O]L]ygb#z/S3&CݷSL~`m\eq 6o}m7MbY[?'ooڿV7;L-]:mc[zon\حl#/FN~/se}70g\1a`d<>ߚן߱%1X3U꧙}*꟱voZbLЫ#O4 h#lyhq5Xxxe@7J-|ƴHP:UnVTM -פ%'.4q]j4>GY$1VߢF$SI/SV|$ > Ƙyg@eE${2>HTKր [1HYӞWf{XUHHY֟E-(J)J~ ҢTPpnDYxwyy睙~tw/v NQƢK&E."v|٥؈z,Eu/gvefA$E&2E,$;78zsFdjDDs->RB1KN&^&vLHђyAb#5C+C(*[BhH# @A`Hf!1 4MK*)@!*Chز`<5Lƴ^iaԥ1DHC)pc50Y,IAN%&1Ua<|D&B I\ !cڸp0lJQC0':7jJ1Ɏ]> CA3DЖC>Cc7lqhuzJxrx{=-:>ne Dhxkvޱ&u$OKGs?C |}Jn2_~{ǗrcWsOmW[_o:KwLGx>9oWSǛG]<=}a}9o^<_\Y߱m;v.Ww|0Yt%ŧGIvO+}Pʔha#`{r Ŀ8{WZ*p9jfFզNr5Nj,P96wu-nCljfNDIŷZStZQ}Т^+{8%[g۰uVuZGV&LLϵ%e)!Dk!,j'LiU0lѧV@(vp3-C}Snls6ܘ* n,Ve}z=ɞ׻`2EO{a]0|F{_}~[P.Yoʷ;YpHPKiRk 20(Arebar/ebin/rebar_prv_shell.beam|X[pUVq[nE+t"'!Qȱ qNG\Cp.,eItKvÝ`%afw}۪ڪ7j}-'dRsNmw D"mؾ[ի3H,D9y/WfsS.knŭkE/tּ[t<9%I:ֱ|kUۊd՛sUFeRZ[͕݉y.Ԩ%٨Iu{%JId0_r*u9%ׯ;V6vG:*$9N8^F 7x:X/WVkh;FMYV: ͙ '93YܥuT#1rwQ)j%#N}2_(78M+2L)T+n;F=pzxS/ vH]tHͧ]T-s xOtno/i?F34Ҝcnݜ˻ )67X#;x~qjJG"Wq$z4v9Qh~3.!WrEFl/BK!2D\<%[gKI̟ך`t^{^t&N)*)DZ;idѩzZtFK]50& TskuVf&ݲS8O0Qu'dy] rYv|(+ ,B`ϒnAuK9F>;j+8X; iℓQ:U_oK|17Pyǵ[nNTu*Vwge $D~-J ^|֨~n+P=bH@)Q $q>EUD{#IѺKWb乕RC=Kl⸻|L&i*Lv9W:,=ֲ^;sa-ɬܔ[ʵJ]M٣MB ZZq4zm9]dOB&~\ޫtR{CVE8Q_"Fء*Unt>Q 5:(UvQ,#2թmu Rg3tNЎV kVF@WƙSƹ>6z~=Bs)oC n|]vC8Ց=#uNe6$NqQbK} ^S!5'2'|QW :t7g !GM&%UMTSwLh혟߁eOsl#2>.s:bRzd5#VDiPZ&ԛr a#2?36<-nbJj۱gp4K/[?ٿNL, < R:iJ)Mפe~LlcƙGC'u 0K~IK/6e7i縔E)@&c'J6.Pea-]Qz62reLt|r&}X̺˟ 8dȄ2Ҏ/z/{4^hCXɫbtʣˑЋH9dܩr2ؼKb>:k@eITd 3YFD QnvCbaZ`i7Xq2;ꘙcYCŏE?jeM5D.ZmdmW kF&rRA S9pCPjL5o دɱdigkj0ȲvTڙW33mh(gB4 ﰸѴے}4u#If z/4G?a7-j~smzu8I03j44Sa[drI`}Y57?=;h|}ըz;\3w\< ؊6|og/CP~PV¶UX]Il;u*Znb5QLd^¹M#<0h"{U :I RN -X8Rv|0Ra(K@4W*> }i+I[OB͜^O_W* M*%[iA>j\=/!STwa[eU`#CVCMx X  kMSSJyI~}m4Cd>ٕYUMQ[Mȗ~oW;3֫tCSH lF9!$\ ICH-Qښk(*mp "2, ^J])!bvkvD 3l)"hdEBD/ɍHva)7$Z&a++= P#vK7R -0]*1VX{ h~ ~Cf1YuPC#~_r^,>ڎ.T/v v2G$ʝb@H#x{, &m aJwfP)F|6 0@ I~ҪoS 4cE>Z/Q'^-^ބ (66X%5ÊWVJR jރ5c%BڇBKD}BzNs,`95v /4XN,5H~R4L͊/"QQR܈"GA[m5v 'icd܍DϛV.{s,)Nԫ }b\b}G[qK\+u0?~Jv0Ͱ KHma O)#j%[,4tX.VUL۩tC6I!n<,^sbX t긗ZJ񻅸I {Enf3x=.]X +Mk[ )ԿA}kc0-E7Ewqȶcҷ 6!|A g Ha7~_꒏B]Ko= qU6O <tY1Vy eT ؓ0jT`N㨌!`OWŒ7o %Qј1OCFFG$X@lo س$sPr"`B Xnzu4`/AS5Gͽ&?\M|Fa+>'|~>?O~edA=|} JaO7/Yy{0_@X}e/P--ֳN"e&YLi|6`Y#}s|Rle,J_xc#q ~&eg˫R9H~iȋK0LGv eW}P>r~ YoB֙pL7T9[T>C1UexZ$&|PypbWi$hR~MKLk:X 7o+X@ŷ>5)|nFMV4ⷅ=S4Q 9HzVػ1FrIH])*Qw(OZ$oZ? > rH}Dpނ^s}Beqk+=8>ǯ o򡬧#Ƿ~VO'o!#l|&<<ߴ(i߁Oο0?ϵ9s})JK}aӟ{FW l_ȑ?L>_"/ yq=C[Y7t𙝼M 2GvR2z&`spB sߍvFy) goaзa=,#T/ '8߫W T0f" VY4D 8".T\UIX,j+.X].jjݨh7TD@n}G{<;9f}w{&(H)PU0m9ٙ"r u"'[HT1<k\ypYNqPOzWS!r\BPL)吮rA$A橢S\e@ޖr9(xq|P(Z#`e29sԩ(9HPy4C: %^c=7 "=3R9S~'BX+rH"gAG*8))!^ .`0L"eJ`]x 9&[AJ#.r5,]9RnBEZ w Q*X~.郲V㼰ͫq8nUsR )J$85@ȉFc0ޏzH TۓQa@VADfo"0{o;l#ml6ۨ$祀KPuGBJ%{DPEPw2߷z ;+@衎yv`a#D̃T>+e wT.h(Sb"5N󜠞jgH;s q\ 1_J842 ynƛrd8O],A%Yڈ;Bü&5IH@FA%A+ܰ0o@yc^+nS87/[iIG)^!ֲ$: z.m)B'f 8*b@OdmKrB%ձNB 5P#R{G/RruJ %!*g*@c[.h˸R:K\k8c87!jA cP2Ẋ mG22^[M.dp,.!*QxJHdRJBB:#\@ 4g#dVPK W U+5zJEibZCZDٵDX}Y54k3jvXY"%lRO`DCv}~S o-TC'| 6 *،R~Й@l*O8Qb-@d,ۀ4q+O%rKBT68uA0AF@P<HTID[Km-:8_vDoPzBР֠cM_/լ W`&Bpe(-}'!B{T0|ʽٯ ]>^ 54}i8қ-$:YX*9/~`{* Dv߱, =tձ!A)]8  BS')OEyGPH㼵 p j*~c~KBl@vOχY #]85\OJ6'A-9Fe( w ` :v};A1.qS[KQ Fù(@ O5xDQ&dRLT#~2rp\DUy@D.2xW`+Fo| ƿڊy6D~/@;E1`~7"ȇ#q?DR/://`qnL%"+"WEuCMɻ{+ypC:쵎'-<Iޞ 4楒{a@wuH&5)Bu6 X K3^qoG3^qOO3^Ug^3;/Qxd bts%ѩiAn`lڹGw1s+KÕ{q7|d3rW8ut'}г39qz&nuRq['T}Snp6a6?I7v|#hPyT>EC QmƷ]Ώ<ιGsCڋmETӫlqhmκS7;Q_|e cbl]ث|iB'ն?=x\YxI]؏˟/r{| Y8Uu㹐6הht*?}ll5vl6UqO+U-^O/nn~rOڑ\^Vaѵ T˻;ND{Q^T{К[Ro9:,7{v4ѪO ,LZa?9ٞyQr&[PjzY 7'aҒ> 9{e{<:ײI'}Ǧwr(g`cƩӿo}k]?XYwuG˹֧xB 4r?@BY@KZN3,s~[Km?,..- _xifЮ<*f?5z\6e=<. +N ^Ũsٗ7ܖT7|0OP+ٗ [O(̳`-u}mܪ'_Y 虜dx9'̯m,m]7Fp M~x1}q⢟en.) g)iлȒ?s̕WGZ񼐽av{>+ycúY=9wT.Ɵbg?m;uvOqόΏԃ,hYV^7[yӂ[]W$dLoʯYbM*G$ǮJ=ł g8fI=r'U'ܽVBٖ =7Q?vE R0|M_&L_zP+JR;|[rxbY;kXCaZt|z\1f5#=\Bfe~ԍxĸnfn`m}w>#?ldS?oٚQ/ͼu->ד7kİʆCm޻Dێ2 $6%ɜ,ko$+'ALKB{>* EC@q#MX_Vc}UKS3k15wxVc[pD2/^

s׀a_\}HuE6b H~̣>̣] =r ։101bs0AwUXf &/Ƅ@_`~. 4O?4ɒww]0 ˣoy b| [ƌX/o$0zѢ0±~x>cTý'CNXɠG5oۖCm6`栯#$̰ aKFD%QaC}K'#t[Rej3C, OUOfI"~i!pdT4%,6epj9͜8-ht))>#9-bw(Z2&|I4)6bh@ XKŚ,4K bĂ\jE6#o6ƦS7#-!X->qh2J\QXIxavIsjIliH6'MZz'-t΀uO8SiB$H!fZL))~PC6!"&V U :+P"\i ݏMk$z BL:NJϠgҳz=^@Aы%2z^CMo7[5kSl%$D^ KఃBM6Oek؅/;E$kʊ?A1v1/+dhJLl] rMRԮ=VK FoA!jDcׄF:d2Lc3\1,aYJVc5k&Zֱ ld9l!<|vR. )=SA%H8'8i.9.s&q۴Nx#:yJ7饇wgfQ3&'y3陞^E^jcv9\.p ]bvy+8*r}g>F_%7-.s/xFO \Rt}W k@5G5A4K&0z6AyYO*҃T" H/PKiRwDrebar/ebin/rebar_prv_state.beamTohEٽeknr 4Fjۘ6i/IG[T(fowlnogK⇠"E*%O~)1EQk% b@ٻ="d{޼y{=O~+O8x&FBdLȎzȰ 048a蔭*Y-eg G\,2`ShI:! 8SZea5]y5hm6mGE*fЍ5XKsE<E"1IlH'DX^ xbD@HD=KzYg〇'1v7AR/b ݑg<ARPpțbs#XC`m09;'F?ZyʕletM+^Hf޼auf^g[KS_[:Vt u|㭧H _ޖw]I/f~Q./Z]xۃC,vn?^sϿץ usy8w8yjՇt=ct/N}vo:6_AiP@FK_2_c4N8,_}mGcoe1eyR4@EۊUƨ.>R4ǻX#{#v l9>Qaw5d ,$&1&Vr&oZ%h#脞1, YXvleҒU S Vu}Q,MƖyT-Y(B,ud)md%ss K5^kSQ?> "-bΖbê%1Bx2~ H0 X=\-'p۸F!{۱ PKiRurebar/ebin/rebar_prv_tar.beamThEݽ*jѥkkaz=cxݹfٽhR,!Ep) RPT*T(R& QRVۣo7ofxxΌFwXг feK`tb>~h>r:h {ːL=xsiMe+5FQbj4׬# U;df khzk_đ`e}~-%IFrR(7P4hDSRrtNI{J( #À+E):IȒנd JT4R)/4 XPUȟ4,ɏ!XIXD5HȦDP _{9x@H1'q849džgl0ѷD\5y٘Wcm)އI>{ȫyx=uaDžcOe/-.5>f^:Ȃ8Uu,nl-|Y{~_jl~RS=?n+ G=:2܋Z|fv߲/ ~G;? }O~:텉 N9w|]d*{@iD $; }?}[~ ^ZUMq@w;ـjêRF'`-G4̀n䩰jX),DH@KK 3#b~j 1Ӻ1{rXVI5 bSF&1+"9bR*-($ZŌ(8&iN!6V5 1꒹# +b+ej++Α$Ng*EfrEevqƐ쌪$},5i-M>·2J&~taI@Rq eÎUCSX k nG򏋥 M!" J_HrA ~rh-&X,Ż/QB)E bݡ c7DB잂\/l/D1~]NU 65جn  F8B<Ib$k (wg b`kvOWi`M  CNwr&l?ӾHEtY}#̞A"\}1+$x0T)$ D;^\U<5Ww!ب;D;؍%-$bBhti'I֏Q[Yt /#HKGhZHdsӨ#\-h|0"ea;Yr% x&1ZgHt+`dc 61 *a1*!]0(}u9z}כ?8wΆ]5?~7Wd랜l=y\+jA=8۴e:~{1<%>ݐwg}^׹'Xrx廱no\·fyOKm۲ek]hkf5ݯ<‹'N._llj`H>o8/WQ/D_c&~gdOpɀx}W)|W&lT1sf2 N]膩Me8g@5:f>unȒ"MظHV#MG3>nxn/Ĥjr!qi e@7D++ Y|cH)E֠dICvR|&M) 2.XdcVӠΜTa՝1zJsdXr o}m[;Zro 48V\L$-D6:MC/0Q`2Yδ3L'ŬbV321 `swPKiR@30{ rebar/ebin/rebar_prv_update.beamUlU׭onU@Ljz :`!$o޵z;ߵH I"@J*hF4FDJh1!D0d|wo'۽``qՔ% իZY۵ifE:x̜G1]9ʪIuɐb68e*UN*-2&k`2`y U'`[ҊQj+^\;QSGa;96tSV+VfdMXyٲ!;,cYHP7N2fu΢yNQb&[tiBLH4ӐҺ\՚r&JbxD$Cg<ᅈ+&tx"D/܉yP%q ~1b=OEчx:=Ϡ30͉-z9WqH}<$0gp5dp!Zv[rģb1'&tvA+}هq1C& "(0 r^O\= D+&Q/^/Is #01@_b:`g!@.; E`PaGĎ+.Rݚ8![B&DfI$"BX>~7U)jBnt(oZjwBbMޢ^ $05 0sA&7z<O:U1jc5q}\>Myۮu T2y-رH'%֎i?g浼-\YYwŽ/ןf+~}ʼ# qZ^ ?w":m/;;!3O5t ttiWG߼ȫZ|r;W^@^-&'K^?9zxρ[}gO}p_?$__S~MKy >v殆ovʶΙ/ PKiR$8r!rebar/ebin/rebar_prv_upgrade.beamY tՕh{i$NDʏG 8[׉8ei(5Hr~4?$6d6Y vwp  -l{`.Ό.s}}wߓձv}6-yuf+er.Mk6+dxRRTf5u4T\yBSyeC"LƊ vu؞TdJsXlV+PdAJ"gx6kpY5gjLL##8k94;乤͕+Z:8p|HAvt*QPӅB>ѼHe^OcNbV yCRh9%H*B:?J,RԚ: ONSxS]1L: > V̠+䔴U^A7Um$,܅pFݝJ&d{'x&ʧFX%+Otj_&ab!`*)ԂPkDYХb Q|.ȁ2Mū,3ץ4(Hkj!83jkJYV5_flY!j 錊1IH4 Ǭkk9DX6S/+r+y c9r\!?4&MV"/dc)sJ%.X22hpy5K+B J!S$0(\J@Qv dLSS1J/ƍd#z2ӿ}j鄿Ooj2| BH!'$`AǙBTՎue &ϫX&f_W. lFy=ڒGLi_$9?rT aN\~ OE4!np7|vS(TR$~&.N'L<"'"Ag pŹNrH9]4D=:{On`HnGvSPb G!*m d8a L7́ Z3u*/!W$xʟFAL5;h =| E'~"Q`c\`u&`,sN'n<9]l#Ax$I규?vsAJ>;sxಬds . g ]WhgQxզЭNb'-l ݁FhRZ6\- =@c:WF&Xc(Ʃ9V8'M:I8o&CQ9"2\wLnVXAR2I' sUWE6NL'FadgTAưdb8k"*!Ste/CV@0,ЧC C|QĨA!a> `UʚYxLbn^,L'!!Gω\'a +i8Lp|M qjYl,,9z(>_l5/t|:Y lkae1);Wr68l^dኻta~dJbOv}ͮgupZ jp  tcu` JF{0Ή' |-حE$ig2a.A)MFQ=i9syDzn7YEǍ39?Cez(C~]ɮ}5ݼs,Xt9jbo׳M,_? !_\gHurWS KN:M?kJI5rQ1R|A8Cy 6E^ ˄۶z H6l H +dQ|0]&A0 l35C+o$>dnľa /CEE}_- fzYmlIՠfA](rQF <^ fӐN֠a:Y n}T{D'] fq2pgV(fd1:amIu-l8>{`o|: safl2 Gf o-bK߆^l11l >bqbfI?_HL'qi!>i:n}|!q +M`YF,tQ1\aeDqA~]T88Ÿu)dqٰV:QxJ7~p#{T߉Vw6 DkxHq#b#4UZEo0w~D"%&F`d a4dpG75 w*5w;M .ZW79@!+)mӏS+^2(ApBE1ah0ð=!~/ _1arƽ|>1n.WkB~D0H)k\Kp%.4Ө 82!CF~o\%yh|=){0F0Cr=|R8t$ _ hoc!#$F!7oƸ$CnSj_ZKo6Cn 7@TZss$ O.B1ʠC[}Z"g~4ޒYs(,=™m% )%%i h^ g4uf϶tP%G[s,\hĒ/V[Yo%h _X%x]mB%(_ m%oj#J8w؊ZsQg%JK:kJt֖ijDg}NmC>,[J8nyUI,'Jp%+g'8W' ++Z_jc.{gs3;mk7޾h'~ {G{罖zo㩕K)ʽk|ίl]vֿg}OǏ;ݽo?dG~I.tSO<`kO{gmޱG[ēomBjK>|;~COz86ImO \:]8MokzOcPn:j[fEo]?9'{CR[߹2«g/;NO.g:Hu?pgP)r_=zO\[y?7oj8>s<4[XvX:X3WAs@[L Xe5XCٮԋbwaaO\GFͿ{͟/:7|};Q(;)jI)h K eTMєACc~=3;wF:3P7uY#rOa[cuD VzW@(zOLq2$m3j,H6V'=5rhI?f{B6K xBISX*3s԰-D!2t!Q}-c뤺`]~K,]hi8ZrlR3tە=Ÿ:VZZGDh^Mi]B:ЍnGci!0UifF}t?=@ӣFz3~Fo;]>OOS,}/Wk &-KG?_T3դ㫅jwX=Y=OПCC0PKiRƨ~+d!rebar/ebin/rebar_prv_version.beamT_hEٽFQpՂ)Jԍ!b6UZv.ۛ9f.SڷU* %mo Tjo3{{āo|=u`C/=1;t[%-\EPRh ޥX2iO7$^J9b 40OFE "i}.:OỬAB&zc"c;I$ fރGSv葕A vJ?\YМU(nt`)*ic*9KJ)`%hVfu`UlG@3mX@6<3S^cҪ`1UZm5K%tT'&aH '%$$oxbvbc$Nd5G6S]%k4TvV7NL j:tWՍ-&'N#z3W~u,=` 8u汕G+֛ǽuo^w2r_{|}uGk_m;_tk=;}:B+9{']_;Pȩ]0лS W/WO^V F,];"V ;x;G  Gpyyf˽7fx;%("4&]\-Rƈ(p,b09qQ#t(vƝ06jJ P> a33R0Ւ^3G00W\(ks蟦M 709 y~MY_l4A; {?d(s)вZ:L=K ƭƨq3wPKiR[J!d!rebar/ebin/rebar_prv_xref.beamY peNO2C: 003t@3! CL'd23tOAHA׺p`ݖYr[Q]]On9O]oۺs9u [[78n`c}k}&q/r72I:#z8Z#g1͈ SpBZO cnDu-&BFpa$ gR}y##celǻ)]3n-[] 6o"-`<4=1RXa9gdRi'Hk:)+G2aٕd2Z '6ֵ͙tDẞo }Qtx# Csōw ɞ$1*i]D9բp$6l=ZpɘХ4x2L3J:3) 9YHsdd$1tPhZ hz"j&DS1A!Ͳ,YJ=bg=\ĜSS%p!Qzs mO$V"#S Sz8 RgJځ4'S&1䋐wF crW}mQؗcL'Zw#ʂ3cBe_24ƨ%cE35k؉1iCN4ȗpWYU!Tl>TXK5'8n8zk/R^\LH6=G0DRG- r@`( BcEC,$SuJ*ȪoHb `)d^1;xu"QP(>3xT`;$z\Vsʩ(pK uXOSQU"G|TUyɖTW͓[NjqMKN|@pTouWoui|tkRkZrPCҐ\|p}4䣯iRW]%TP{NP'] 9)(x &_%DJsY#5 y; R0Id-yK9R!E(%:B<$Sa~ɮ)qJbP<Q$$Ӑ44lX$9@Z<,΢ސˇNN<dT4y 57L`ZwY*N& gv 94aO(rM}&zxF q*O d“54PUOݪߊV:T$>Rd4RyBZ 9Z,(A)6THmH?TLmZHzVM2>Uq:,N< JnMm[UTuvkb>n\)JJ"|Roh/*ʠZeGg.D"'| ~յ$HVYR~Lɗ#H$RmluNjᵻ'5 i)q(l $ʵXmA󗋭~WG RCPv[۫l;փ_`W _j![MhI5cn49D@łmllhm!vx J @=+hDnuUpuղZ/oE@)Y*c@o#nctTԶjq;۳}d D_@܉6iO(.@ a7W&cP'Nz%{1tm.E%/ݮq+.uî^'Fx,>XQ/#ܻ=_RĦT:Rz=)k)SAڔk)%lJZ4P4) +<ןI)8l(F?@ $'N`ϓntDAzjĄ 8Kӏ (\EV b nbܕ{ymG[?Np nELR% > ^oS$ON(_q낀8ZfQy'ƺjtcw(uPyMÖ| ΍o_g} ҟƓB|,ˉE&b2'a ~e,7`Mez(~gb1=g6ST7??_8.g<!GvI-UXK\ςW0<.L?ߘbmc=X{~ >ՙ'z4n*/`bmX9y8LeZs*g9Lڰ3,6Tϯ}9ncr<1[:>0 G3},~ٽO0|{)bzvpv[z31?yoly-^i [7Գo>67c_|ok;_L'?i\~}_ug>oOE+. hi~]h-ʽ??tֲxt|Hŗ_.^mTm-l|ǟOw=呋 xtߏޞлr;py_9w/xӛZ_ԛodgؔsOϸ{$G.<^~h~sg,>}EKߞ]OWgpχ>Xݙ}}a幎mǖ|3mܝ]䫗j~τ?~],~'f~ѣS5hϾjֺyu,>?n2|?Wr~ƈr?6>wʤ[\ƞNQH$g⁣OXvΧo_š~`(S9^rO|?|.|WK+~;_XwCO,̥)?oAg[\o~g?7;8:KJ7=/Lyu>t?/4/}P>Yﺽt;q|=rR3I\grIވ1[td&6K,jdz9tֶ0fB|kbz*2X g1Ayj<.tQZ%FX>LЌ =y1N4V?^9wGs/eQbq$Ebk~O -Iԅܮ3~'BL,q=~RH&5rJ﫴>Dn*N]I qTHG'T:ԩfC.3hVCOF1-ebUm }seI*+On}5?+DacT, b̯sFʂ,(3peeea#DrX D F)!LIzz0qu'I j#h鞸}*Mcm)e iF6yQC$D9UTSC4@3-F0Ĉ%Ì0L043̲K,lq1'r9\|!)Y#|)_BbOR&A[xWCvYcy&Tfk߶-m8:7PKiR< $rebar/ebin/rebar_relx.beamY tպtfNRJiJZ )) ӤixiLҴigUg>qԋ|<υ |x<EP9<|d^]w-$i\IuH/A Т=Zd&VRHo7D$<^1 ľ/(Q1-؃’.W,tB QO!X* &\4d XwLM|+'/yҀ7MR˷T2Xs=xR/oeK[2՛67w\621vWl&`R%*}TglF]ke&˻ul~9|$Dz@.I%:v=_ec&D!5mM|2łB]X#9]j&kX7+48GV효uǻyXfRsb @5=FwIbH^t?Pf=ObWheF̣!M8.p$%$2 VH<64)fL〼{9.iM5j(6 ]˧e`%b $@{5.ݔXI0 Yv-۴2UR,/9e ٭bZ;G0z5/>-nW鑠rg$'$k2y8"&c+ 0U dbib gp""졝[|rXեM&p{=T{T&kPt7M\eI%EnNqIPyU9rZ7LW䴿#6Dw!y@2AF ׍5+Y18HeK#]&ȇ{;X*i4R`kǑ1cXHC$0@5"}?J@iRR,SCj^C 2Xa)egLt SLL)W9[ޤF+eP%= ,Ma>2p+#`AdWppn"tY53 lٜ-lk锪R1 Ue`Vbgb,J j NfH'I#u#l#a6$]Z8mZ7GDOLY4TLc<8Vag!uJՙL &X6p:Llj`6X;",Y2fKapHp$R;dJ:OŅ8UUQ]SI$2(XnZ'4.g:&o+C+UNY%8+Yi<(q:ʖe$±Q nAo2v\7vZ*z03FCU%4T" & !25"2 5F o .i h43cI܍)̶ceӓmVJjAm~Tݔ+)"Iw \ vHaBdV}'vh^&)Sӱ]`lF`Fr#tDwIwh4:[<86l25*PM2zP3D>:2. pFZT݆V6T^N7uROJĕ;!Oк [#Ԛh$ X3j%>; q'Y#S-`. ƹ80|kpA~(is8kmPf@n.!yvf.\nṗ. ~x[vdYz:djj`W%r+#&$&>PcLŕ V ~2 / aej5TB f ՝,JPHh~roi0;F: u:adHSyL`y=R U-R0&%o9\}aVKJR}:ǥkaLPpz\09s: c*S3T1nͪ]ڌ)5bCZޒ&r3/ַ00tRuB— *]s߅sνS߾: 垿'>տ;FW߂|tYٵc~rWcsOCAQr&eq{NxwJ=ߘOg;ǝ[{\z}lƑ?|ɲq[?/nޓi~+Ys9^6W71k{Q6܁5-tÍ_9s; V/v鞼|Ϫ/`VGޔVdz ܴ궳ݧ>ӯqY:nu<0w1c ۷=>uޒ}fBOnwg}8-X|_=:-u,k3-c=0C+Zl<3s2煦GGo=Lg)~?s&m։G<:cC|sڿ'..zۄ] yYzHv1Ҵtyhԛ7z%n73IW}ݏu'Nm(hy夿pbIysw;ٷζl<ѵ;N>W21Oٛ~~[~Gd?>rښ;:-ʰGOSO q_xlΧwkg?n_?ngp#Aiˆ5nmc6f-vغf)WGTwmkxvWc5~_Pѭ6tk/ [1Ł>s3;*sKU>\vsOs[tGW+GM'hߓ;FWO{d]Oα*jeidᾯ :*nlWwϘ~v`wX1qBN͍=yq?oYݷ%kqwuٸ7kWwgo/180y_/yt%r|b:}p#[ӿ?ͱ""bjKo=-~ڣRG|uĕ;dʏ3JW5;IrONQy=$ܱ_yj{nJgb1eĴ4 M%&-Xkڮ9/˽$\Ս2pDH8ԗ׃Ξ|D6$H#!!9ILjsoZF)-JtЍ-WW*K)?Kŕ1MuH wsĕ%d\ F%aȌ,YQ% GNBn4Q#Q jC|ԉ4u^C b_h ݈VhZ֡vt7V(ݏ6Гh;zD7>zFGW_k:ΠЯ4}>BгoPKiRrebar/ebin/rebar_resource.beamT͋EݙΎff:Md#B.ɐ8u]%xfwz:;KaI( *Eś(('/AͪzկW8q/Yz~)a,'];,A ˓2_QǐtgC2WXB c'|7!'vLgduynv],JF0? In8Mxҍ$qE0Q_&P(C= -c]mNPl͵ D&3K1-+Q #5K:*f>qmNMe%˂Vkv*6h!TYV: ZU^LYGSsGȰMլ-hO)уRnDqׂw*ElWݝê^uis~gCϠ{'h}Y6Xbens:,2gN[6}) ,R}GW侾'żY蠰C3ү ]+"Z)UA"s۹u:Ӊ7ptpʱſͶX˯]=W\ysс~{;ܺō)0\0n3o ̶.go]}C~% f 8qv8冒C8g܅[[bbHZ$DY,M ӍM6 RJxcx_D84Fęnn@}p2'!u<6pGy$e@.FfR'NUMCӀnĎ1`qܛDxvꅩO&QN+Jimc>(=] y/_b-Ok]i*{=y2FSI& 1 fe-f6]E[L(z![)5ܦ[iq-Ε s~4)wյtmF iklSuUْ1cr67P<^2!|{ӆvat4͝`Qk׭fzq(.FԊ~C׋lZ}<խmenzNy"TS]hUGqkVߍ,sѿ'YΘ ؙ紅NU*yPjdZ%{7(7LRЮ]+`Y֨=Cw.$ gE`?$ `[B + 9e*MQZLE"ؒ'X/Iȩ(8˨يWb⏣%(B VZH"hTV0jPTӌ 3^E)I(lNM Wm壈=f<_Ii?IjhW?/8@c4p ]}u U O{UEAH^ 4~o7D_B1=(ŁfZB,:lP V}فoW!*% qgARGQշ |Ժ[`Zeq@6a`r,m,xj3?wq?8Hn[(ـʀʙD&d)鄬0Q{/^,)&ԴA]*aQE2DWVXL*& x|/@=~Es5awӢsB*Id2L:Ň^xZe@Vߡ ϗkWIX])`-ίAȠ]z\Xq]:Nshr | p)-%f-'|HHH{IQC=7C潔$KK#Ӣ닪a?)ڧdPD 8eYT80ډp*~76s ~N {r,޴LCu4hGSQ82=Д6H!-~~pb.@6a3ÁL"=T~P$~(x(DD[ ?!z8L_H&N;W?$"MWbVk/)gڵiW k֩1qBc=q~,co@XbDq i((v**(ʲ 6  mp10cj:7'ޡz#hGd3WY<Nz,\]@4lPn炓(VT~wA}Q^+a |2'<2OcXnPkL׭U ! >bd]Jf Z_zsq2-l{-ekLש#N]'@:@"NwM .$\^ ⴶU..Q[ޕ-}ͱL_K:K>~.擇ș? Ww>?F~:^W+:WpٟK7^;қOxڹW;/O2w_xg{B͏~o⽇(R Rc^c96'0FXxdΜ#0Y4ręrG6(Ue#07ሞY k-gWKʂ5-bi0 Fh iЅr z ,j~-՚e4Ke&`լBîP2 FQoy@^ytԗذ2qoCPK_hdVs5t\Fu[+>I]IH$届ָ)%Ydbb:8r.`z[x6+9>-2i;SCual#{zܖ)P%SJe@&*J>Yue%\&Ѣ4b!%Oղ tJ.vzKV+gъS~f=ħ-5e;Eٸo\mB>ji٬k]ᨧ{Am(Fm$>l58Z+b!aVa d@؛ҎĖr%T"qWz4QÂF6*t٦P'5KJD>QD˸ra=̷byeZia0CWگ^KX@Rýt]e)!1,[a7m*Q  RU17vwD$_NtC)PH8T]h38|g).Uv(Cf @k޽fMYQS@Nu躥#);8YwDuxaGX!LS*i/jEV{R ,G&W'):~ERXTfEcu 4!=q`8ңWa .,ׇ)>l__%|.l_-Y-l_$pE$ ‡мqe ;"{Yg*74W=Bl R=Vl%ۇa[WkQ:=iy>']skarSSD;^6GbQJZe3!6C9PW%oɹy. 6S.0 Y 0~KCȄ>'Q))?(-ZDה.Mq|! ճ ۇG9ⳝ$ v>SK&G1\TFZ+,GL>RsB>Y+ؕ@&_ .Z&u]ERPÐv9]@]+QJ v5MP\kP^R'Dq`H כ|6\@FRlp(76 o . Dq(nl=IA&oFbKPk%͂]啸oza]4`v=K--?\  pw$:-II@t HmOzHgy^)->R&?6~l Eɶ0h?¶ܘ>iNkˬ^:;CJ0.'+7a$_zeĬEv9|؈N<͋QǑu309h 9*-p&YV K%s pڍO~n6x:N^v8[l{#DM I׬0IE{Z*(K4;lӭvwu3Qm=ҭS,%ݭNʰa|'lZ,=E+X`}&R릥]nUF~jPX]& ;m*?/{@[!x-l + ~D ?]]m~`_5nvw7JRҿ k=iƝM;} @o}@"죚ۧuP;%ge 9m;K .esmwK!=! $`$1Hp:r`w@>*ݐ Q?>J``;!_3{܅}Y[;=` ֿᾊι$}  v?n&?.04ɮHk}waS̷ݰ|z 4}mI/;D8@軂}07&$a,4dF0xγ!>z ГRh?* ySr^H{d ߖ= ?#^D% =G`{a disnY=1ɟnEO$N3Sj?Z|AзM^p^hFi8Q25xхxYQ񘬨zLVɏ@|yYAl3{}_xy)f2SFQ&ѫ^:,_BqߠaPWU3KcǏ˔5^;Y-5pP/0w3As#`!GQy9<.ؓ$7L~ bkF)cN~S[&o$oJL6vInmYIrB砝?O'ғ(1 ~BFD^ $qǰE@!Ɗ y$uo&,DV4fYk4gs~"'a2UЊjƟ4{H{nRrD/Ou+P=&WJFѮ=y"8 ~?N?>H: /NC/5 :  BR!oW#~⬳U>>5C7s ِf͡n!|H(_iúomfa{֩b/wgEC>b/dO~@5^|SI#a2nCݼ@iN\ 1'w}&!8Ia'D8zpJô4Lfy3m?etSȟuZg 쭒ꞎLoMAHC?V{TT>ggr`x(q J|M#0gf%52u|a/fRYiY\ft0Yoy+- jB^J!\Kzw x+nV.}Rk)zaW~{$\U!jaUzSQe ![)qFo#zNDޙH`]M4Ex]ְ5lKѪŴ{ڽ^ZUCB:XU@"hg.0\M; 30,&/IOrƻIiJEJ*bU,ΩH ihyֱ)m%ܡ#"O&0;K]86X\B"/4JsqXtDm?@U͆#Q'ZIS2DX!ܠΉ WjvV|UoﴖNtxtO@LYE+A_͏5%s7^&kۿnh޳ [2^;W[C';4)t_Mu>PPt:iڠl?㶶ډϞ5Oاlz–C)TyG݃JSe}^wԦ_5zҜg9ѯlx-xrA΁u,)ӲyksQiiL:w1bGոæjI;xg^vg::.]x[HX[&9{siO5k/*M;q;9`a@IwO޽jnmQ,tϑuSW|;wo='rF䛘 mFK7E}9hq͖-zU{*??\5/y{ek'h7ͱ?r4~5gc13_I+2vaP9-x ")|gWs~g{Όφ<Ƕ(!o|w-( oxq8߷ŴSdξM ']hKxc$z±dyf-g_G\a儙.GҡҦ2ۇo/ :rhtf9T#J;L .uJUf&INwE\Y[gv,%"nYf%L&̊+â9#2GGG9eGt_ h`Ӂ['<' U>1=H2%O(㋄#BT+ '-u,k<.NILO,jL$(N AI/jLt5*ի*YΏ.+qP1Tɱey\QEzXDQ0eRh T&:*ƀ|qN j A&[žbzfޞ=Mp2qxC\ƕV =L*BϦzO.\K vHir %]‡#)Q?vTwr+9)6ϦxN pW~Q`Z.@ZR=Mcs]}ꬉ:gdVVjk[?W|RVׯo8˓+ }A/֍?ҩY2ǡ8χlf܌d1gOw|O􇭭ol%D vo)*^7.6] ۞gm?l0Y6JA7JS߇K6hQ`n{UokU(9pUsowƴ0, f8fTM~NV\ZcpG{~;{oϷEg}| Kd;bTՒs3L6byZůXkѩB)9-֜R*7Jn:#R NZrRQ/v2y*Bm9Cj "ؔt!|Nѩ;$e={<-6(e83r٢SgݍR$jb^uʙbarT:9b4P;LE@zw\9Z-L3Gl5mgՌ3-77i%L"[7QQuj Qr3r}&Biڏt< Tk:-_sF`jBYNU+ң]d_i';ɗsEdFHF$s֩ղI'3NMl+pn&[R52dr @ pZ4IwKj),7꒴_/ n!) du3b6k)6y4%zQZdg4)\&IBEs k&qr lZFWaːT:!["7򅟘/Ҵ9Rd3RM8y[!MO}BQc2)Je`=$fZm%GoՕ붘<[ ۀ=;0\䀍=` \אMn/ʲsT)5o؎`w+[f۾>`uBt4F/ \?0p<ĉ [z\_T]gPCa*ԈbH_3:RMGTDéK}ئv!V/UWlzB}{L5FzgPl%" 6:G E2v|+A]ߝPC5KUNT w+=ssC×Vx0C7F=K%3/WNFH")QCH_ Ղ¯i]& }Ϩ)Ua=P.;z+W> Կ!'u/R g(_E=!4o{e|~ѣLݦ)p֣_R{P#TC!FVL5ޯulMvb+p_v<tRU+akýLޫeA!F,:bDHX$`ԏqY(`-V6ċlÒ}")CU{lCamd$,"!1l;WBժ-}W2&ZMB&_n [R %oId?1%9+'=$B[] pF}P_ЅL{˖>>sY/|>Mh0C˖|"Z/:11lR 'l8:NBTCMT557ue+(^WTiFRaKMT۠MUDpJ8V'lTX[VSFV,%ұtkci5 &`k)=׹l5}w>f0&cg27; W\oTLeD (eC|*SXD+:U[.[ye&}ְMNb/T#?w)%Z%Hl$%IXQEu% u^׺--߬2ĶO/{ Лm#sLG~Y0ͨE..1_-pK[H-dyKۆB,,Ctx-F}x])Q Mm⣘Ri0͖vfn~Fj^D2*"5< wN(쾖#c9&|Lk65/wC8%uaEn\K92O;H]!Q탌hC+<ֲcU2 xR5BI=wCA?yYitF=څ*IzW]}1A0MS'*@-t>@s:H?Gq󭉇4(aA| Hp}Ȩ#5HbZ}$,rT dX,/@ 7Ԑ>uƤV{OTj(EQ{ QF<=3З.{`?vv'dt8.(c"; nwN% Re\(-& *c3<zzOɑf+̽Ymߠ ZLI(͉VQ=/֠Nh*jS7%vYBLO"4|a4Jum?/P:CP}7\a)|Bƪ⣰ ܺ0!wX)eT Sio0ELNnC@{eςBc6?NyK3ŝ/қg]v@u.+NǑKKIԅmݑcݓaop/tgh?{ 9žk 8IYL#T2)))rP_y퀧<-/^ b?9OZt$e'30]xL&xYU:pNaRG^=m0;y>OwpByMvkty|xd޾N5Qʥ靷V*ee&A-o6z [o4s-< Ђ`P|s.9C-[`NI6,$NPǬ0x*렿r0Wj=/(>ap?AYkޘtoĐ e?' {ƶ7qPxCI đ.Q&kϤяކܛ1x>&dlr x2T-uݠMhG4fπ.{feIn27?!->`~V>T~t Xwpi{-U,tE?vGؾAY c?3h}: m>A׏ 1'0?}֜ġW>S>3OԠihD|wOm~ȟϛ=R{ܢ K}>)QMڣ -+Y"} qD/s2( 85DүoTD*9ԚFob*dXzDcY8A~Du |e>~1~Lo0e?VT .|IB~P a<?V PS*QsPHX"VjMS~jE!*ϿVO*D++ڇGm_[Ccfތgfq>ܻ<{ON!uzH"o6 bCiv"1bf|3+jv""|cjdZ]ĂxLP,Dz0s1'/M, Bvn/7һ8`ʲɼ};WMµ6sF;c^XlU"kK뒄c1O/1Le$d[ io?8?OsɍSْͫvwߑw'щ^/8qp'֌Qjue!*DwƉkCCTk+Lz_gxj`zQb)PzfΈ~5քm;IAFfן$|uwk0T6|B C_bgevl:'k|g_\`-q{un;˵sp]Tݷ3̋kY7ege] wT^ J>_?iXL&,aaҋ߭)8n r&[^fv2.VsAZWWu'גb~yGVHL}'-X޷zr2XDW >ܗeNݟh]7cM)zٯvV66,*&&N9?3-λj|^e |.9|cWtI~~ftd0/uڣXWx|yΪ8랉F_]ۂ>~{m l8fNBиO1FsmmoՎG_-Xrdk7Ũw-d=xB7 l&&_P@tGHfw _9?{c,'(|+*穙z=s9K%mX/p@xp&| #J Oj6ex|F!eטxu8{}P0(ЇphIz.D$p'f3xNINN3SZQ}Ě:]_wNYu#UX9mB#6z$Y8#-Ɨ7^6\f`\F%S$cVY -'I7rNRr=ʢk$Q#TvD40* 1%Ur ֚L8O4hF>`1(Lf:)Z 啮 :CI4ٷa%S߮8Qv :Zoh{TFJi9j}2, tdpjm2^Ta֦f-ǫLt-ʨ׫(~?@0)a@X0DhY`6d @A*X8`V V< P"`%`}`?8JAp#88 8 ΁PA WA## :3tC!zw/)8/d6B; ԱW-qjm-9 5¶4C­YnOujvg8t\uvMi۠^k+,wjٍjIaiӕlb7Rծ4mnawi<55Swfֈn[7Wtc;uJ&{ZVNYujӨga9Vŵv ֲZkX28lʸTkz ´`n_[k6jmF!dlm*Zvnv!)?ݬvvIfu->#Nc'k H` 6pErԽ^zONsE!G+XWD_Ÿ;Xa07;e΀}y5eq1s( s_|039C$rbV3v\lG)aMq[? {ofޚ|~5e `R1T4g>{^+r]߷ *|.u*/9 HXNJFhbnŻD?({\|<\q݋pSy(p<(xSG"‰|&&*L->Xy{C c ?Tuv W-azay j-y+Z1W84pc{A4]v-D\oL袦tkjq4#q xĎY6#Y?V@g H?@ 9ZT1h r|вNMe4;O ')R;kSlsI6Y` HZ6°PO{dx;l 78q(l[r6!9-m] a^Z59s3:K"z.=(Bfa,kYPcPWFYS,i<k5FV!fDtH><;I3G=25gY_% UZ)乮x^&Ǘ<2  Pl[N,r9(ivލ;ɮkK|˯\邎F1-qA7 EMJWc~e-w^v*#pݒC~  ("q@r8@.c@O4̕逗D|` L,?CO :`]rlk5w%wJ\&]=WN[\ɯ>&{"tY9AK|rkF[ 4;nlFsN9FvR;Mg+GiSWS-YMk]qʆ6vidYN pݖ>jQǂ٨ܗA?YBkWQlrӶKVwٵ-4dʾ'sS\|"+~_R"&̉Ĥvma-~&c,ȒVk@W~b=OE,E"]_ߢ%ZߤezMGO@Ho}HE'PKiR h<%t2rebar/ebin/rebar_user.beam|Ykt՝$$4# `B&R˶".iȒF$"%@)6S%[C>x`.n-۞u?9|/m;-;a9j+nuK(0<]^i&  w_UJ{TR3raZ,iW|s+]jYT`T.U<=_T&V\>KLe\)M5/NګB!ծlTjnri4zKgKER*tⴷ9J}yݭTJ+q5e YuR٫vhX[RLiȺaR̖ bt&Ԓ@ifMb8Z"KVJ%Eɫ|s:0ayKN33b|Pt3]ȄʜH彚禋t2QC~rSB54*Kp͛ЁVeJZ1N>Ăjڝ){"\45p2WVsRރWHޏlc䂗ˆgUEX|?雒]a W-)U@BbU׻<۪ &ͯF<;WCi *aeW7}ͱMlty&gQ- %nZj&x)3ZS}Ԋh^ D&C#BZ+57z/ iLX^v1f2]v̖&1w:tȗ!)1iN˒akڸ?{$Mʤ$r[sV_eӆ__ɶo!0N<Ŷb0 7aSa{\>4=x~ʶPT̰m$J3jސ֫–Y7z)uuZӘijX aiwh;xb;f|ii[HM[mhgeF} vŮHU:|[o}uu7}~AuRvu1|FYkiRn x/ C̃m,uhi#0inǡ6VD |~Q4~XϮeF"S5T5/ 3+6(GHu0"kSecdɤe>j|IgqR.>:CImm'ڄ#bMb=eRh" wZ7E"-"EO`y^mX%{Tm[lPl$7ŎJq/&T  m 2_lWӌ8%LJ&LeR=T?ʔ HKdrܔ ӽISA^+f|2}3ѐuYlu' HnB.oWƣM(ִZKc~q5~oTir٢OS[ 3'dnk(9x|&Weo[&!RC75o%9,_4ț`*_H!k6[vjot8Rf1df1_xaX] ڳ fǝ#0H-> 5T#2aRN mPVKG,x̐sbI}{24iSDzuXAn3X7*JѠXPaaϷmnO<z`ǽd'-1]R0.n F{c;bFV Cb~N)vR F׈ eoĎK}ءk5pܱL%_$>*1bR҂:1Ir&ǔ8_#B/h)&b2~hhr5b}ׄMWLh,{Ijzc|rLbň" и Txv 45ǘ,/L%e`㦕.Gܣ[P-nr۩7\ΚJqh||g6$ngQ@ m"Oui5$95JmvhCZAˎ tw#tw UVccXgd5@]?i7,9d= Vߨjkt7%qZ,a^,iö40~2kOn݌%>s;a3D5>lZY:(E25DPc=K",|~wb2$S/I_ñ\ˆq. b֎1wbFbACPۅuaLU^hN /NlLP׳lL1.C#0ZXX3`V8]LpLC8'Z:wc*DN^QzgDYQ˦cAU(֞ϯ@m@R YzִذI,,@΢k^0Kb0rMu5{a}XnH:CQv9=wGa#d߄Xxs3vuDAUTioeQ=1Hh?#x7wݣ> <x@~^;_CEh'$kwc>I6kc6r vN'%C30caDp9. ㈧ۧmBS?9aE!&HOu'↓C2jiG$kπ=Ւ`5> }~XLw}XK, qlBI#^$+_T1y1ƟXcBz5 >G>Z ' EGG:gk+_Jlǟ5v>6D8"J6e 98V[29[FL)1)spA|Uc@|iyY'K_9;@kqkI"LȊڪ3gY\7XNtB(C|7(-JKNWTWLK{d+tzIݱ^;Pa22ȓ>O7Մ4˞8ɜ`ȑ׾Q21#à+f̨emʊIUȯ|q_=qR,tC8_."|l<ϻw^Mw4͠A\oŗHo|KѝLM8;V$:LpV}[m;Svl8_??H6Iqe8tR1p:>}oHi{us2;*^C S'~qB!3CptNھk&?ɏ͞xDO5V/q8K)Ⲁ}d p5!.VS'@ 1@MG] bΌqϐTI2#!mCGsH@ߧP?g|E(01*!C!0O1h雦 M0B"@lFmr7B˶`̵Dr8~"?AORvGYCgO CTrG lwTaFxaPVk(#g^F#$7R1-kw?Űg䘁ǃj=Z9ZCgGC#9ћ4:Lhަѣ4zFuF3]4QzpG~]/5hU6"SVؠ- G}+rK/[\@~կ&Wt5pdɽ>Pkao:v.U4-n,{p _yWF^YKg% )>L{~^>sYW.' |O`7V&~<@ {?$P!-OCZ 9H|u)tqB >Xu\;dG i?DLP񦈋"gc/1V tENO&:gH"t20 H  VhHfc2$D]uA@?Q" ? ".";]0WnW#9Kh!MuL"8H`Jrː+pH|8 yh1Lg/qIFrRGTQ RY G辤L!ʣisQ@8[J F5ɟh)C*h|DPIE9q}ZT(F,y#<:T"pj|\\pH*gtM|t8TFʟ0Dˣψд(hY*G*e.5 8<{hB8AJI>%ͤZ;VR.q#ʟ{% /ֆ\b).d@1HQ؅8$f"wT$R3HXUˣ/ d@#P/ӢPP"8?x VJ P$g)*3<:!%#E7tWYiPneuleu#,,хImܪJ4rS)є`'Rݧ'!!y1x`I;A *Z LpD+ &6]hPYVhKr+Eԫ&)s )'͌BR[_tW_sv}?g!g(CGsoU)J$nMw 7*Q< ~F*;>keD(si!UNa3Po'\{A{`@A$1G@OT/)DG 8C#_TT"4G4FȣK`Ԋq|Yꮧ2E^xO焑:ϻEwi67ʡ' nޝTm|V$n"}E4W}dETk4ލ|DM")Aԝu0Z#6]tny p)8$w;|ඣ- u\wŒxwOτXGjnJ⁎{:Ճx91;CܝH I5A]l]gOէwyw7_ws`T=}z/W{{YHݛ@ LHT]F*6%F0rhsb$F9ܟsQ?ekb|b66ƹz*&uOs)Tr s_>=W`@hYΡN^p(P퀲vvnH̅yz|4h22Ƞ 68uǻvA3 Mc}b…!ffapR?ZPa] !b cꬎ ,[Ȼw mFq)c}@/i0}T̓݉td~8)1*Ћc1BEHq}˻Gs^| pW/ÃN4<6LSA,=A:NmmgMə?fkd09 cqꖝ-ˆbb,M:I- oY mLoʖϠmmȶ6}WM0e Yv܆i=af03mۙ>f&n뼜_ V<7\;G{3b]i μ;i V_| R}R3 ՜yq ȭV_=B,̽ b[h*gȍZ>b`eƲ-{d~^sDwl-{7k]]{3,av׉F759Ro 7Ū9*LWV+ñXc\][^]pXa1F ͡pXo z.uh' oP"X0\ 5f؋V6ñp]g976k]#SO"#Qb8F,NJrBQIY,"*Vwj1(ũb $A {b~qPO|B >+n/Bq?GŷC}Lp|I<T\pqw<|#^W* ~ ?7-x'ރ_n=|/K2>g9|&QD;RfR? |&S H{^4ҁ4CZF'IFz3Jkt*ih It!]DOk]{n-}>GӍtOCt/+}jJ PKiR9`rebar/ebin/rebar_utils.beam|WsVz'R<"udP"Ild|B 6D"37JP R@@e7nZvYxyd䭯yh_ٙŮg,s}+_;Jܺ8u>_AVMm'4+۲V ܫ\ ZR[fs$v{7g,onV8U0{gPC6} Z۶;;(!*MDjQ[-ﵐ C7Pk[ 019Q,$ Ni8TEG#2k-ۧNYl"_]4s1iVY: {0.n{j\bz IZHM!PRxZN >WF1H G)(.@[nmZ̊ɘv;2kP5d7v\TΞҷa')LKsiS=Ǫ_aGK혫` 6:÷x6/s>4Ea_@R'J)?R2_HJǎ+6R"- Y5ɑ4%%鑞[U$j h)=kMZuDr˲f  e9N~޻,+Df)1EzE >Kwvd>+swܐFZVodNz?E1Lt7cF=U62jۜ\%L=G2% M}SRwЄ76crCN1ȣjI-ՖL>G3mIUPUuE^)3+~UV~hY)=Eb739!#?ww,}$J9e=1V;8Fa(j%b\3eEYkZmQǡe@7-1tO JTZ@vew'q$h2G0%0,W쑈?gQ Euu~TF ,?c*eC͐O $0LۺIp2:HTHLV`dpLSUnj{!Uqx\J쩄1n ed~:I~F_XOf )&l3*wr(ˋj9p .ń/CoHluafn@KkMsK2qFTKMr޽73S|y~b4,o70r&FMYG(i\rs˔cg7eQ[*rbm!boamJ kjlMσcS7 w D``rYBBJ ffyjנ=ZVOsWVc.2uO-Y=?Q9Yϖc$<mfpeߪo־N'rO"@+4b+,b&>?*>E pFɨP"'YeCTbE{b9E# _(N.Ŏ J_{ 6rF1&/=9 o2s"Qu2(1MI~{w&: X&bȢ4#RF&g__R}RS̿دdO$~G*eEP|/E,ߏ7Bނ6C,wӏzʱ C>L:@?Dlfn⶿3FbM:;A?,Za~]5$TI.Gz|uVb_~^.ogƇB9˿_:gľ |#+rI'tu"z~ YbO?5~gtY |T4SH`A@F;ATnzVQ9"xEޮ7z*{)Eϧi}7̛ilY;o끭Kvaņjv.|ɦ2ΉhlA'2$32YIsnF˱50 8_|(Vz4(8VeKfEzX4zݱuKmilkʹy:Mƅ16.`W.WA t"] Y)"j1N;$ .4vg\eg. ̻4$cmL.\7jbځsP~%7ux\ ]N6,Xvz7>K}czkeQ ar4 %\<8%^i+]xI^] *^ 򬷋WZQɮ/ +\Du}| +ͳ.y_!Vl4O ŕ= oe1EHֹ~l]oL+[ֺ7_ fID}.0;h>[Yk9*B;9]׊X+ՈXd7uLEe9Xl7a.)VJv tAE2+(UX|H2WÀjN}MH}p[I-0jR^`lOs!iJp'cׅf'{:6UFsQe>5g/ <0~7c>kuhs\(rdCjH0Eg!w:w gxe@˵ JXd 0c1ǁOU,j5IsgNKCu*!jӫP 6 HpZ*vH,@3&dMkn0nuqQcO$X* ŕ!ŕ'c쎨eP,0X`kuH3iZ͞܂|XboW!B;=r~C[G꥞lv <߁]IAqkU@zM2J4P9ޭ0߇nna{y/$-/2FU>Z{l>>{/|ScJ _?3[27x?# @= w `K'!!pP=>lY6ao0Y҈pP,_R,ywg{ `m]k6 UW(i쫴Ƕ61f5enk͘[\N}̭[1ɴ_+oo59Tf((@ I ;_KMe2;Ӌ@ ŤQc- }Y&. |b`d'#Q^M*HC-4d֡6Ujc5(?'H4|&E?*" J+kqX6d U< i G)W~9^<2/ }̟urxB>_h)8"XZ=?#VT% c4D$hP+3mLdJ"̵mb0FGRy6LZ`l ]ޓ/>ZbSn|{COAE|òJZ|CI/Jh|hlb*JYBĺkM!W@2%.]\:1'Ŕ|[6uzΦ6aЌ*$~}i0\[US6!0=ɾޫQLg}[cG%`㫔ud?B#ü n$}N-|1BM!#m<2J5l;HF{xe dImu|$UxE\ ʨV8h`JVS/Jr} 6(V< Yjȟ8f$/ sDƭuEv֨4^$,w)Xt;xfT%/A}'FAدM l|jK 1| —ᗏ_$7@ Pψ,BV5L5 zGhw&gƳJ@"G[X.<j07loGgqU8fW HC%n(A9D=Q@?SRq2Pֻ9[syVAبFSѣ#rR6 th2rk-|:Or/V 2[AB Q/TO%"C:HrPfČWpq%)qEsibAnYpd~tGwt+Sl BZgG(b0"* 8?Ւ`>n_|Vz+mѠܐUNbGw-`-,`SH:8Nh~( NziqCsl̩f YgFRV\3b zЪ ʠ94oT;|:Kt`ߑoM1 :r g[ngaNX%0&A !%]O7a h>ʟF|es$?{7-f4ԝrNs̛4FWef oI9: Tڤ@Xu{NI~(4: /F|[LmI84l' #'/)uH>m፨gd#R CpYX@P];"S|G߆_|j!"ɏvnK;MMU@|Rh 1ZB;4'ۆ,3%6KF Ocdp|&{{!h܋2yNarI~9Oher m/fy< aM9"%#RPNЎ]RTE2+Ѵ҃! 3Kͫ.V\:1L\WS@iFGF('#<ׅ]_HTAe<k;=*[GHu/y孵)0['17yc?Ci:,y (;JSXDj` hZ(*Gj=)$ɰ!0DYu'w2^x%B 榴xh<0`驔H$gŸSs}U8Wx$T7MYbxxx eX TSG HP54FS V:VB[g-/nUVB֥j]q_Zk.j;d\SO;CjJ>T4 A8P;auG[o /y@n !\w :L=@ ' z;L\iCñbC<ݶTbi&>wԝbtgXm4hh$2 ο 7ihPMCmzIFijF5@y5gY'hfS5;Uq}hijˢ dG@sd|]t5om`U6vUG G6ԎVfRӵCjS[x 7!nUࡃp dPmKn[ ێ&-0IEd~BxVF (vQx(K^Cye-eiҖqڃt~@:RC(OES.ԧӖ=@=A©^ e}}@^\_~2FAbd@1d@ HyVR2Rf;MHm@S_l e8#6٘g>$\Y?ƕSN,ƕK_P~=ȗ,kOkVO/O.ݲg\*z']b{ ZecuqIʟ9Oόk]\B_FunFWP?w)&'qamW?I`m#wG'Ĺ]H3רKso'ZmHc|@?y: u%Q(HkPO82)G+P_'ט.ML&JI[Fb<:ޟSL^Wq!9!RLF?5h8<1 > Cz$!쥘ԛ~ ̓=7gO(q7)&?k]YM1Uܧ7E}\sDYf>H&@ޠ#(&p'd(Ť:@(&5)bRi )&dAN1ɗ aT3#8$6~{Y{Y-hɹ9~ ތkriw*qK-$WI0h"cnJyyɧP';T4::sw(SO*o#:&N>dmyd)s)Oݤ=f2Yj:8k'ߋsI&7N?<6мr׷tIE3JjIjpR)ϜvG -#ZU7帙6呵fTɺ&/K[~G^/ЭO&?,3_].KU߱PVM.o,x[ڔ^sW&n~obl]7{w5Jq~^X;}j4''wLY&;m͔_^4ܥ1D}(eQBv.~QW&52 :ΫYLyv$E;[ eS2w=7ڤ˜jxz$Os=|GO[G]4Z :K>x̺sjg?۫~pҒpĎSƺy }ŝU5khx6zиY_I8yeڥ5'k+ֺ5  ~V%x͸VVnm5sɧ..m5Oj'i9!c^{sOxזaK 4IVuM2vI9 C3fq՘<=I fT?oINI9?r'yj>zֶ^9sM)OQlT)iv^vƺERzvℹH[}nf«w 9'־w91-b`,.QhX-&8 Zhٞb#F-vf%ޮ8}%})-oO_;¾DZ+o~VQ[ghD´WVTStKJ/TMO15_(hQnl1J 7]|b|{+[z-q|ۊs~g=" b`n3 ;Ut1LBA6w,9s%ӳ ͈3pG;:J5U^GS^/Q*<>;zk.kkkkkn!#pA=3:b=CQ?b=C̉TkN1^sچxŋ P w\q6A @q-Ձ;+jMuPLt6Π%pEiMdzgG A<(2{!<<ɵ۔'srb27)&g_(&}brO:Jl]Ku=brGNj-8;SLWLrd>ɦ~弣rRLyFjn>EEd܇MF\1^af Qtm/앗K8K5'̚3Ic 22rk&%h O5fGAa."{Aq[rBK5jʴꝏbo-Cq3h)ȰfZ2 &kel d635b(;߱Ŗ [Qfv^53;fZ-2Ζista͚ybsKi0w;p %*wp$(wA%x ^+4ԂZ @!D,z F!F B0THF M%DBf sa@jEaLX) Mk[aK-|/ GpR8/\ Hx,<b3Q%zbkQ+!bC^%h(&)b8ZıbX.Ujq^(nuVq[^#֋{}xDl'ExJM%.Oć_3ݱpkZ;W0 8#p:3p&6q|\ ³p ~GcZ /ǟx ^z%ހ7-x+ގ='‡q>O_ |o{~᧒+uRG)X$ Rw¥R_%Q, I#ti4J+B*Y|-J)͒ޕޓHH5BiRZ%Hkufi4Pz_؈cp,8 444˴δy=3(zWb2:=?1Vr{E#nJGYd%2GM)WpxA$-IՏp GA&N1@rQ"DjP.vq\Q?Q73ɲ̏ @=Zk89|pXRj ʫ_ghUqkˈ!i4\\U1z@Lӽ&툣3trAi~=Xϖtݿ C8룵`u,dIVHaq c&fTP n _/. >zv$n-tK?-fL%F*]p QNX/UĦ $/Dv x8ћG kUNF(ZD\$qԎ4ɷ㨠 ԙuFFY @f2 c IGAُ\E8\T ǃ Xz*ӪG͡,4NO\VSF]N9d /t\8zqvQXУlckXKSr.FN CP W1Dd[Ys@=3QQGTzňk l/ .d [Ąs Hd<6| (X@ui;pG 8wpvl^(V<~6V(sj(beVOS/& |OCCZ2( ;q-x*(AW(irXW}%*u_ ;:aeQȝܤzWR8z#>fEhnqqg=e#?3֤"K_c>7z6C~!y:#rz?0#)F L$o)ZnKx5_'cSHد)FFLK=mFt6۷kxDz+Fc}܏x}s zq)ʬde.) Ql) ި& 5GM`y L=vO9n:h'Dg<0n[8)Ϙ.8p("QN"7ٷZ߄1?q1g/s?UD 8Qa_}-qQ =ڴ-yhQ|`A9LL33(jѺiQ& ՝?[8YHn뜴`({_YWkT舠?L$ic!n 4.8R(bۊۖv!NAޛ $ܰlms/0r63O3"/E'nکo@,g+~'x]X:݊yR?-31Lq \cũw*Q^˘| ]H=TV|PЖUmBAg}ee3H&ߤ\} ɪ 1 9z|[a'=rכgtF^2DZ蛶C9sTrXo?q:wNqGiڣh:R(O Dq)I&{Cy9Ku٪ VŹXy;Ek IaN*,w*dwE(br{֔J?yreq]$8})7.y^\iŏBx41TvA܅ P)gr QqlxI.J$鞷H;.T߂k!2qdNIGTyKj`o2ژ-}ՆJ2_$YY (327:GYY U  m< t? %~<-"< uYR=QCڢ'T(^'\N y˺Aqlf/!G8.bPn'VJl''Gg͵ɠHȍI$< 7푛[wT/5Ej8Q{d4Σjgt/GuJ6yTSAf)'aәIG,W ߫nכV_ }_~.hMtOV ~~}>%9$ˁhe?.~W2|+8$ep[g3"jy bXIx(GVEB(zB/qj +3o Wwڬd y7k#5Lh/ S3Jh gZ]Sii&B]PKiR` oI rebar/priv/templates/MakefileUo6\r=lM8iiǮkKSD:S&vf'FNiH 񸹛xQ8 ۄ7p* O;`Q>sa3:(յchv4,s8kxm[pR.2z'-1Oާk),QK.rZE sH`(v&afpk0/9ǞsQ=i")V5`3z:02^Danx@FqXĴGR FEo+/]KBkT7nkrh--,Aaia= pm]}\1lb;SDzpñgK-pd[zݰ/͌ k=q*M# K|Fm$zG^7տuU4kvP9ن`3uqy:+w3I7ҭέ5.,M&\7ڕxRùs4O7xvAp1/ӌbaR96cPKiRtI6<rebar/priv/templates/README.mdKM.d ˩43'K@E(5)X!9? 3' PKiRTnrrebar/priv/templates/app.erlO 0+Hуأ7Ao"%mِ(nb@2.!$ !oaPR4P $oVŴNWA=zCҙ_q&PdFVo3m .%zq`Džr(&U3cPKiRZX40!rebar/priv/templates/app.templateQk +`C0غmbMr5ﻶL˘ x9,'(5-[;9-$/o{93HF#kOZ7b$1`jL완-a47OڍhgT ӐH5ĺ<Ũ2 :9k7& T>?@]#EOpr5g5GeUP [zC_Y׾{xt*UEPKiR^[o%rebar/priv/templates/app_rebar.config% D~"t\-0쿗u{3#d`8_qr(Nbӊ)ItKa=db^󴣈ՎPKiR3)Zl#rebar/priv/templates/cmake.templateNI-N.,(QP .IKIKUMNMIUH/RH*IKWpwVSH/.JV.I-I,IjQ "@PKiR\%rebar/priv/templates/escript.templateuPj0 +D0غVFq5uc㨅-'w3IzzSڑC#!$[ZHEcD1]ײq`_i|)rtBx|ϼBm*CCx=iڝ'xț|gTT+"$܃@G1*5Lksc#Pv8.33qIwo~T;<V^ X謆9\)N['6X!\7 5(*]B9J9WHe[bi'`/ntKt|>vhWp V0ALU]Ol>?PKiReerebar/priv/templates/gitignore% D/ J([lKww48B ^Md>S6 M# Ҡvx4TVM}hI'퇝7~PKiRBW !rebar/priv/templates/lib.templateuj0 y }0躱6p558A y)I|%o0j2!J; &EWP!XUFy5ƈbNB1ȢQdTm1Yީ9ZS  |kaCysdlݜ!dzDNCGIQildُ &LrD|ZޝL+5ɴg y/:=V{}zd,{_PKiR!!rebar/priv/templates/mod.erlO)IըKMM(/*шPKiRxɜ$rebar/priv/templates/otp_app.app.srcM 0 }ȶ=i؊y)Rہ'}WɄ`Vx-̛>C$ b]BHgxiHo+E5E>Jxj (zr ξX]7}~q-DL@=Cp꽳~*JtPKiRhc$rebar/priv/templates/otp_lib.app.srcMK bP VB4>=;ۃRްV@d#B9E2I· ON Z1}ֻWg!~niL#|..d'PKiRuorebar/priv/templates/plugin.erlO)IըKMM(/*ш,7&+4RKKRJ445tu$jA U\ @V 2UU_PTf\CPKiR%6$rebar/priv/templates/plugin.template}n0w9C j* qell"Ļ׆"b֓mE8U KN{0kC+QW0Y2/% kPEK%Wp7햎q a2& ^,~&q]Ȟ- IN48,U0^H)(7yaOgVWDpT@pQ#Vyx͏SQ~kugH. uͮr>nv-\i/1~PKiRU֊!rebar/priv/templates/provider.erlQk0+.P@; Qn{ (M+jKBӚ,6=$ՑǕuvx]nft$c|3t!W km+sV۫eT \Ki6y?t,v,{dBBo6qp7>&W,._б3X@h@jqm<8h['eEP%@#z#Zt"iETt#v箙;v\gjm%*Q6^lϗ?[ ʔX"Ri>&AlMMd x}@h{r .Ii 7`!b~ȔT}6l,gvltHFmrPp^km=mm!Z:I=V.p\ T9vZb0y+S 5s㫰N!/Rt{9rՁ(u^դf@j—PKiR(#%!rebar/priv/templates/rebar.configN-ʉ/()QNIM*MKˏNI-PKiRޅ@)%rebar/priv/templates/release.templaten0 <.(<zM )&rRs q*Em 傲P>PSvX4An5#[4>Ĥ=HRQSm- 2ʜs_\A#D>5~TsSyI.-BA("gx|.39Qתqzo)F'F~*_]Cz̺w\$q1IGdۯueoJE}qQ=b3oEb ^c qC$ [T5v'(rfw$w0BRVTPUm GqsX#%U>yυ)G_PKiR24&rebar/priv/templates/relx_rebar.configuRj0}W /m?䭰KRbQYu0F&z<3s|l@+F.+elqP_s҈2CX<,ʃ?Q9*;dC-P=Q}J!ښFA Ƕn!1{5 tRyc[1:͜ .OB0»Z  d$HŒ A=/ec]z^SL2AEO\I744!Eg$Q}lx{YT0}IªSqCqd^J1.5jfqs z;п~H 7wnP,ӮAܣk/k[!yn*,N nc3ř%} 67Ɛ^k mMj_f{wzrR+SO!}}CC{/)IVVҁPt8cډIR?m-+ M{ -BH=k` *r|v9!'"Gx(L۵e[Cz÷;Sha%?P9H: ۾NږnU(/|Y 暻h-oԳ-گגDn*G"nwp,jvT[a9JUY‘G\Ƒ)[,zEl/S'PKiR\2-:rebar/priv/templates/vm.args-KMUQ\\ũ%ٙxK[4K؀ PKiRvAl ;relx/ebin/relx.app}Mo0 ( vv\5*@TDv`PB`Zա&|7C])xzS \v }@Ic)y%T? %A`Ǎ`oe=`Цa}/*kJi4B1xSжq#qO:܊qq^ih{Ef;_'h5{ ejP( J5Re?/U! 5/nf&fNěP ):?gY/Yڗ"1tPKiRz$relx/ebin/relx.beamww\nd@Hhf"6Z! )"D EAEE Q, (@]=!x=}ϟkfWVv`{=[*LF  8%\hD0.^Kv2Wx|n-MP bq<7ϑQJ51iTPNj(-bIX ce6ۊ$1xq\X(Vu/e'p0J(dҨXx2QOm J&r5Q˨\2 ^-IuKxsLQaK$dP.41O.$)@l2[$I8!Jb EO> 5#AeR_U#:E(;>Y++Kǚj9ذBAZP&1oJj$ކf3!N&iT -p aTfrQ(8违5 '`Lhh\2h:DJ049 fL$Br&bBnBB'| BXhMF@ə t<"Z&N1$&$bX2ƨF] r28Ӂ Q$&I-9@&N F EDg0An WtʗVNw) w+|t&+?;JC;!r:XdP2"!;>.hcd-U& r c\ aM xM O͚'5hL$y9Ϥr8Ĥx$hlq Ae^ddD$& D-@C!`aD4Yׂv:`_$":MJc(-+FNibqJt5OٓXC0DM? 1a)YBD{_9lf6!^HEe*T#pa" ',tx ('9aa.]D"!0^ C;:Bx0gL&(;HBbBr<>W DGPGҙ1̌H@X>Gr'aC :LވFQ ,4D?Yof7Qjhx2o@qDOk'ӵ o $#5} a@ ! "#k,BanLژ'_ Oaр&u6YMd?fBF- 23[ d o,238fde aKP( E>sS'!KD&[O2EGof9l#κ&VOݛF<>)/b#g-؀X*A0H?a[m%\+Wk $8 Gx>QL,AÑ]67D0D7@d" Yۂ~v:Wo 1Y=0 N;d4LCb} FdBt:DÎ``%0'0%0:FFCeI Hrp!BxMb-'-G!A28L.2@vbAA0G 0#((!@!dT9N F~m\LgnQ 4wQ.r@΂+S f`#cT  2LB HCwBÞ@$^x&)OMU,|— O 5B6^hFΆعUs9Ȱ6QA} "C虞,o:`'R aRh'")u>w. =@X@d@*uT)7d-o hʸ6l RwPrG@N*cHMP"e=\nJW (sQ= )oRS F)X/l/1y O~i٦ssTf^]hUh4{mI̫LyoJ$bęGo7׭rDpKo_t>nVZevUn|iZl#Q4LZC]&:;V]|Ó"OQҭW Yq4o$=׫fXڒpznj}E^} =5'ZБfM&\d<{f]s?j_={drMv)'sw|h?yKev7jɜjf┞Ȱ텛Ќ#>#oEI51UI:ڇ x7kt6;!huU0юm]9UP֥D̃ >Mtt[%R͊ٵNJ3w7|[fd6`^a+-K޺v?)g~ט97MMm.s7;^>{8Z뺣=ޅla--P_돫Zú>%4&E*:lvBլ] fcS[]}6]S{(v{˛XwƅR3En[Gމq}#b krH1D:6bֱǨe3 o(}D2}2:a#&!("DG2SX޷*ҬhxtUioxkoYo++a뭾tb:1Pa9<l|VookQҖA5⍴hPVy#>V:vp̷Xך9غ4sSͤvm&Wo[yN>堁{4'~3ԁ»ۖ%+:0ʌ%Z[Έ4ՔWB-uli|+MrQTim~sE;v|˱s;\vi;zDzC͓ww>qشg<̤o-y&W7:g;O'~!6_6k| Q)uykTz]/ sMl$J͔vQ_C%vzh׫D҈-9E$5}Biu?w:Nk`=( 9%xM.uJ-yw帛F]ˏ{C.K-I{!h/^\_\Y )YxMZ`edg,E.U{tIONuܹ5Aު#U`}9{[]ݨp)ޤʧfJ4-$sj.)FF*F_/.r1+|I/ [bsVfHY*1 <9vA|{}!;eSnǃԠ! ˏO;+Ys/EVsA'y{Bs<0:vɠw~/ᾴ$-zyvpQCKdücO>x86s~}ڶk 7/?e6zUX9i,mw+Ry:QEQUsW} xA k1!$۞ ;l"":%\7TAM޻.]ʀ? R DLhh!%5 d_2Sk,}?LL^+YrH! 2: 40ИaaB 31pmUͺF/ 隤 Zq4a}l)+n 4O!^XM[#sBg. n3*c+ԏ*#[H[?< xضg֪ҵ?ݱ#o]h:y{x(g_Jnqi~q S$'>"//Iu[Uym:>ywwDl^FUGE<2<P?^=Ө+"u;֚ľo}'=Ɖm+=ysGCܝȈ.&`yqpe~?͙]0pO)7+~$?r3?[7^)K`F^6]w:"&"Bp6i~˧0[x{ aܛ ߰Tw)d,V7_E7+|*#&$9Ltt3y x%Ypv{RA'jakC[kMẌ́zyI{h[8cÄ<75eCeK~Tg\<,?/b@U& QՏK=zYU<&D]m/wW Gbn%]!Uv=?H[WhOczwK9?bIyږfXj滚쫙WI)Igzôrq쭼z/ks<~;[sj<\m[Q#t_z^jG2_sv۬%>{gLdlxhJ{n_3N?bW~?ߏ4 =~S.R6:urM'Ou6 (1h qN\ ^`֎>8m oԮ~۴iƏ%gyw^g~/ykt}"}< .MAͿˆ]Wlw2', PK:g L,&c"{kp0www $58:22$ہ@ Gs48rs"r!5|V~u%l@ 3Az?PN|O>P?e$u>`20f$ t~|Z)X$?Line`9 8 9 A B I J K M N Q S T W X _ ` g u w q h i k { | ~ D/home/runner/work/rebar3/rebar3/_build/default/lib/relx/src/relx.erlPKiR*L relx/ebin/rlx_app_info.beamVy<]gnˌ=Vƒ0#K%1oYTƌ cfY,mHQRJI)iUTD*)6dL޿|s_u8{A{A AZJNp S 1 `HԥrNP UB)u9~ s\PH.Z.^ !DK|j&H z+ "(.`i*Θ%IXď/ꄋ;7Kb2G(Al:S C@Ih DtSќ<T%MXdR#N0Hu!5$K Pa"ˑ518 0 I45410?2\JI*2**Ts #U@T5l' O#HhA b*Hp*d T p*R# 9N:EBHT&S $8 I@S7QfQ">lɅN"`2\(`@*TQ  <&:YalvWh әfE5B1y@Hh]LgQH9@RD=,Ajn Ȁ= )TTC@3TQPKIHt"0BHz蟈ޯVЄ$>6" +%ES񿈘:*By>*b 0Eto&00e`a߭ 0,0QsfsfhMS\;W꣏95>FZkGSKa^Y֗J>ʕzR_NZONJka="Ts~fhhh)= :7ȑ;zKкLz[_b|%Z+bʿP'&ɑ^()'9SkbOޗ]Z_(L24puQu!O^:TmLCڋD6N#Lm,SNvje7]: ?Na/OuO,]'!GbXde q^K( yz0!TE,8b0!$.ಹB]8ӄ@! aGC+NW [CY7DME,K g /3ZM'!:3 A |{S| _T'{bң(\-祭R(mR5ww5LZk~ͦ! 1.7+LV9پhf7}h<+CLR}ȍC‚kyÏ6=gfxPI٦NW=G~4&ި9陟1poȽse= v;#.w1W儯VMkl 8g p'+ *QZE9b1g%d]e9 Gc: tr5V^k'۹gƢ-O/N^|śu wG^k:boyݵ/Q_e8jSyNœ\k-2"[]y9Bzw#0%PAsi],ǘ>_h^޵❨u7UU)~~q{}̷lg1jruhĨQG[]o7\˛;?b?E5閟mwi Kvres]m]~!e̝aͅ*Yv]Y??]ylH d9\d?/3zwb:?Sx;XmIoblfS\*=CyN70Y;j?k)75,}x0_߻L%s4زeo|B'$:J ?+w}PW^G+OmѠsuuv#m#mnϾ?%YNk- (yj|p͢omƓG;I1dejRܡvPيЭ~_;EFKX7=b={Wrk>(m$HC1p_#rm^ t@f݇Ļ=n.c_b]=xF|(s|/[I4|:~>/PKiRt]Crelx/ebin/rlx_assemble.beamW[pVz&H< $hw!Ħ&}/1tع(ds `d%ݪzntܙf?lf:>k{g~HQwg8۷bٚN9ۭXlױuu֒e 4Ze!!{ml/îu^?L|Xvw:~ilٴ :)ݵ`X kI:@[oZ^qͶ_wt5Yc]ӑZvZz{ePH3fCwǖ U%4\vS >HCGe­|c[k ԝ}ܩ ٨C ۮixxop(,BHJaza/ 2 !C OƆVAX6*q5q Ziz?D u,ogԸى.exÎk;\uc[ƒQ?6P,&֌ p,aGC!Zr|퓷 .4l-OlpnԽٷn=m$>%9Oan|;ZZOhwj$2dsÝvvoMe :Dvɳha`bXMFq:zb$:=q!nՈ _ǎAN~j%za:ȑA}m%WD`5+FpIQ(k qdComDUc܇)9Z_5/=Nx)] m[ ǾgުaYz .n#g7c.:m~{ !#UGTpv3D8l[MKFțDzTOHya 5: ͎7FBP$0Kp{%\UE7BÖ79i(dN^נIv-xC."" \AcZo7k<GpNE"ģw9XX:*tOw{" MM!xc_D]; RYX6W8d=ut/0z}"Ed4~%@khW&o\_0=+sb"Mq)TDF()X{Z5UдZIpȾ5M%?ij`zWO$o_i"kjfU#SV"~;e aCV}Oňد Xz 8F5]RR* Zjem6`G8>TJYFR ]GCJČb/Q lSyA$^r82~B>".J16A7C WiGSa3 Gy(d Gv+6I9m/k &v4`ș/8.ύj*?zYRT+P{$'=ǡ簜ckAOHYyjD`$(9h/A줤5NA\P*+Q_I:%Mlj*j"?HydY NzCʦRժ U.*Sʼn]ۣ ÓIdn)wQ>ʕg[n4]cw>q".+rVҚwb- p O&5Vcmx 9R`pEz)WK\ejb572Mg dPO%ʵ'rA 8%m ˲cs]lD8V*P]Pz$OQLĜD\$OhOK,KZ!ռD%^.ȯOWv[DC+Y *y נi`z:`7l6u NLʳ0PUu uQ)mY`B7BFUH[~|CLYonq+܀^ ^GMFXy$H 28W| 56i~#`s֜VuI+gxxNHEos{<VjLѥTǀܯh -% C g4:`Y habQ],*MYrΞ+C0+$2EqmiX-iWJ0Y=f7J1It Qm(|KrCUpM9W`$ 95_#?I/aS nΧ*4U<*-k[ 6~#WJr8X n4ilq<2bD]iU(W32s|.㏤Ro 'iSGl"ZAچmiW 5ڤa)>5e0OweW`"2as'|/礯?5ԈQL9Iz0XA}H=Gu*Q2ClWf% ó@ gQ{?ωϰCQ9 jKret!MJ` (N3%gGgSTgb9?6@4qi.`=ipvDmeK+|P8H늼OYw`U sjGv' KG  ( T[Pw;B==E;#^Pg|I+$ۛ|-9M{~9,/ řgquv7. ⢳ǖcN{>`KrR>`mz[*^,bQU}k?3A2o0^2bO7_\3UrvȘ2d;z; W!W'"vAe+ݲ{v`O p1.ˡřIY[o'PS?;P -bAs1eϩ}iZ3?n,[f{{rIk'-SzV.&;O2S $}V.:ic,Kqxx9PKkw]8[9v7*s͎; #L |=d7{=?bW@5+lDvɐ! H"x8us}t-gC+2^YhkH0S0&笄8+z`q<ɣkҏǕX{BڊI?D @kx/#KړJӿT0=R!x]~oE!+>Ɉ ן Tu B5KW J|Myy`O8銊/3`xlBW kx6bA3qnmea ܯ>}V݂ G7o8"CP8띗`}Շߦ>l*|]m9vZ7)ܯ ;=B=OY!C2̎k{觹*H|!"*V >_d GoF}(~4[5˥7LHsQ>cgz2uLގ؇hc:l]>b9H8\\d n\2|+`I C! }j YW.QR* -bm)nI^0ze0KϓLiz`|#tOipr@|sy{tVQe\J˃WJ23U׈qC A\Afsܷʡ}&Gǀi!Ou g?rW&T𽊓#H`@O">3"e=IϟGGDyEuԥdLMy %ԮIRUwt}7_,WT,MO5 t9й0bOQ٨F5ZWhmxJ[f|++R47,)w6E+sK֜2D_zȏ ~Sī Ӯ"DI! * &nYGZʥj#DFWQD9%|m:a]-+L[V-_I9N` Ts5I .1LVzR3EkXQhZ5G <=^(-E$+4C0%M~<%T}gS{nxYT:S^Mߪ 0 '!Y}V[m]r ΉJI }CgϦ6ςXVHd#6`vo0uTCݰ3=04,f5'j(B9Th̊ vƶ I@ "McHٽDSFC4W/PNTSp2%<|iyPJ@CwSAEbIq ^A9d(cf|j?Acx.Y/vDE=mDO]QEOTTcN,GU ޓ )bd;G|Or?|[P]e/dìU))9W^ANt7"q8w+Uu6^Оߋb%7hL`E!jeע}*\iDu^WU'$~ #y *Y`f? ;,JRTa0x(0F:S`値(Z7X$Q_ϞmD_mn }30]/x[ !#RewD$͏/k F]'b G|0$!)V UPcņ szu#Cq)TK }iܘTOG]=Eݞ:Q:AQWY0i:Mv@. R!H8AAA7vl,z&LfkO]y Pok^FzqA.ţ"@q>̇2JQWz4bCOTr8\g3gJlw Y蘥!T EAHlT{-އ EfNK^~,5FGU,.4SF@AE1"EH"q2cE$1eUx Ȑň!Ahj_sK$׳$);Az9*ނrlgz~xap'@ Ð8=0jXȚ-ayXqE v:Lӡb?u IE Q <<9.A:"'$=NG5":$pw"#6DP(h67Nq B*:*:%KzT,7bxE9pltLjOaG-*^6c{iy@膣 >JoՁ~ Ft_|#T_%T3 '#D0`lful\ /e2 cc8r78iw{z5^ 50M-?]1hzVٟiRw۟ ؟ma~Ocfz?`2KXlZV)Efbӌ#s赶xCK:M'x %ˆh}m4\x>\йP. O/Rsvxvz]xYPYEBo`- >!(Z&F߃#~$kC!!bL3)k$[$@Hݾ_wͬo MMoJԈ ~`9(CL1bz4:Vk?#ZbZ1^&Zb}e.f;9oA^`D1%=6 | =c5 :4tft 4tVt*k.as*0QzZ#: ,<7Yi<q}8^.q6(YiXDQ1O2s?݋p3̌6Dc 0B0Hf^ Y$Gb,Ph4Nb*:232Z 4Lf8yD.- 3EL z1B $x2 pCFbqhT3n< 9 zEH$ tB I s&97?,4+`DLEWB!zRM(EVƬBhSJ@Jf 0$3p%3qM 2C0c1\Lgj-Pd2 \23=ai9$Ay*O]& M2rjf bxk֘6Edvb4`*Ipp.BffPXi[wT:5BgViG2#Y[Dffb2a/d80O; 9!6Ɩ&'d1a%9FћD`JN:mQ@ҥKHӕJG0%D*bң Q$Xu!2`g`P,I'I(.G"CBc fϘ0cNR-#)NtxɗC3x_j!r 2{KQY?Af؈$qel4?/P1H K~FN C@j~+5$RAOW`"RzZZhD"ig*X+ *)B@ 3`&G4H>m&L ioD$n*6Z W8ω9#i5o9Fb,&dmgo(n> P6btXpvf &EaЁ]9bD 3AA.BHcZ1 \jZ^CE8ŸP*078K_F$`&5,DZmEq]YAk @V7$`#Y}6\kx8@p=4'֒7+ފp^[ `'뀏6N8c [ mm'`¼;cwl `O"{<!חP@ByͷuF?r\'G|C}2Ig8>Po>=I b\_CoYFW\Ag <|.6p[@:9sk=rG)>~ 1yww{2=<}nQlK1| y'>~Z,px,YR''KHg ȷ/8A?<{MmB\3ߝW"m*;sG4ٖRmMzeTYbzv[ݷoy/Bϛ?±n/]ϞwjҗWU zb6ۧ#ԯf\ ըn D ,"G׸\㝗کRݦ{ӳZ.e5k.&]ϋ z5(,̤eԫ^2vm9ccC]:˔fJ;u=a, uĉ4< գnY$C[wK(yɝ&7F3_ܣdn }֤b7IH{TŪ 5tiQ=>ѦWr|h5 "O]gXMiECk¥b6Wخ:ǯly{7s'L7zHs 2wUYOY]4΃񑪓ۚb'w$0wDyX*/iwMiuјUW e s^U԰v Xj6>e^)F>J5L[6Aj~,YyTv%y,M)B^>jcǙ9<#؁؉U#F.v4;XG䚝Q3SΖd;UDSP %scr"/<(gŌ$_]sߠwWK٘_qjroRׇ4 Yjr:4\9p؎e(k.|VYt2f!yϠ7la&M܁$SWtm{g7\4V$c?Ս,uXHoVipVbWk4F_gJW,1"9ee&&W>k_B۩eݛ``‡3xc)r\ҳJq%걡>{wZ1iRږ'\W?5<k,5~8}%oH`jΊS#n,@MzuozQ_h|䭟^zIW_rzm̷~S講@ܧAq`adSn3}~Ɲ2'bMTpxw3Ƞs==ܭ),fD$b6z>??NP_QA &S^z&*n! |7lcp}-w"YsF?sڳ7xa㞚d^#WQ=f+?&m0&s^9/`<3Mھb qK(Vi6)SV)ړJl<*5ٿ%B6[vθz+Y wy}R饓F¸AE&o`ѦSں6iSg ;)vT6XQz[j߼n|ISvڳm,v*܇tuF>8@ѵ5\:d\[JNVϡ >9w"/S/?,B-m?Z׵HӢvx[@ّ_WD/,{ZSZyJD k^>O\јA'o NvIWMgDZ_y|Qۉ_[{TcSBbR!~|!T̄a~] t##,0T7[& C<"n^A!~<[ ؋'"$7zV7Vޤӌn>$og`obyxA@<yY_iPc /%t)y!EI@GOb0/dSWP `az CrDμ:=^Mԥ<a>[?Imr7ؔoVԷKNC޶~C3[/V9G.x>b =pgVnm}xC_SsC'g;m=RS .9 xwtw=)}-RmxkOS~Y4إfgZ&mgǻhYj9IemRu9KHx;b=D.?*ɢ\ȍr)}qnAe oaPŷa[x+QxXh0W9:([{&vU6d{٠wȣ):3Dž1bMU:cV3߶Tw=Lw{ЕoA93Vo ,> I,awgǞS)=M.:k7zR_jiQkLFeDʜO)rFY))᥊ʃKowq:g9.P0`C~܆6Ov v6r$MVxnu\6?HxhʜoL-CwH$D`/G%x zG idU \@Cxm6޵!0A8|7Ÿc:]Xe1u7S1/S  u;|gy}c}oQ 2Yz~%?ܯab&=;t ߢkQx0HC#nz:vb,TCur@'yKW(l64M C42$g T"`5I#jSj uАx52 :͟O<|NS=;hv`Ӡ⧅**nzFݕ6d r;iаHYe6W4ՊQ WK*mW#Zzu];0̢ hI!љv,fIg28g8grCi0Yܳ:4YLYDY,AN!lxr1u|C(mq|tsBscts,TKPPsύ\raTV^Nbp΃p gj[l Mq]T^fYʅ řB9#F2hIXV%Y%,ƥNY"H/ ׍ dr=m`y=(CC>1\"zhH y&kpmy2i{Q@pQ<2(d`ՎkiϪ8xSBrb`PPC,*w^. / !L !\W{c GE%%/`Kb䄺XLKXd3⾂sx)2چtbT? 0,6a0,6bdž07~h"oF{)PUBU?jjj˸(st)$y\~s_]-|2B,~!E?0= ]a4H7@H~ XҲ*1 ֏M1KuPBq:ST~P7{FƎFj :6&+VpL$ Mthl`^xؗPߪ8xda ;ηʳlzA2nv[6oEmXvaC\C'ϥ;2ԑ~ф*9 $4!'#̨ѭ1'9']74߰c[V[C.[Vr+UXZ 4V)AL.["W`4oV^pdyxcc O<S!S1^zxxxJ&\;-9t@t@Əa4tN!3ŠTή ,ClW~qJWʍMvgs< s>kNXsn[G^A=8J+yv0Agw'|Λ^^Vmb[$/rU b)җCL+A+.W++WD4h^ WWCFPW߿F5ΐh'ILvBΈwHN^H^A z$M_vT୩-o "%[2?%)^x@(߰b}{oހa!7,a'cjoЊ| P֛Vʛٻ@!7>gUeI oQ}SC XY_Ի@x;vmN6 v0dUUe!OU߅]n]!U}הK+)U}t ]iۻ|yW4pWw*9*l!А~ӃO#}rj |vvyb3^ViiŒSx8nƷixTܮ[hw NQ5ZB /)g>31#RI?h?b=]\н>Vp'i_@ _"O66+0UC5-p+latY |IR/!DLg _!K-;ddU+S;WbiPԱ@fe8$~[^NI @x 1/s -~ e=OTRvA4q x4 v? !GZoO&ݼh9}o" UM |QåޓDMJëiH.VIU )9Bu5y}WqI:WgAo\đؕ B[QTwE?k幪JVhVט]c47n2w Ѳ&[8Ӎ(UX Ԫ|tqAg k"& kR& kXB1m zBu%:vC%yϫwQ:Rvx ֝`뎄w2ݝ +7ÈB=Ѕ x{ L=čfp[rgL>ƹ4H'.LJ ]PdA a4)ukAA/3> 2KlS]B'Bڈi_H /aS Bcیi?ij@ j /gs#\ QGRs j uhfM@pv he pBbҷ9bd J . 7.7xh^@k| 0JKX=iŸ:tLA9WHo_e9A ;`H;X-д{0M`cA d %S^ &?!}х5!Őv.ď G #3/O{'81PW׎WNiTWbth&ut~sH Ԕ⿔XPYTBTlz%T,3$)toM<ߨ AAQɎ;>EhD$2i@cBtHN@QEc@2pLyE5Aa D AWi zJ@Ĉy㐼qjbG]Vظ''ai3f'td9 69@!ȳD[.W|^'FGŏ?$b|y8Cn +VUKD+vN&x)^tA],e2c8XG[7"'㹅ǟZ8hqbv-V"!HΠexGDw[!2> Pۓ`pDnc9DZ\կBIr#Q㓃|?Ԉ,F|Z(|;,F~##I_HQiirR,A 719`Fs: 6Pxe^UrF&6b[)U0j|wp 1f+̸Wqи[-=E̳|[/K]n:WP[@e 8Ԗ/x mSyOyC]mZVx)&45i4oBJꚭLF*Lx S2 -r-]f9T MɳlIkl$E"vLdMhg 搪]cVfXpr )NgW*tN3 qZ%㮊ʎ2MżS]u!`FiS5,Z,d+J+{e&uibVJ]҃ &ao{%t;ܮM30ڑvcqט\u q^m岁ƷzkH? 1=G|FгW.ȪZʫf @B?0>88z6|6ug + U ْ]Ϧhwu`砦_j7ZTXM)*RBڬ?*71]Q(vlhsE g`sPĒd! C]9qC]/CU/,] E 8Խhu%nx_^dXeD)kA`/!Bb7|`*-R]q3 PUWeUA^RHgw)wiYe~R? m|Ɨ0m`AQp6p8~ ./wOچܴ!vL+0aeD-7WE;^RwJkZFCC6N@a.߉ m|5[פUVWq1*@U}5%qu.6 k]^&BkrQT' j#F+kvǍ,lc.6 󩙏_˯lt-ѵ_ /g4c/$i64m\oGp6XzɗQHrAJr[~q J=o: B.g=Ǎ||cO)Fb)Cn4Fҍ&14MM6qhiϣB{w>6G!ַ6cF4ÈߎݎuoG vv{{G'w$NwANcJS‰bNUU?06{]\x\ˈNȽ7"w8FA=FUHa)(?T{!IF'ތhM|`讛iٔco_"߯2o|Gn h)AA$@0n<`8QI,Gv"JZCTYLGzD^|*`t)Smf(>@66[_-H6 (O=<ȷ$a '^yPi+^8:0sHs!0?GB=?}*CC<DPcÇpaaa5y$|RG|RK4|旬~ '4#G^Aj*>1 _Y *ǗV3WHjʺ mn3| |TWytH죦g!Zg!T,Qź1Qccjb"#=?fR1bTcDM^6`SZ9.Ck݆yN>.63z=2 33H32 1ɶ+V cĤ-s1FvQѳHgyy5CY!&Kyx2t,yq\O06,] b{306baC}kve{E[>g xLH1ЀhJ_p6rfoD!t:Co"k2a X*_+Zvxy(y90޽ f-1nރΧ W7Q]gUG(G]L406F W\F)tTRSSi+(9B` A ѿ@~a _D/ )|ɻK9("(BMGW䝡4|`oo)eJkc+o1fxqLeV3Յ݄AcM nO8kkx JOk}j tRxw;<6Lnoۛ=Ltx|AW$We{b/VS Le ;)۩]} Ć=ZZPtMtC. gM>Q!NMk}ŨMQ!ޗ ݐ3`h0ԋ Ƞ 8ძ7]nB_ 1HR%Nev_|Dᩆf!n銎j19ȴ5:Q u\ܨ`'LsG͋`+Lć@!\\GWVؕӕ9> sA] @ 5Z yׂA\"ԇQYտu.)% eÈ%!,1h *_00Y ÈC/K+É #ML0h*\rijÍ 'GU\fGYv#Qv#v#"]xFF"# Hc$Q5}@T64b(F?J𞥝6 "LPS%fϜn-hgJl>|41hsU#9hq1>;F *a ALYYe}whhcQylt<0?OHѥٸA) G5jَ~!&s#v%DN8x>8.2ܥ.]^z@};y㓭+O&&35x~JB]g>*}`d\Z̺`ճN#3 fi3 nCl?N#JzH)#r4/Ч=[mFp;Qڹv(BAj"١ l&Xo< [wsFss)ŜbNd 7U c]LK\,ʍq!M\Fys8Sp$#87O7OΗ'bI:8[aQK "6*'eEˢΧ,ǒ\e!lGf8>S b]9lG\ ?HG"3)mKS9oBDvddzQB>Gףwt Sb jd,H;]K_LтGs'̽BlTQcH8#ϙD̙p\sL1gcj0R\ߜ!68hcu!wq8EE:E 3\lԐ-NԖX,*W;@=5=^#xM~'SF`h<i x'Dl'"0<(\DE TN3(` jZ~rJ'p1;8E-]>Nʜjz(sZ̊=4Inp1P(ny:] [N'\QlCU@H#iicGÇwT[^)a]Al2o~=Ͷ 2nJ[ FqY` QҒ+:!!qo`GL 6eL)tMb%\O.)i&6hFY3f4cAWT. #j^on$r]Et:9lNr\KIwIK#\:qe)#\:ba?M[}iV?Ii=ϣgY]c:[~Fd~\Zh =ݝ}V AY|J߻@y(\h-M(7_`XW1zBEi"LMjԣ0ZiWFPUNlZcaSjjg[RM..㦋uEm"CuZN΂g> EZDVen {~ke_k g3~:6Lqnhs꺜}A~8T.\>u`:r`qr0~ND`֗ks!ٹ;j=FàE5}#|~ F. w񎟛3{20㶅R1BAyTQ3yF!SgyFQGqYh$3'3P3Gt>Ng TeZvR^>m|b7|g\^@䢋D0يxn4\B/ z9{^=9z9{)#v Թr)k[N*[>zOnDiFWHY(Qյ9"х 80 EExV5B4bňئziB7E4ќ./n!`h{ a4s9K!K텅et}z)biR8PbV{A_-v)qˠԟTˈJ!䗙T̨hV2ejb#\g iC 7KX>[}ı.\/r^ T.'py.sl! {r\W@!WuQ ZWբj]AԺBA+]I_p]`WJXjHt5UUcW# WGC׶c6vUYWκ:ӞzݗnL7ljrf#³3jzhZMoiu܀dMLub:]gzμu(1u>ԅM=BM#z]/9]or,ޠjsAI'un0-TE\W_%v P_n7ǔtw`nę(Dn,D7jAnnn'۪A6a~8MJ$QK/Q 2ko  8oFn1V*ĈryXn߰͆oDX1[[:wDceaKwDW{GBB[jkqEQ^VV$VcнUCLV1en";;ؾ;l74f~s zNNN+r"! q'}NS>X9A]죦kTb?ͺwFd.dzb(jU׵At6'7~W !l]Vlwgp7C==gcxvpO.6‹ )ES˶tHOX_MG-tR+SiXT?e3c ^$^ekOpE 6Խ^y+ǝ;~>}&DɈ33\I,< neב+B쟛ͦEf"J^ ̠\,YfD.;GT3;rAP]L*ۤBll!>@"w­E2p;lٳ"Zh |%9!)H?3A~am/qƜOՌ0OCÑg>/,Y_"_Zj~2i #Om(6Q~ U^-'x?A?$<^]haWggyh7O!Qw- : =ywcO I.I*I|@[I^?.e-fp}J$@3[=Q[!kLTы`mF`I3Fޥ,zcC~ ?17xJ,A}@4|zg-#j^Ծ|%_j. T\Gkr>dY$:LHtҡ/ x7U"eNZJMv^i!nzvF*GIb'Q'?cd7ݼov{(o7611|=|_`7V/d[t.l{aS!~~*ͼӨ3g! gL3gY.Vμ \_M+TܨBʦ ϣBnd v'Xc)\h@`89v .nӦ= ^B2^:"{E*гވro_(Too bT[1ӣe|QV] eML͌GY!dxŎ&&!7F&(+ڷNuVs=]OBh ay t2dCQ,MS/L.}]3/rrxX LzRx0j|9 CQJ( %W'Е9 9 9Ĝ\lIkJitGxǡ!ǵ֖1,J e}s BƙO4~SN6ClS+|-tJ9U x@GY@eRLm+d$]>9S4Vrʒ6x҆@/ elk+~0j]h7U*̶ÅInr&xQ0ǺE!>8"y/R>ܔx@|\2o>I[WTGD"('G6.6,ODoO$݅n|NU"=ccX*9[. v"֎$=twRyab9<|P׉NuExL) ")N\rat*sS4v{;z:ݒ7Nsm5؍ZksNXMjh˖uݤO>uY}v9՘)/=w4%$!V٣ޛ&y2dd%n[H׹&[$ dwFN41ԟ dFR+r4#'I7Wtz\t&3[s]UTY.RmÙydjst߯¨ sС} t%{zws 6UEjV,F朘0pU'JVS\LMX\S%Z)"ZToqUՔ_/&`|lsY(-i{?z],:I;d;ZH[ ]*m˧b49^qzej{ }*<I'^:Njʶosqݐ9-R<66[ݜ.y%}dK˞/Qc6|*rLӬKB]VnK M"C1 W/kykyIkϳLcjt>DeFWRy[Fq:3/b *|bFGYE%!a"9!mdB[i;piJU&a2Yrx5T7ͥ>qoIYF0b5oTq\,FghJu/vZz\c<L.Y×]5+%NM}&h__xqkiN:_ԕʅ.:a"ݺnK:K˜֊aܗKl% 5n@~-d{E+1tn,-lA8ЏG jπ@K\^y鸶z)G$M1mͺט >V>̉#>3C=GӚjDL.<@uo`'V˻gWz_uVb:x')uz |Pb?J;h;H%~4T?12"ZDo%zd|,utf+Z8Ǜ5k'˃ӓg1zghNҪMBMf4Dα"-׉ݛyv9r  iu'7J+7h< "+evj]G޹-?)Y,b Fu1sD R(beoit:d]K)uުz괬jK/i_erz{;p/X,egzw|{N 4^ We[@8{@眶lsOͲTFo%P}Փ[^dV²Zsnai_Xvujy'<|qJl8I;vE即˄,GKtcVard8ZR[9:&{wkjFPv!Ż˗ X f/|,Ħ 9Zr1'22}:H,?񿬇?3N[i|(b\5ykt,% !V:ߐb#$ +ԉVp{5ky932U 4qX0%L<.`j匚7 * #}* lSR3Oe23n9 ɏX[3֫zLb>mn<5k{r~ԅjC&YZqZ"H2v%g_l04x{BKSEoUrbL?( ~[sw̒Mqaf(a;,c3>p>PE4+$`۝Xr>Kttg{21V3;O1@!s>kk#Us+Vt$4՝II-fl/KGhI*%MҪ9Xe;)O[,Pb2 ETP>ra$g&z`; 0|Xo1dMZ=" $͏/g%QM[7X[H|R+KJj;,#՗|F; uw 1; 1ǔt)[v8ѓSCڿhw}oeFv&-p!9p lyM$%XZ;hbH̴7 ֤X[m`iଡ଼:E@Ey 4p=xі9[?KʧK_]Y. 75~l>+W1^b _y]߸pR_2@b3:l) bfn7s;-eA 5t䟠dT'+Cs+WC ULsx1r!FXlHp't? Hò"Dt lf}FES<$ݩ!:ZmD砘nDxpG42Wּo\nO9>\Xu_@&]̎S (/mo`3U&{NaqN>qJ>-sTR]ﻯ/w; g~[E`۹Xرѵ3ٓyRARb+?>c^]O]?{cqxNefgGdXg8ju},4WpGk'|BEMXL5`i׭6eҡ_$11ϓ`S+IId0h|͇RќƖы%b"w04: 댘;㌻9'b&c{>K}Ls}Ar Jpj=@2[ԙ |ʊڗs,UW/88>Sۼ5m{k?Tn<8}guu@X.˭y3zh3{Ђr2S̤난վsli[Aٗmȴg^iy+c/CdédT.l 9yTl'njGAK5"ܳʡWm3<3F˰A9l%pxS~ۮ/6g[,!d`^ k$'1!JJl[(~)Ft!j%X"u](-MXR֠K~̏?ʹc<^.A?Ψ4v:hdW\˨Y]?GWP~w튁än2aL'պ! <)R0nfIH!ﱯy e# :Ȏ;R{S)Ɣީu@9p6 8u@9T5 0&1^< Dx b@>i9;$% 'vs't;-;rK6[: 3*Qwj}kߩ$'JlT?Gsta30{)1;}knVcAl*?jT$u3c׳uT5?ZRhgvư,Ӳ{kY+MF=h.Q]0rlf{J\78U9Tۙiu)ذgYV)7 ;T1l6F=/Y2MOJPux3P4֌u7[V3]2@[}aKFV>[2rxpRBs#S(V.S7Yr=Z2+* [bT-O==Q6Hsl ~\d}ǜJeY)<݅l6BuaŮ !<JL8YFf@}RQ/'r>ZdG|Yލ-lgrwфC8sa!,!r۞GzJwoe,fL,Jp 4 Uֽ)|[뭡 c*K ]-vKU-"N`{=da$wۨRﲗuQzb})TI~_g\ew,k3zȃԭl 2Z932J{4'Kլ,\(gb7Bg||rj)CMQ>jKo2[1ϐ=X1۬R.wؓڝ ̏ٱlլ> {L# ִ=c;}&rvmWμ>'6[ǚG6mAHbw `XE9G?Z0)haA`REUvWr0_UYH +sZIA-0뚨;]l&KM .kuzKtZOHB,!)q&bS1\&o.+);~/T4*y%Ay7-{tz.=(CT5zX>LGhL!y1:&gazN\8N҄%T!9Bɕ 4,*BXcILUp ZXo-t$tY+ :$:7:+,>g!E5yގE{zVj^r^7QQam97ay װ.r߶%l{BפSuΰ i*:g.a)ALWR\7$,*[u}^&144:B+C4 1½ :Qʛ@\jX%.ߑ""~kP;R 8v5$JdV@GnVЮ=E[AA{hЮVО vhw+h_GAwho+@GоV8[1:bV@+pG@[AG:t:PD up+NSq:c"=HLMaΤLEP H+F:5$90:ٞNGyn'ĈHNģMmA*J5vb=jcc[k1SABEr%֛ NjQ~M]Mns0y6{7I1y?&$-G%uNtR$q!+9W"z &XNbK69"&ÁMb~թ ;q͆;6ZVatU :6SmMXO)/#tꗕk6}{}c!>MC"l<ƢI:fCR}6/@Za~}%^w&,[r&P}<YNDr5 IO3}Y2"G IU8ThEg1C>A^Zkϰ2ʧ:9믺̟lc~0J?{#3)\$u/&F0vn {4#ߧHU< )#[iahaR$9z!y߾Q@L[ %%Y( J|y >#,!>Ea=NH 6%'eBơ)!5wxsiVERq VM 5YS]Q4:-RoTo|}m#9?DyDe1$@iI:hr-j[fA#m,K> +ܸ(KC/J OyTy#LmN$?Rd'rDNBfR,]wPZ˭^x^[&n/߸^moۻCVW4- k2F$_O|o"qaqLr~)y=§Y_ .~ej8x A\Jq߻u#/9m\Ux#p/hs T 6O^9-,Y]mFp=~+m1kƄ( 5,] .X֣͊Og%AH׭{J >+B6ރ׻}Ct/8!}ї0uk5רr? ~k?svuXT]##!)HwKw 90 HwHK "Jt#H H|~s|:kg>M5AB hϻx| x0;ƒj7]rc^o3<&a/5M0ph9UTT6!)֊ F/DžK/NoƐܔS/HUSHBh_dVf,鿪$gTib\S`ުj(D vaڌAam+ڂJ=#_cE4&ItM4c/ -1_嚟VoÕ<_G-pŧ-2)a}~ڃzi+̠2BLR>C Bı-J[ؖQor*)A UЊe{GkB^*u!yr &2R XVk4 N®PnFЇ1 ȿuOߚPpQlH ߿> (Xե c`8P b+g  #ӯg}mllo9:*x?.n8 C`l ' n-7Ep{N[?ᶱrqC!`-PH+O5EX ֶH  P nom<0iaE $*C I oD"CYCm_.eJa@jH; '7ꅪbe:J(9⺼rg q;'F-2yx 6l8.;s-O|wQ{ ïo5| (Z_!W%-NZ\?bgy5|`;OL CFo 5Ex+o^^??^Hng]Y[oMGx0?˯e˞kd;'JysàCx&0^և׀"rHT7eq;D ] D}`[Ŕ-I@SM\A{S@qjYd7]q{?90hW9h΄hF7Dq \/A- "|g;hý&|Z&Jfڢ?8底uM&2pZ!ux1Bx [O"U u9|9Q/VZ{MTAqo͎o{A0}G(mPfl.[%7E[E2r75hk0o):~y<5#WV44bf~`j"^zݹ;*:-Mu>ZnW֖.1spcb}U{=̪=ʪ=^"YlѢ`f\bkK9&_0jC˴!E\]y`A]JT'k ϙu1iz< <œ6,s}rsN GG+BEj} S8.RZq׸9ֲPYd}(ɓQ7M6p'6V O1EqIyD+*^3TL!֮.xE߈FC WIQnmp` k%nJ5H. !tfkLT%0w&gROnHqy=cRzԁF׽R;R 7|#K,TpRfϙ8Koq :ҐY~lpi3S>4>Q*~Lϩw,$MU .O V`q=jIT#Lυ]9 ÚqM4snخ_0|yWgSy'[A֏ʓ)m+)YŶۓ0@G4yȎU&ǣ\Hg*l[5{tl۝OGp{izQFVbm[k|1< }Gd ;eo:~n4=/ݜVD z_vr8TV3TQO#k95n+&?7pR?7%+Ǥ|-1x9*wÝDbɤg(%P(HZ^I'j0?st<bҁ/'pW1efR>0炯ZVG_+R;ūc"N0qgV^t^oQsthk/&mMZ&[nz$=qu4z%-F12ORzwO}~rR蝏 ̔r  J5QY#ybO߯-xb1ek5׬ {.a0Кe4ɾU7WU݅KeW1Lᰃtص1q zzS׍C5t7N/4Z2_*/t.ը0nA_F{2H)Vo yR",`ҿ|,N?YT/<5?aR61Zr&5ڞ Bd`ЋD&6yLRB@:䫡>K*f G[xsKj)xyZ^\~UYp9ϰ(-]$]@- x8#\5ȃVG"p!X_`M5hPZPԌ/8qUXjvktD^U̇h.S#_?(\ 9R} I -85@s,|x +WY]v? &~c2?!KB/|jXP%Օ$K@כ tg :#W ^kO%<#3(-!FLm6hdU%[g ?N՗<+(&*W!P'zM9z =v=C±GBNԥzÍ ct4'3_RSy ?9ŧ^"amDzb8\tt  ,0M` ƙbDANV^:c}O4F DTNJRΏ t9^q(n~ &M\5#u Zb3 dl5ޖثi O-L?~ZuJfp p 9`'魥5e9hJ?f3^3"҇XL>(6b9ccaQаK ?2qh-JkYC+;,L>$(5S`7T]$cZ;,2N\;F{D޼Mͳ >BJš6<(wL7ste  !|Kzh-D >?&c1NE8 (YЁ` tXBxs}Ka- eX~fO\+H/ Jæ_+ɻ dC'YK۹ >$d$3WW"3AITw^66ow(8رR_կJ-k"T2i^wrtId|pa t%/&~\udzߙ\(~r 5CN7~^|}T^ݾ n4ؗoKӷ=dD?%WA'DS CyKچ%ca8ExKB޺ w\OgTjo㔈w_7#v$> ݬ̸YTIo#OԏOݕjڌ,_~vFپ?U^ۻ*L; )%#(''Ȏ6%sG)"! %9zАN?P?$446IG$ dcH$HH"qdhz ~bϺ2^)Fi&TX~(4ͮ<0pno|ù`dVJp3;+D,>1j)o?7Ռ7l'ՕTו|F?)vu03OMJb*8GL?W쿺C/A=<#wwƖ)>## ޸9h&^vcpw諾K|1Z\l&.IƒsN9~a-1{0λS&ItJoD^~!}R=CMMGG߇YUPDc qʤj]<쫒/ ?/)W-}s }y%byzހ_u28`KtYY`ꇤo`d*ۅ>XYq z+Jp:rf][Y#Jujq<-Y1zS .#宬8,X&PƤȓl`w4hXugбCn"qcvȮ^D@E&+l&.$YKN`=-^dUT~ҶUW>twNyW;* "kdi@{i K &J˄"VcK`!Gxo_o6XYX&D8N %iRx䃔hvſfL.6p&^d8H7fԀY(su`v$EcAJBʗџ/DK9}kɵ9<-%⯯;R{:I51sM1[_F8u=z r [1ޏMhu!Yd;R.&53IoE5G = UU%bA=Eq qt9P ̓^[{>}Ϩ'Yg~z,W=H`I&'ot[R\^ŅSq`lñrg#{tkES5Dy= #\r_w6ET1#n[,Ȼ褣Zݚ4_uI Hsx*ulC3`MgIWr~e,aìyk;M^vuG|hY$DϊC逘*IԬ': Rsl-w>kZX/o,Kbu3+~m+t(VWf6!`NEb^(=hlrO˭ͳu9(nYS8hwgnj&ش =Y}m"W&jĭ(Ba1#\niQ WurI qeIed^dÈp56fP#_d =KF>EJ)<8VY>!3Qe2ʆj&QYlflۉN*4Dɘ_1k9}s]pcrG^$gNmGCon)%(Z'V ɏE`>j (JJN({%f]9.G ͧ~ʥ\/~kӄh.N^0 .n6(n:'c89An4vJVy3cE)|1?E}8 |bޚ2/(6&V^FdmhR]/.BY/]#Ft!/nR^|ѮLwU=~NV vugӭ{ӞrjSٛW a=hetV$(5t^<׋7Feyb)u{F\=0V2gYe }Pt&՛~ӥ~#y3ȵp /ʥۋtw4ை9Am`17^wstx2"9c,D\Ztp%p$x i@5'#'o!Sdl ־ƁU~*mbf%CjZ'd\^)jsȬ ' 7ob)UR( -y0DD ;Y!$)J&Յު3Uc~X%iM=*ڲ'+>l*[X @ ؉IIL<59tt4q3m?)A&i=P4+/ *nVfG %> X!%iW-SC5Rr.g'ţvٲ!ma"RlM3W:tS L.-$Az-Gl7lmn=ggXi},BEFʏwsapZz i'{R__òev*K"%yQu=U[Y! ^I_kYMox~|S} $G !*HT&j;\ͳýŰN2-\M}+ә(XS_.r3 =lmXIh;0RyJFʈ}=^ūaz̖^ un5j̰԰f6vngOV&Xp(5NZ/2FU;-w1q|B`T&ųϙa,*jQ(& K-װAC>=vvlyP@bA}+8Y@DH- tV/ b`? " 6A<A0 "! a &:8r8Jסax#&`3x!-0od1b!d)b3@,+`WqAErLv8/it~6c0Y|'pqy8'?-#7qPKiR0?relx/ebin/rlx_file_utils.beamY{tUJuҷS  B:I#T'$ G$@]In::(Lj/FEqt0xf_usp8 #'<O|.Ⱥ8H*ʧCi#YHjxR<Ӈu#KlѰT$;w)Wd0KiA =%Q1(ٽY`#Ft$ӣ9\O Kٲa=vEYčpbw<FX8ɍdY[1OEE:e|f#´T7ytrbN<4X6dJ 4JDrgٽD<5Z'wqqINh:<Ջt372T[kApg46,EߙOEstRȇr 8#WLO#M!\*r5&%czUt$bDBOO*Qh V4odaqgtJI ZC$&VQ9O tkD}QƇAe`DĐeD-(g=)3dz^ԓQuAiex<\Ó1ɬ:wQNcU ilTTςUii)_6+T+I0 }VT/]ɅmPVx<$e644% ;xꛮBs컖8M٦ ikn %}$djtUILp 4~kM c -MƷf"8PRQvLhl%UBvh_>#;D!k;dMv!TBȤd쓻Ő,RG3\r_bqLsU"C3Jw}̸F] _4Lc^Ti!Mr(€CW:C%UE;kgrC8nW,.s9t'…4KW*.-yW,/K>7Q90Qe`ykdUP&RxSt(~ZYH5ژT婕0JYy R56dUXꡛ*VjE eDU|S ҊAw|l2Fk-jS\ :eSUhUhK 6ϜN3wl8&<QU:5ZL6!bV1,SK1zs'VQNMs9!Ā3O#>Z5>i_`3{*z>c,6Y-H5e~i@E)G&"]2l択:ͩhՄ;>s{*zL6`ZSDVoo_RiR/upBjSD *UR+ZXJX P` FZ|v5!g-2Y` O yBjO yjC4a^@D<@\[l5kf! س&Sc<& .C}l< wgjۧcJNn"lF״KŠ+A \ΣꏢN F˺?zz''ΜG;wѝF΂AVAtHUVyo5 [V{'{j Քw;T5nTSީiZRMsGHUzIUgNz{gxI50>H5#/ygyg/rcI[ٟ[%%}! ꆶ˒ dKԡѦ &OZ8,-ڴ*V湱׹l_ĥ$-I7 FlSnKɼ||J !nvNjPC8zMĖIblBL됺n.]ԍRטB`]>-BնmBYAD=(4ZDs$R:ՍhDd P1K"QozANZtVIHdrT(-N6 KZ'[cℱ@,ט,Wڊ4qnv0mBcz-'WXCqyedmaMۨj%mjd]l)7]Tug|שdz.iN{/Lx dz oW+BkmJ_mkK L@6}C}CWTb>FWQmd+ $lU8f (I)*- K(K M@_B - t>8,k MLK`y˼]VozyG =l=(l;l7݄}NC`wmxતޅUL]I-d!eTHUg+Yn'j4Y?m׾bv6p1~\I-MtmK+!o [o[[9Vo3 ɶ V[guH`1Ak }MQ*v5bm 9^쳊j;62WHaKE}퀁+Paf,;vǩmJPoR~btlg8BpЛg,2-t, a9 .^tz]:*6 h@z D`)`zt|+.Nc HVQV Ɇj0+ݪȵLjyC:u<&jAk]~ x:%ӣIzf.Qksj*68ϡX"v;EnϬ 2xt∑kP0bxl86S_As'Hb;l~PbORAX/EvizDVmBMVaU8- l} v' V~;X?B븠W=%g쑕fֳpc #Мj~xEU_2(ؗ'mHu Dv*8|Rҥ#1'Z4X^ֳ `%JPzUQW:uH&ϖ3.T![i4}VqL`j\qt 9 5Ku(6 ǖ"P,X4oB^`?5Y v+Ev5Ns~JYvk) }W8&i'䝀4F{.%Ȝ!-~[Y 6p hhe\vO WIymc'q~'sy*4<ǚ,6os6[pl.S?%fW̆Ӧ%}hhQn-#6^CEeCm}hAϫȟz2Շz[sꬆTL5p}鯤's7׿1$׿^s*GyO; {N8{K~dE;oU=w; _7GnzϷ'>3ɥ*{+~G6t?{㩷=y |#ym6~Bu9G[#W|Uݑ#}jg'΋q_澽zM=pr ⿿̝~}5?8[~߉?x/S\Kb_ t.4>cGO=Ch;|m3߁Ǣ}}h>/rΦ5\3k5; }sm1 Lg".Ak2y/F\Έ)cHI!ʦNJv)Sݡ'%ߓ+}rήU!Ht(K 7)\,n:nlI'f#JF1lXsLD|ĞUPQ;!0:X@Bp'8[pACH xp;O>~^]O\\0+W3 nwyzڣ؀ll@:PM%YY͝l@X60+o;sW[7%=m@0{' s/ᅡ ًߍ[.O1 Wkgo&?  !CxEt[Ü@>om-;;Xyz@ɡZ P9&3n(X 99iraIu*( f;q>V_ ioh.vBTqZ&|dF oKk+jUν}gZuEgɵzkk[!1寰]hBI(?^}X7ł7BV{b]0)'ʰ*oUA7ʍwޜ%$'v{ z_C'B,̭-IUר5oʈӢ 2:5>}6Ȑ=zg:NciFB{ ݾ(L󚤽g_)hJ[|eg襘=/͸e£Q|45~8~;z?ay2b٦9cjtʗf6(_F8WӴIP7+7ookNA\)7}rdh0O)k S b#nشJQ^KW:eW *5"T˚eKi8| `Ui\jH*Ny%VuMJSF!87Yb%l":]Ft~FCf_'wfG0`Oqn ayËLV lL Ʊ90A-b6~JF:nfk<F UTgȎb2%u`.|[Qn%$5飔7.UKHەd+"ڈ22 Ry|N"46jq~AY#Aq=$=JqkGeIH?4z,G {jl:@㏸dκE2=+Z8b|}xZ M}m-fDNa|3z:QB ?"ջhg0=cw{ 0e_=Ur=ş'n,|XL~FfpۿU9LkXIWQrn-VbXԙ޲k^Wxm~L@ W*M&rSۀtn[sH7ނuaRxsm曲"vv%#I'q^Cͷqmrѥ תE>%M;5_@N?ȗwЦ%Cӷ7"6M2{k'Yr"tS_0c憵c& UO38-Mu Mi t h/sw+yUuLl6y^ZDy; MC4Ƥ2_kSi.#o~#oܘFRNԹwky<n5N9$eHVk$-װw33^'k0'lP) ڻXI#Zms+K_7IfCpBBF ܪG؁JV;Q["L[i7>2oy; +j^zWEl~ď3m8or~4s/A|j|pu <'' ôYip^Xaa~#FAHCRĥ٨Ԥb," UzH~Lzi[nPpyWo`Z໶>:QZ<&+J<+IY\]I_)3` Fa ~ aPtBxT>x3D>xqF!mU2}.?4dprIUv=:UnfH#ZSHnyoqQy7eh#h?k* tjEi).[, RtHr| Xu_R>m)Uy+{$O`ݥMޣ%*/ߥ}Fm4\|:5Y|83%=Z0;[ϮkЬɎȚ'',y\x7n}&~ܽ#1AkWk/+V*0id|-^đ45=v,OTkV71~\<(.af'c}+=Pֳ=0䓫Hܑ9C #_:lނ)$8?gwTf=OR:yufr8-ŖWy,QVټj@ҕ#vRTBiº a[y#מVcj@ 9G$7g9=HJśX$BsU; (ڈnEgb:D;qgju͂5d|bp0܂f=vc bm[;nI6n&n8Iو|iXO\\GgaHPΐm1 =Z+[ %nSȡZ,d],o$?+vmt~QQ> -p\[4yG+q>SZ٥? s,ӦUIa>S:Sa} 7vE^UR78?xY1OKxmB+E,YpNX)jkI}A+.TEc6URMaFD١~;G\N%h:WCzXZJ*5x= YJ|=Hלfُ֜|zs, Q/_9} ^r;d ⤫?^#ti0kt$7ɶtM.wd‹u]7BBćΚ;O -SP'Y;pvg~'ǿ{ϳ=m_>{U1h0e4?:(,08Lq vxgK2S̰ie;xᙹp M^TĻe@IHObtS/tS*Gx"Q!cu>:B]9B^aj3'JvG Mq*-mD+7OQ!ݣJ](L>waB]4[bYDm? 'gRDѲlWǝdZDxkTѴqf&V,]cpĞ{j?BkA5LBr_:WzDɳEّordNeAAyxQaZYѝ1Lj ),:x HD$*Vf^P~swgɗ5.k^.; GcɅ>k_NZȎ+j;kua6T/Qc3E|b 5/p0.Gy|n۰xg F_^`'aRUfW+ C#S}b=K2^x?LAF(:e–>}2N(3VƂ ' UrE2P>LLYSuqoјu}LeipM; ބsOs*}M}:MQA8T1k}wLdL; NLZ`ԉ;^N#qgd{U5u_WM2-o m]?*Uѐ߇\>1I&>@9A(P]mb͓{AqQOz#2qzPz#$CXS1(UUA=6בRnڞ/)%d׉L4[09#y?ƧH`KD#'+1!W:^"^,:"}ҏ]nNqyr_>Ƿ`>fetևa8| @lcG*`$ޫI~W9YUeW&Hl0'҉ˇ&?Ys 'H-QӪ ]dF7qv*%܋?43'E?%5-4D?'3 (S _\X[Z_PZ\:'"=l"(RSRRJ TX^XZT_T taf^Nf^*ȱ 亳(f=fd%甦bN9Y(K-*E6!8>KS)>4sF($ӁSs0A-;lY[Ox䢪_,ӯ*jMe/~WW̎^T I!HÆ.Qw}_?۬ v~R ɕݡWnl*쳟 ]eƥ5#yAt^Ե>[ Nzܐ:b.HXlYO1ښ,on9DŖ[Leeܖ~DtZdG[><̶ؗqO1VMf|d~ݩFaFu[z2=}7O/Ky}|i ͯBT7PKiR^mƻ=Qrelx/ebin/rlx_overlay.beamXklוWjȏ{#D?k2۱HID#6E2$%?R&uҴqNRӦMvbAo `߹3(Ezsw=W'== =_ǟ<ޮ/B|K(`Kzu6k}MVrnYoRi]݅Fvg+U7 YE0/`uVPug+Jݱ6%vx vJKf/.JԭVVu6VZ5b}Qa P(1Y_>[\mIbѱ Z-T+}PknZn+5I>8 Yvf{6E bJsBum//˫fU81\RRXpjf&V+vw^-U @݄fV BkTBPB1P tnzI9Ԫ p^XR8ܺmAΜ Pp5EU}6[/йb̿lmVJn /-Vu+hѨB6" ExFJ -bVdP,UJ=] :xq^ Y+.+K1{D-MqR- ,.S=M5kZ`-^ ,`gĭ*x\?ʼnU·JFXD&ݢ~ܣ"jdW]Qٺ:GT4S"`jW_QKz[ŀV Z|0Xw0$8^[/Nr0Xa}wrZ cM|p|,` =)]Sn8J ]Q*uWT#wE5|WTNrWr]IVar8Чw&RoC%7apNF)  kG|߬SC תmJY/z U0 SeZ]o'['yhDۙ쾊V"'mk, m=<G43z[u.M[2SL4[bGfe C1gL0>a<>,?7='l4G2ݓ!-S2z8hқhTfes,JahsyLY!Ek `Vʙ4O~Oψ?ڲ!dMP9bYn[5נՕuiUyhuy+zgnWQ,uKdHcH'9eh_Q}6uD }ms261175 Yf7H>u>&Lťx, ҙ4uh6/A.㦝>3a1=a[Ōؖ=,ZѹD6 r@Qq6@`ʦMj7)0__ 6f7KW`زEqsǷq2H! ]߱l;!{nM64 6dL}elBr߽@=X Q/!tfBB4ep!D;Q]KYoqv+6pyȟ+މ5O{- ` w):qg=&3/#BR7Ҭ|OZe`9`awizl"\>?{QA^?6X7b0!Y%ేřv/R6#iU1h @}Rq;;=&FD62aky. Jb ɄRL_8Ru-XQRFWEAV x! o zؾۆ-/d{|;+ {)xH!SٌIϚH'RFqL8E,R"@t:lvDa":x42$jQ b6cϨaba˜ T>QEp,q+4鄓$bq~,\PNҟ1S&ҜQ1;@Q1>ȬGS&,i-:<~eCB54۠ b4<0$Jy(Ь\H#8tGJlG$~ 186 ,Нb\e0EadfV11ڏax44?>Wcx<$?D2m2edzP0_FlSʱs7 ٌlCI)gXle)aOSd,I!)NMkO&0hrm^Z#ؓi1m{,=~c}ȷWM<_Z?c]9㏏=rl~FmnxT$JIJݧ8 ' #= qdYR\:Eh&Mw ǀ9!GBJKL8" tCBNGZSh+k7\J/[9DDj98wd#D.rZOD4-wJr<= ,rNUYΑ{ʎ4ICN8|L~&+-i;%DT.L%d+"J1qRyYwP1^Rwi9e"f%˴Ʊ"婍ڄhn#i??Ds+Qt)CrHUH7_,T^Zuv_Oz *cN2Ӝ,vWV9MZ_і;teqϐxEb3?KauLEO*JWPL qƌl83& d95Ф B 82b#<~!h|*#Et"x]$Y|nbix ,b/RL{lzX*M s hc/ac{w<>c灕 Ja~GD-Khh^*m/jڿ+pdB/M/qL{!TYXWq(ߏ?GA9"f#gJffT 0'<%ܷEE>y #__'u_xW@|ǿ!ZKW\K`zK~Pk3bA Y1J2B@Gqye e2BuQe|-@BG:} I,Lj(a?<$%Ez۫=Ҽ["r_*|Ti;,m KDXZovRXR py/E_KRr"UD\E_ݢp%k2* ԯ];m1q=0 viMu'%_BбkryD ՆœSm>KiNS6^xyeC%Qtjβd?{x\z-Z´}On#Gj j_T'KS6x{z{ ~$,5=Ͳ>)f4c k`7~O<nYǿWJ|ķc)ލE5%Vޡߤ #<>^WI (<h1~F꿆FK]Hv $9 /4srzsfHLRFI:HzDM2;M=4C[_y7oŧ6qUA._<Յ]4^ QuӴ5[<xn\|ה }fZQA!UTOnK>r4yZxG-[u+*"{B(Έ//Tݿ( J( A+6t|(,WeVp* Qc?#]ԕݡ2>@.[?& 2J>/`)vuXT]-% C ]HwH 30 ! * ]") ")wwy{u޽~>`3@(b1A5$[ 1rll909ft!c0Au `6 z$Dd ` l0X ' R,4A=ב1)`cS1ArlظH  L\T3@Hx{8q36<66R#I"21D=7!U@`%0,L܈ 0@" 5Wd5A[Hb9H`!F-ਾ@m$:3 6΁'06j,.4Cp~/ q!q)4.eи,2\rEH}x(n菑0t8j-t+}-'=XπG6@h{ϱꮀ# jt^O<h>@~IH4$ AOyѰfxs(]m^M>hP&W[LjJf9\k5?v o#&8)9sK7S؆Oc O6 F(0bs$izlղ]މWf7yE{Vj*Q .T@R)RbJo^Q@e/Rr (#iV,)}#816݄R$$57}8FJya= reܩMG&}ޢFS{çaLg:ݚ7\ O߫0~KWM`O02|Iv&bMeHM '!} M jGiq>zSD S~G G!F9F?^u TS(w=lpԹ^QƕGB<%tq$x5EJc8Q 1b}ĠZS{x^7-;FZ(ʽ{퐺IQ 0F]=0wpg )o8Bm,=[;{+Oh|Y;l1`p wW+o jpEmm10mCkXhbh? |fbPx[!`gVvu١ttxaCz\>Nnxb|||;ac猡1OcGC!B4|nx<%e.Wc(,ty4q>~h&Շ֊w0BBT(og?|P$ͦH}8kotV<{|#$ IxˉwTWpN+w9kO%NSF|4B.vbs#jbn_Cr񶵙 5{:|[{7I.BKn .-߷KwufcZF%|3w]FO$[U6uqz [sy%nQӯ 0ǣQm͸E=p)Te򆜇^py@LE뉮nucLG ވtӭ9CGJG2dDpL:3=!X]nph\* sTUک3P,I^ B7u1]uZމpG).|7Tӿ.azD$eZZ~tG]gqj " /b) 6p}* _=\|T9-1q9YBru#Qk~CԕU#\J'^1, 9&\ۂňż nقm W=z,a.f ֬L e+x+<{֠Z:+3/Uv)yCOzӏpK3[uCλ^a:I<0:8S )>IB{m_[O" 3ryYGs|Q>.Ó>= ;O~W┟h 9ǗԱJ%eB9u_~t4 &XT V;;45*rv/%_At%92gФo`јOf[3?4=} k5ubz.a^E(hPd0KF9ID:ԩ[ORY;" i4\xJ'111@oewU%<,ny'vbl #RCe};2 Rtc4Ci#W5#6Y޹V7SO >eM*x\Gvg n&!ˡd,`-022]bvHU(5a0F>V+]dP덡mq*uiR>k|ZOXusj523rn%!;3Ԭ UT_Kؐ@b2Wb2~brNřbسd8_E썦 oڽyPq.++K{vEt5lVr] ]%XG8.ښA,S$33o.C}21#ɗ:v;_Lڅ w]3FuGjKDPo.; c}QE|}>B]pkIW@/g}3yRlĝLI;?n=WjrR=k3Lu>ު-NXȿnwlGc<2Wke"hew9KuXae9\阙7N,L>Lh*ֵ؛=1^hVX8xmcKhcg*!:0ѩ,Q?8w~_ Eݱ4a"ث*ss6i/ ٦O7zbq^L3.tSޑze,K71g/^C7~^|b(n<*^(OOd%oh׆- °U5U{z1*njk;C9O3OM?LDhFkcE;KؒZ+->yF.RTc_ʚbT^[Țj}O5 D${/6{џ;ͫb44lypBpw1'd##5ϛY< ˥& -XոG˴#4B./㵵EGO~p-Xkt[ٍצ[k(釚Җ]fϖM֞.ӡFcrƆ5;$.{Gy`nF^Oﭸ`KcFcAӆʩtΘ#kݲL`cMu'A#M`mƾO_=Pn~}ҒhX՚f_\22Ҳ1GT^6S9P?tRYȒ|0:Uqm**=f})Iyy::Rήm,?gV8] 5? odzAmpA)0+ /<}[~ab/I 2n̶9Q}cJ{?&}wD*=ʙ㶘+X $FًTUG4I+,z.<`(= *Kltt1< Z32gen/DjDUn^> +{N"#= u7,uitZ[?uy~˳~#׭,eAa.-/FGmp" uII" Q꫺uwvfJi8'D`^膶mW3iZwMkZ_q7Jyf/4ayNxO|80hMtArZ enU`!]Bf_ /o]e%l6жe^NM/ޣѼwZF,SY'ff *mI\'z=ԦǛ]Y;EIӴaug)q, ؅G| 91@r) z/8.Փ0ҥ*^J.nl,.ZjPYHZ#rH,89vl*nzwڦE7p+Ҝ!6C4ԉb>m n_Cbky~S&eUW0Fg*`g'7g|yQZ7EIZwCu!:v*M3WF+Ş.m'N3 *\X\v^:x jg\auȥbШIVg"JhV_1.LGa+Wt'[oz&UiK!POiP}n7MΤ\v}'ۢe\}9XPwM'=R3KtԫC[̫x_9H$KHmHƈ,qڭb_{9y<1ߴJ#0u³L Payxw_#=Tw4)իoǽ=Ceϙw25(h˪Iem#} YRzT",dp?m,N`&x\WC_:Iݘ'X''܊)I6g|8g|kﻚ'''gyYBs/'8K/:g;-gn3SR Lm>k+)?|xxmVdHJY\QThtJso2x[/uHy9 Z5{"_fR퓳[ɜ:`9]PI\j P+[{Z-핃tWk3]<Ok7iLdK&qAB#E|̟y|>dEUw85/k$m]+=. {W"#3{ck*d;s9jl<,/ь]>[|*iIr&_ (GLȆ1ٲ}%ik4yTQcW8"bqYjkIp5&l*K a5 &9^Pt 8Q 5Ti)ek;{gߙ=siqYWFI/ӕ /~Qpdpd&o͆+ Tsrh%j?lX:-'Chޞڶ@03xxp\P817ٵY)Ek_. M 噛Y ޳lQگ#\CNNy8W{7p+ձA6#x q5H#rn]M oWpz\!?(ڒҔua2ZQZ^D.ro][̞WqH_e 5@4}cCܬe-qj$R|u`T%r4} ;!ڡ!qy)ۧQs%~ :0\k<%a݌Mn)M fmI.c%yIG!]k}R3>i}4bd>i2޶?,(9߭COEĉ+ 9\p=J$ő1~%zχEm?.WX뜊ljVM eXsw˪kaOHE:I7qY׋?O{rt?BW'"LwG3C2kU "_Xyk"qz*ְ1>W1mqk$U<=!zq&Z݋#yxe+bb)+䏪,l.ӏ&nL+2cIӞn|$2Dc;r;Ĺo]ihMph͆CZOW^Sç:; :=MJ  &.u~DPD2lX;[K_iC/H8m~K1+t.s`t 7]»)bn}qgIFo^,aOn 0dP0yMH/kiG͠6D۫_ DPUR_`~r؈Ҍh󉧔??(qxLj1R棊2T/i&J##Wl%l ¯cf:BU!R/V+?, e\1:|쥘%O]=bYc>v<*YN*9%0'ϼIRScboŷӬj F[}1 댣.\%Ԓ:YVt6IR^Wѩosxp$KZ*:y9c|*>jժlyEz{MČuZ|w%x }O;6˿a.^JC-e,Q=rUE.ڭQĞHw_?]ö6$s/ۡmWmiWoctn긛 >W*s=GGDž=ghέoWxf|]\Bp f- IB~vɺ2^y_rr B K2U:B3{n_f3ՐͺIZOm40ɗ:tM"p^ ؠ-g\BUbte곁[32yY)Yf% GJ2a m+ TPar+1<9Q{ d>y_Ѵq c?R샻  `@ z D `\p6 2P*@ ԁPHtFX+ XG Aѐ;xA!5F nzA_c`LILi0fX`K A^ A iBO 2l G z 9C.P JRt(ʁr|j:N @%C)$ A,+A5t dDRA:+n` Ǹ9:ZbN;8?C[-,np{'D9[b^PKiR^fW*H3relx/ebin/rlx_release.beamY{\WO@RyR"C DD`*(>jjqiE*ZݶukwJjm־~N>7ss{9ɐ=$N H5ڌ I Aܦ1f`g0 3ߝ3YklgSg23nlKLÕ٬6`v9Vlٴ447MjÍ&a0WdvwXvd<-V2͡~m.c0fW2\Ƶ`naTeZ`͌EvŪ7s~Oj |z4CODO8i 1Y v/e߭j3ջYmM "jq<\XLȴƓWqUg@ @MF=A, &Ó[M{4M:+l2v3"Ȗݵj6]sGh7uݠ]'ֽRDf8d5 PQVXZJ;0Mh\:GT1Z,hP>;3^gģX`,ƙ22 btɴ+)p~^BH4D.{= ]Q: \Os%Tri\Hc)ӱ &PDMWӔ(zƪ Ua(&Ia5KH{IaRnPE:@ ׫c)W=l\W}K9{;[6#o8o #H52 !<c׻Hʅ8IiLDS#"j[G#{›E)b0UR{sNz\(5!R9R q{E9&ƀ=1 }D1 ^s~|EJ~9UNCC$_M߰s?9`qo0j"Q52R0QFㆍQ^*9dHyHejRW"KSK`K("HAa ":SF׆8%&l9%$ʖ+tulI@E)tR8=t >J@R-t(qfѹB<Q_P)D &@0)hoa,#VDXa*e% vΗ aP AClB" 0?f&;a]dz?pDQ}<5"TD '"x\2 E ,&UN$AZ ärp2᏶cmmgۨDb 'b f 6#4>RZ!LDrtfa^ȍ"SْB'ijI& ::AAMtSY|Q5 ;\YܑU@L Ű: t$C "Z+fqVWݵrbhK (SI5QјɝIq[GGO b%t(@vUP`UBRHRDR%@GH'΂2NALǽ) Sx!=ԥTii-fr҄LnKPJ"ftQ!ħG\,Ri`u\ ~Gf*TBTsa3!*ZZblȒhI0r\9,> @ԫjA!HXbhVMh5T F!n^( ֧GLH@KDTTE/pɄQHTL=Oq F}0듣>D4)X`x̀sHELRzSpjQ)A< ,^ȁ>P  Mq6􂞩xL6foffof!>7 73qxE>/sa޹IP &YNVɆ3JK1VPH⣅xS Ppr|AZg*ؽHKAM8ܞ  ݃<EuM+8JP`yP\| xRZAѥ Z!V;D &"▦ -Ur]`E6}2DZWNorB'ۺaqdAU̅YYӶ"6 _>ͦZsz>hO]DZHf3C5KrS+to?OG%;D^קodan9pIU.O{y1c{_{v[[?yssgMM?Eᷙ ̍3lVy]vb'l5dNOێچmߤvS;g>.8p{s|a2SeT/?^Sԡ_lX>h>?ҋMA|k-&>g|;?!_C/%6|Д Z3xSD ׈efip1 _B gz.>r13R0BCbk3)ܑ0lscMLbal󭶹6`KkZLfc3fS b(Xf|l236=n3 Fp]?C?Mgsz(zQ oYLz`wCl04Wj1[ }0ΤKy@>ؚzVk;]z-`F橜ʱ%n+;-:vՒ?nپVhJ\)~gRo++eVLuɏ?5>}9*5'Q_6hwfi7:ilֶ~kB9u3r9gMiKb̍lᕶgs1~{'/~}~\8=x~9ܞe״ Rꔾ2* ϭ~SKBbmЅ;ߕ|_$csN*d!Fx #Uj}jVb\9o6^㏻ Uf܆[4E}y#cm |x{/H󭼾gTÚkϮdc6|kW'zYNn:/x%ѥ^/f jrSĝ?9{Nrz'qe]?UFm/+0?_?uNFފkh<2kg&jo]ZuYGNi;v׫}͓M+2=*g`앷o^d.۝g/=•<aI/> msGљ/"Z9l?m6!_!?lg#2&9z̲Pgg egcrteLr-˜(L!Gs(ܠ!i91{-)݁X@`~x?`>@Qpyb ރAahtKNecVmtu٨|Y3&9?^`wai+#5R|]ד_'z*Wo0qoz|F8jt{'i x))o@[)-2HLXCCC VocWzVoX+EhF,ck!zװ4lJNʰ{'Cc=b4dKl( (xŃwI;WUry}5s;l=Lز>);ǝI`|@$-h???|GvGGU>c;cq1^F7U鈛6MoxX܎ةGp1fi>*LnzDhqm((3SQ`FD>3lm;f%?%?fe<&J~>{5a嘍4z/GF"uJ =AϗC[C[E;;.www;5m@~gc1hP~+c(y,y?_i,Ĉ_t՚`UM:E .@~< Ԑ!k?/TݏnTb}b:~/_\̰>a9-tŽ=!ZlB'd @ub{;5>G.6 @HYb~6[YD( t3Ҫ(^pI?5L? J4StW]aU,>Ck2l0,y} [}.s.z/}b9]j}Zlvd o"MO]/xOB=iGdT_&'T'B~ħ_b4 0~I%  ,S8ET{)U ĸƅ~S*a.Hs#r 1hyt[Z3 M5N59lTQ7[ 9= ]Ci@^/3?!:c/,pזTr/cFЯuK,L,~7r"|p2J4ңn>.ʕ4l~M!{v:iJGUk_ԮwiQ7Ct81߅6*Jzj.Gˈnj"7tϪGob_= {R@3g4IF9o #U؟afA(M3HSEc: 44/[M$;,T4' dPt@f-ri2{\[ CC1 y+!l  C|I £(tE]~"!VqRԚTZR4}x4jQ_4 [wb7"XGg8~fp6R&S=9!HFdQQ*$]FÇ((lD0IF lXyjI:ip Jw.\$)|5UOxHf6&dt?{׽>MW8Lnyasz=v?w  w~+ ~)-BӴm8\8\XϲEu:k:?1/ڨ͐wl1(xC@Hge*( ؋},q9ő0M9HTa5BrR`/.0/ wItBT`z)cz)ˆw)?OzuG\ƹ\&qd\.\.\2.Wp.WH\eKM RwWWޕu6b^ɬz*W-4@%ds?Wq':yju>J_FH Xغl86)[7 kZAqHq1 myLzzʯop |@h'=.=WzX5,$Ԡo#!5ݦZ{U[<oY'N@0NOz&+c'; /NApXPR}^ԍ2d_%㯅d!eo4JjTRC3H 41EMp,{IHI;$/h %%&9q&IM2 '7~c9tflDf,n}nF@ЌS {M|k>]Px;e)LBj]]S.%ܙLM<4 \G qbY{0bpbo N#CvUJxva2K,:Ki[ѐ[3 ^׮o:ڗ@pS?gO4ҎEHpӂS?=9XD "3I;4K$35(y]E]g"1|5&25+tYRߘ%,Ȟh6[X>f8[^FŐzg-^4<;q;s#q뽍sMr[vvq޹\\0/ib3OyXb21@d~fhO ?-2ز7 R4";H"tsAB\hh5nEɢzBջ_,k13b dx'8ɩoI"K4Kx Rdi,@ e4WS!ː4g݆GD$@S! yH.GLEScy lBFo?uX7i4 T8eJ$WFgisY-N&tRJ+aə)1D0~tgJb G0~t5gZb:,`hJCrM  bF5{8< wB+9!f393ؒab_!s ђx܄ngͽDc{ۼ16L9>Y[O9_[o9vNŝK8Q`$1B8E"ORU9BB-H-$D5bO61Bz&d vTx%@xD8D= OqMlN?{͟mep 4zz)@%DcY瞣J*-[=-KeV,($!wp7R8;XaCCVޤV qA t5P17B{@͌P"IXY0fT푠0@qخd6ն0\n j P:3`yc"2lFaYeC[ UXKS.Zb˥*I&Z9<ɈE6U̵mɦ#x18|U(SB86Q&IHsÝʝ$P#JV;d"[Ad+NJeJU,Ab WH=iF`CYfACC a夲 WIB:dKHՇ((jSqw20EA:IU+I4O+ lhʐ$Y8 h,"$Qw:(2p1j`ا$1 0é#h$28qZgtz!Q,ҢMiX]w\Jn>oԬzgܐ+m!#q̠sGsVrfl:ksWM 7~Ǵl b]ep=qyS)]9%`g߇I巿T{ ^]"Qi5fmث5w|?gՈ>b{X70>xym F~bw~beXi%gr󾞹i<+.nŘ -s֛G'm|/KFЉGFYr^}NyS+<{& ;bb>ݝ?mâŇSMsJ'n?)&?=Id3AV3;o&<40:nTyҼE= ǃ箲(q= hs.7kʼF`_tg>Sh鲊~AH(s{>]stLCqŋ兎36Ol\:wdF+_d"ss쑰­Dg* {2GLKվfo6K?KC_^XrOiS\"-?7EyuvʒGMB,\I[/K_4[Q~ǢMÿ:2=!n@-8.Í;c1~6rP$ԕ]sU?$_`#v9-ry2N}煮vSL n26{O߱*Xgџ =$Jv;ZmŻռ>oBݼbRTUvnm_%9:ѓR|F,{F7rSsy%&B~t˞3}gvefz7[ lœ6([ ^;^#Vj%8]+/z꜏+@dB㷟{)c?[4ac*oT\>Ng5PcܵGx^:]i7.MIG#wyKJuz'#<ρ7WOzѝ³&:wgO2Y-PeĄۛ{ͳ+O>_7J?yɰM,Ц+W+Fte/i۸mss歃ˊun֍}Gyا;~Jln澿{CJ=;琜Ɋg^!-X챓.vgCb9cWu&4 0u ೆbM֢/F’i׽mU~K&!]^Fξ ٙp}[Qw8 gpaC(jč%{.ܚxr=3ZyUt1#/2\e_jN< }2g_YhZSh;U.o)Gd]-._z8Xf v0=r =,;afamӁ4ì #{ƙتK˯22}HJ-20k?v.bPM]%t4=Y][b(^~uӑ_5鲊~(nkn/$N{xfﳇ7jYI Or65 =[6szeϐDݠvvE zMy{@Kp׺?OGΌ[z. :fE [ & F}5h˪a/Z.;glѪ{Xy?рjksۍ dF{lT~uz]wL=O4yߞuGrU*dc}mdqmOzߟ8m%KdY]duM~_݅7}fp??E) NSm=}0`={yw|){踾寜#E>?Lӆl#^G Zw9DSj4~43k׻~~>je{b`ivgL YL2-fYH|+\l5VX-Ok휻-?qoVi=sQѕMJ/>i5)㹹G7խiqV}aamAߏgޜfzaFY}ݕy_񇯐/p6=?UX{-qxٕow(h#~><=ub#udu'+CM Q64$ '[V5mx3N_/Hf.~{cטM/l?{-9icuv$pVփ>H#[)<=6sNٵ-- yB*jޱ'! l9ܱ)hVeVw >9Vq'c>kg6bCFۙ/FfZniZ0:+ͧu㶯r!c~ǟ6ߴzOs{`EG v)= y,X)nyCkIf_ʼ>Q-?1Wp.BӅ@ P G "䒜8H!ddt2N$4,.Y&(DqX$E@!JŐX'' 222e\,K*$JKEBZ}Ƥ'I"$[!uQ11 汴 p%*s0ˡ@q#DT8pXPQE.R,U/+%V68 QET(FGO7Lq~B jBm0t' @ Sc_J,I5;tY,:K5B # ;Xaڰ6mJAq|/i,`vjL¨ZPp  $LO5=SDz*3c 5V :@bR0p$a`ӚKE倣@;H~D! 8xf1ځ4~(,X)롑$6u:AA]ZRm)VqD- j :=0 * Ӿl`@uB(> ژ30HD*Bˇo/뢎Pb!C] pˊ:<с XQ硒fR!6fP]U(@G8TwbX0W}uaa8[@R)pq%.զ1D7aEuN2 #TΥ7LPq{PT><HU'c@F1\#7*:E<|0 8Q}K5p&@iGmhT/>ɁO/0b0~6jJ_ ؄"t$(@vY1U?XcppBq`G#>>ucufNԴ;aaoi䙀5zPóJ9J`S5C=bĂ==grY?=j+55h4 Z-@y,C.Y|Sd"gyT*;/ӜG8C\BxwM`I(L8E`|O/8(R(R!(gN:/$!~5h rXh>(Ba X8Tu:4_NxzRթ iDDDu>,y(yw"$^j %(Bp`\F6!*sE9"gȣݛx|{e/8G{ueʷ*ZXgn{bS"|nΐ RIzr-z={G@bя_& ۣ:Ԓҡciau`]>+Z(gv^7ʺJ\ Ƨ^OJ?{?)3_؝Z}i5_;xzk‡=~Nd^i?}Ǭ9mMǞ7)zu18i՚\mygw}Y߉o;}|CgI~^* iɥ=kS-D]ۗ_4x&^lMk5hU^-nӷ?_~E_o}Թٵy7! o?8#,qI`݉]?ofz-7^_s_+WV/8HNunZ1;uagKS ֯/y qz}Lcc֜԰ u>ΆM9^M(?jg Mck6eS~r+%V尿 'wFSĞ;?W 6NM zbIxoڌZ*R~1UY!5/R[]ãxǚumͤ:l= LrI)MRak}o7*34,+7NƼ2eOW^sUسo^{?o/Ι{뗯F ]<#ߤź&uI-՟=k&?]0ŀo|M6`iFݻ5'7f08m q3.eF[[islm8fW2'weٙ1WLwS7:.l$7 +/~v:FvcOK2X(/{s<#FGJtu,gm<:s_]]؀j/$>ieŚG??\wy|{_9)YWu$K;˵]ּXEPPHsgU]K)ʞe?6V0MF`|7>_23S)p=y.6J{g?4~9ʁ͏Ε37^ngnģKZFS9'z>2vMeUﳕ 8~TKtW?ƥe:Ҫ9挚'9՜bO'*IXs~΄'u sQS Z]Bf)Z_G:djt}Xcb$.9OyLZM(fw&m)-/^N[W%g/~_pڟ~V2?&w ofMGg? o~|y.JN5k-/v:yq}D*<]V՞[K='tY_N'>ө)aQsƼ>V^kŝ Mݹh>s\IbNIls'$12SK~I3hD{Q5%_)DD{II34f.emwKImy#g=THof#1*}.}% l(lsG˒rA|heerEAaJ/.Ws-e9%קfmmդK*2"W++51 oo`*>]U䪒 X6;wGаkkh5B[`q׀SY5=^ I c5 $ y]麂uU~OV0}`>Z?$t*JcfUL757T4n(h-?yVPUaݤQw(Gٛ6c|)+$dE^Is穹8kgu7+|fw3|Fw' )˸,Mj_5/B j\|VhXЊBiRXZ_= ?VCce?[?6ӱh?gE"X4bV8/*>,΋J8/fP 2'6txVo?/N54i%ԟR?E#dF "|BHn/q_>SL)  f6Nn~ݢ~Boeu-կ[h |jogj"/.no4 xQ}wnw[.ppydNf=壌5ָ{~<$yO3n򙞴vJ?%A!.q%<M;o m\2$dH=!ᜮg]51}se}}4fD߫N/{ Pav-"2xy9$ x,eA9GV_#a<=T7:xFsPi7VP1 &ESȁaj-C%r8\<_U٭uPXgpdqb| v v?5dPb$Eb &{+C(c9E )pW< ;\Ws!2EDcY4±)LMͥU(f{`Cz(`4lNMRjjdž%E*?mJ-V J %6MxαӀ?Oa%v*5*.BQtCREu1p>ZMTiRRJpM MWJî[ؕ.XptpKk.k7d dܵqwx"dܵqd2E`f>@& $Ⱥk2@axX͆uT)WDIcql>1mT2L"űP\ ! 0aF&a0fsl1؊%Fq(NJbj/E}*fd&4ekq ;W'Csű%еDz%Jʹ;!I'r%3BtI y2_DS|&i/jDON;vaB-[D,$b 2@E ՉYıP].A C"b@sl bB41 iR!PJ RA #1(tlϱU` UQw&T 3 ZDP j RM AbQ&d ւVBFB_Ժ :1( Hǚ,A2 C,"6X96 BFC 9TDP= v] K%R! u)A`O N$@ETՓP]/A $CE4 cmY>} d RH +D49PB 1JDE9v5VKՃP[L ]bY#9P8v T k$C\lHǮZ v}Kd RB D4!;cvYG nc@6pFm !%ultA&BTlMPIl"P "9v+ضJܷ@ eʡz@9~D3ymԗENQD&CsȠAciTXN=4Z?As?J |'ͱg;4o9֛!Rac}3o9HzͱGiXoDuͱnVc"Kh$4z%D" &R}!ͱBC4,x9qT?Nbh8Ƶ5~a4Ls=o5iP\Fc>X"4:]Iu;.i.?3;Oֽiu/(=haT_nN {Lcgqb[_TɁ=xmh]xqb3;Vj[mm渨v;r,/s=2?93^.Yn?VJ<7r}Z֗Ci^)ޘ[ssŖڋm]'>zs=Gd~QLi˫^]rۻmRv z^OY(|{y߲WJ߉vY|`zݐr{rVo|_eqOK䕢NLm}kY+o3kÝۿzۋG1~~*]ăznifu}=lQ"WEiDf|E:rߧԯNӤGܥ]!Z_~)71[Z#?_R$te>^A6屨-Fc+~^qhp⧯]1~Tgʶz[Kvo}Jѯ}U=R_S|5vI[)1]|7/3}1"Fvr[2ם83f=!7NWp˼<_o>s_⻡̮Y?C7|ok=ee9tG˳'|gG'l-On\S򙢟_YIɷ_ Pdl϶?Pmsk-SK?.W+UMs>noZzKowijI߫nm\oV˭cx)՜hO;r˷fyߍ0yfsD^*_ 0:2wIϕ)_-fPt-QSnhEugVG[mO~w^F9}v}ʇw7{^-c? f =s%Ȑ\Fp(=,ҝ`R1EWEW.6 JθtMz.W_z 903aa h5m`Z#\ `d+O3 & OKC>%֫{`z zlr֣Mz_::ksW0~+U$GW`ue1Y 7ɍ_G!9ܘD׊KޔɗK|[t&ϗcws2k|=(h)Zv2 /&x}|MMrMB֦-:e9ex; e5hmAkM&ٗTG[m5f9f buxv339į<frX&^VwY H+9̢dLGo'[AłJ~GEHV #ZxŹYb51Z[ X,] ze[QuJĹ_.%eRXEqu 꺒MZg[3A验z {=:C=,=~|,={ ==X=~a 8\RQZTa ,wZj k'T;QRŦ.s؅+Ǯ...GvdL } bBR:{ cac=z@pQpgWlD(F4H4/[3$t+c]+,ُ uw?x?ace,ӑ6 Yq@LXY= Ye<& #ߡG>LaQD+&?YX6t^==؋`>K $ IѠ||DΉ?z`(QQ }y>X7ɽ눤>>B-S>t*GNdczgN=BzL.kC*fu9JBߌ4C?tY1eQO(Ggg+l}=(CwFAEVG2x?b^^':YBsF88︢(Dnj ;-}lc:{h}S*] >4P?~,r   qKa*NjR8; G!뤕Y #-;jDCwUZ'b8Y`4 Jrc @P.?Y*&STǧE?~|=q79Oտ#3X*TC^H%X2צS62rz )--oX?X?#3kb~&V&Xmus'H!o sgװ,;54a5r@i:yp-yS>l >WP&ؕ  _#ZZӓ3,g;w,333ᝡ;cޗ| _}iKBt ;Z00ݾ2nhTp̨.!]Ǵ 2,Ot1L Հj5jn'K5h;8mԽjsNá6pk9js)L& 聋+ʰ8@T6"FF?B'~Q\G k$BEX-PR#ɑu$&rRh_KK{Kt'יevf(n8*NƌhU!!&u2O0˭_r MAGs菎h>h ?`?`yY!1=mCCuݓZB$evdcUp2vǂ`ڈ+c y^Vj|7ƍFo4F`.߄|Km9nA*&&97nT@Ut_+tf-tqA+7f e6] y $SÄӇ1r>{$.C2ţvrd?Gx5eנ&$f=>.fBi3/őRG⒙ $j#--b=0Gѭ=؎nǥ8YW?34Y Tn5S3if3Cñ3;vFچfS-0f6 2#?3̠T1JYecd::gu~saFas;#cN ʿ;@:]]wנwW\ XE;K/) 0oJ1@'):;dߩ~gWa~- ].%.cݭ3l.&#:6Zex:%/ ZDw:NՑv1~qk.RNY]cuUKjUߨĦݥq8.Vyk yi ֔eqygԼL2Q5/r,Q2ʗ 2(}9VrWW (+& SPW*~ZJ+z_}^]$+m^e 28T~iUiFY3l&:6W[UxZ'm.C$J}+ItLר#ĕ8ܯrO] UL˩rǵVUeTVʵ:U)> SSʂV.m5ſ# 3zv=cʞ*{Ƭgt*{w,ֳX["t8I=x.>9L!ehdH1u:Y: ,? @~0_abMy@`&甸>7h1F5l{)ydAyޠQJ@A_&7Xh@iѼz__,@ N/^A/E(46<?ͥF] lFJ]{vI]09D|/3€kWk+J繊Яi({|{/`0oKb{s% \2c[em`mgKÎImru_}#Q?"DK:b@dV7iދD KvdK_p ( ضj=TcŊ=Y]qmoDƴ X]DiuS;hhТ9&0zѿaO` ѣ G`G`O:\CRkD/_0M[emPgNPGu%ݜpHbA|Ii=d;n ׹&^,,~@h~?wD[wm=ku;7B$7],j-u[?[<ʽʡE9Qr#U{u Ji/#f)܁/`)FQ?m ?@?E]Ft>6&Dm"9Y j@}@(??onuۼih aE"z͊8EAN%|l]^^3wX6Qee@cwH!Q2+NqY=LTxSa .S'xo |${Il CBu]qŨ jaQFG==Q#}&9`}4SGS2*#^ݳNNJLG #@ӧaE}T}z^tiySx-v ;FT{S1.'T"pN-z/ NES\ZWS~+]sD" ɷx}eL{LinePx )))) ) ) )))))))) )!)")%)))*)-)1)3)6)8)5);)?)@)C)G)H)K)O)P)S)W)X)Z)])^)`)c)d)g)k)l)o)s)t)w){)|))))))))))))))))))))))))))))))))) I/home/runner/work/rebar3/rebar3/_build/default/lib/relx/src/rlx_state.erlPKiRFLrelx/ebin/rlx_string.beamU}Pg͆| k"-DOi*VnV% )I.G6ai1)K;xˑA$2l&Cs#D(`T@% h.F`0.` Fn ufݲ!#R|x\mRڦ=?+chě Q.E\a0][O }v 9jcDB(<ISD J*X tE|DhE`i`S5P*DrBX#ADcX)x mZht\:58(C<#T8N+еRQ`Ǩ ĝ$"QZDDMKv{DP D=1=$ ]1>G>Qܿ9&С@.c9B9D) "Ji$aqb|dךy2%Z%B=3J>{:%%tGD9^N$"!c) SIXǣO./ZR0O*^$YE)$OM6&/O^ZlMlx䁁d/?'߈Uފsb'/ܨ`oytdjp^(6ۣnlk3Ә56vM+N=y`7TCBan5ދoU/0aG%rmPγaW2gW;㰶7a׎[nl}Ver>j>&LUƺOWHYX%K[ZRC<^?WM.o~Dz]%Uݮ:;[Qnfa/Ͻgv:~K#OˇAI0#PcV>MQ*+u?goAPKiRzA(2relx/ebin/rlx_tar.beamYy\SWOD"+[0Av}d*n!$!IXmK;}nک{vhQ֥uVmhwá31??k{== ZC~q~$I@0Gs=b X}9\!v[|:]7xnT4ntz|0?"Oc粺$ yy!V a p t񉰲]b|T/( }4.s5P<>9(ms>kq[8pݍnCkvNd]X-NwPd};j`鱸5A cVp{jϱ7t\vﳅ5Zaq9e 4Y c0A}$` %&JrQN] LƭDO.DX-:) QE1&ImTЦp&NBv&'~o¤}Y --P'~qt/)ٕ,a'Ʉ2Y@c5BD(\.A , 'a-!9?pf$j@@Hc``ƐQ8KBAvȖg4Kt$ !<)@WfPh`Ic*t"1U!%BS b dt-!c(2FgC0Z@N:R6+f `4Zk+~AOJ(Jnh:xANdb\Im"9ZvrxN8<ϯqAFvu5R\cQ 8s4#DR#3j=0tC J; _1Ba>\B}Dұ*2n  9B*#chc:I !eDpuL#W?AH㕑)\^ rL.K`Q)XD&4` MV* Ibq 0ZO,2BLsNV@y3fBHZ1B'*28(a/&cUl;)DLDT qE(J!-Esb`Ř* Ja4TkNbHb5& g.hR *ЈD2?h8ݦ( ljH̕|BO,*³\(ZWӠ1f]K5wA1̷z|̘H0AUv1V19?c?嘑ۡc((ƌ܎>AFc'F&vq` !CDEV  jUUBdd,NKm"?2@E QA?2"vp?ULj;#t>di Y$J LH֙"]XJ&9Ŝoae KՐE~6̭p"P )f\Ы*"X/3k2^ydR(32eR }V:V|ګ2sV|V07Z[VDW Q ā1f0f*YA 0 _ g+ʠd VPB!eQPw:A g3VI&2|H83J9-Qspɟ%_F΄?Kf¥gJ( !+f>$5p8A6(\qECy: *qAr.ulu~n_7('ZDYXdfՙ'hhmt6 @ %$"Ir6i;k dphfl*@"` X m SeN^zPQ`u䠎˵ ^>AuBԐX|ؙP]0 &Fxd$꼁RFm^wf8v a*&/| w4C CCy( ?G<.1۷x 1ߜ4 9Dz"y|((~yy< e?9cx|~xZ?W˜/~l1+qy?nkyCΥlex|72<ȟE%呵:j.|sǎV}13)^]?3`O5|yC?lJ~z/y Sw]Za}.vBŕ'>4z‘|Y3r/$ogHR堫g^~w3.wDuI!v7)I#|Æ/Z^:XA kן;,N ޞ1(&OF{}qtx-힨_+?.MjτXm?\Ҽrjlۯφ'N4:FTb7ƣ?z>gA]|wuZ;dƷNF8У6zt)6=\u˗{f &F\hwǍO*yÛu_g:/l4ntJi?ޝn; >ѶQ㟊VhqG>MZ~!#?rb_=3LWFe/Pn;¬5_1nbr}4ѝ?-T"RyR7qy×Fbh;>kjO!ڒQnL" ?'au0fjf>G1K`LN`6u _׾,~5:`dD~~aϕocZ\ n;z.-C!64 ݵ@{'~Nj@7 rS=MT_.SE}f˞jwZ]T&q(Z-6Or,^ @hˋfXbH'?`}?bX~v9(ol}2&2_+drPwF$#EII4lFag*XjXSj#kw?doVm9Ѳ#W7v6}|!נ:ԜO9lswEB]G7m;x%ӰVi[r5vd_]7iSx/f7 3RymԂ'&E.w//?{wǝw_,st7zN;aצ:J]k9z㒃Xw-㚰%嵵5%wΞQ~|lKo7;ԛ76}WUOU[[ cV}+NTMy&gΙ?ֳ͊+k=({-6}z;[}q6^Rs&oogj})y[k5;ߊ8zgs lW-/y}KS#5U{ȡkښSj۱cM.tTD^>Ȳ6>Dx2v4V_/jɨM› [x5Tԝs];xdɦݛfwh_߹7_ܳڳ;gE[gel]>]#*ӑL3?BAϞX+3w͗~tExzޱ1X>.^ Ԡn8"0-qnqoi?Z{ :J#.UPHwDmۋOk0uJ5t(rKa f_ړ.|{p|uD M{EO.6e.|t<:I8U_b?OOKIҍ:y7#_<~pߵgZ#ƬM^|>hbܞg6ziO}3ƒB_巑wWDY-֞7.Fw|sP돕x=cD!WL?N#*/mP.uϳ[NN&^|Ss&i}l__-ZԴh9e8Ry WEcڷYoB}6ܬTt|*N{jԴV bꑓ{륎+u̟V=b0CfmJ;Tq[VL1~uRێ_3OǞmKkl_2W>~gONyydne/o6Vg ko~|{ knE>Ϳ^mG\_0d.I .*LsKst9rw&nkQ+(Jp|1$RH(ĞŐۍ211?]3^Nj^_#&_=UvEMRHLҹҹZ\ҹQ%te(edJtIi$s΅*Sf*IymanإRJ|σ|:1|ϋ{4ᛑafr|1oG>=;d|\Xs@4տSa:Wm~(z |uiuaO2ٮx-{ 'ƃ9@(/TN* )$5>~(NS2/_@2oM AIXBj- EkNҝwB b0~ "V!`@o^ Bsa!ehIE; - Ej" - -3 -fhq66;Xh\qWKY-Xj/l1l l lQNMi.!;11b|"5Ѯc Lf) Rm)ѡYޗ^D<&dL²#CDMi[N`q/:NKoK߂@-Łz 7"ik-0IfEHwhv\22oheco [hY/ ioT @@@\\,g\?AP+C g 1+(++~vE<g! IlvZ[IlfEUhBZdzneV!d[vQQZLFMhOV2+381man#M PZYkCc6=mS6߶X\|Zjδ QuZXEX%~0%p =7ŤI B);gD)jc5Z ]Dw5묁!ZUTᠦ换Eʊn:?-P -ի˅^$8齖Ilz)'4cMf.]Hʦ :t#xKi+/$[>bHѯ!0ar=zPS :`^mI? O8g6Spd#ٙm4)x58ߤm #۬#8zI+u*0śf! 2&o7kb/W,7'}YxwP"*u,4Go#!&o&ΣDE Tm:nM܆kc6y~}% moۊ퐴X2ej/ٶ+9hə RAǿF~#JH>?{r..ghOr&eznd=3t &CkVD3*Md TD)w%;&lb=ZD lIl\ VT w(Ϥ~~x?_Cn?Tԉ$Kk(rv@8b;c;:y";"Q@@6V-rvwvȀ6 j}Sq0*yl8C"[nC11 LV?}?HAA AAŐHŃ`iXTxTTd91baDB!B O6^F3[#0p8<.E+w5pm9DȫV ќTϚbGSZhJkuh=hc8;J9;J8;J9 _~;F_L+c&yy{ z%"~\cn-W*z=םD3s8wx6o[b˞b\cjSgw = QW O%8 saxWN,;A_pvN3OVz$-|SP,FNMp Z~H^'Bkq|?$+oM5yT { hdjF 7F!ݣfw|€:4 ^.x/x/+0%e2ҁC]YtE#QJ]!AIݽ’p' Dy!> @y*|Ubc]wº:t`ho֩(xcJۘq~6 zc$zc1::aGi)x:w ]cA_A_Sq]cB:W75Ju3H^l\~_gٸγq½:agiNgu u ҁ#R^Ѻ buO7EXn]M1 xR>E7xکPx^ $nD--*0u :Z|D0z>=^Q_y :*ȇ_z٩KzRNY>rmhmYM ߎ#4XJ')w;CwHȋ<E\@ߩ8\¥Þ*ؙ 7?QT)hyZ&%<>Sz)Ԉ5tA.> AL>>#>IQ1o Jg؄Ds . ].Kn&}bGߣ1ecU0}> ~$,sjsM\,s^??7 ^+|aOfΝ ko~w{s;%0LeO1L&`&=ff;^NaaZvSpYlD s^sj <puυ-3*36qy2۝^D0™㈱cCj-6>x\7 bn,lm6d9-͜ew!?<\xRb]>5P/ק+2/qNo0'xnVY<ֆDž;vF]2.-!~Cp;# {qD.' !(8jqs:SQ[ \(ʋB+':"t#\3>(1Aco12l 1).@Dגvj؈f6+ &(vrK-.!f{=W"hod9w@Ed> ȾrNAD%< 2{@`}pHu5h$ Xf5&>VFSi5g*"rH ?fLVs@CA6΄ٌFkڴٿ6RCt*A:hTPBiC%Ճz@rF0Ҋa ¤J𔆈jWD wP#hf#T* &HLJF d "ҥRzQ'zj̆ `P>Z p o4B[l:GC#ԀDR# H# {2F ClPF*rT$ S1Tz=F &(g2j ZEBdNӦRTbՊ!`Mn(S X=d0T&$'RRp2+ֳmKhP#ᠾb??05kԌqC h# ^`ƂHf6@3G G5iNF@'e'F&a r$xVIi,o@Q= qdP dSkPQjq?#Md +dzHO>}Pk% UljB#NJr43\ñCj5 #UJJQyrhJqrBUr Ȯ0X Oy#UfHBF2tʢkD(Jpr"WEfbСBRH=9̜Z6zAe@]XjH-f[`(%HC)ȿ.KL pƒN|@A !mQ{*b,5'2:t=5:Ul1ɬi_ 1r{@f"r,-)L,}`XX6YlR5iXE)A;@4͌bŏ2HUMԲ AZL=t 2WΒ'! 8iV Ƥ 14#T\d]Q|7+9kL7~R˕?ܹ}w5df5jN>9}7_{N}1U?:'%yf}x~;f_Ce]ȝ;r5mYs9U*tW$7Ҕk9vU@yֵ?mRfߓ~k0S؅߬|a[׋:po|=/=oyI>t[Vng&-qi?͵kޓ~3뇈D쟻Oiy/ylhˊ*i2aC7}?6=G?Wv'Z($nˊN^5K9^ : z &K$tRm3- (]~Wxd{xma ĉv7-6ykX{.ۍN?O ?#F(b|^ӄe\͜s:9i҉ sl\ZjNtůfmwpr<@0[OjPba+0Ջ)mqڝ"}BawrXބevkqy1F1K7 I'$h߄?S2q]{1LUY /׿Q+v*!kZCTjjmϸ okQ/Yų5"lOx4&BVbMYOuyf5.?jx?]F}|~o{ֺ.N]0]W[oo)ذ*i]>Wo,ɛaW<;WG+-o14h7U1&ytW{vм\ҍ?p ._8SwǶ~ōt-_{4uYbtdbF zg# E3iS'&6P~fgk^-ly]O7/y5?E涌$>^iCRvQ:s뛃LĆ;6l=c^סOomaj%tqpU?]ZR7x"oC;}zYL_H9Upiq9 9I/n5ϟ~c1kҎ[ӿh/Ʀ쑏(hӾ6ZQXl+&a в7ܯ9|􄧯 ]gZU|jye31kdyɏOu?7{cۭCW.&, WoYr{Ʒm9v}'󺔷g=;%>š||ޭW' xp|w]w{Gv#'~{4~ML+p-sVŃ !# CJ*>0=e|fO5fږȞv̻Է^uMݻcׅ-:iw6ߵ 13/u2,kG XSGO;;{/3?jէ"z% {`k\>nZaC|i)щͺ~OPﯻ1}'#[]_YǮ{\|̧= O\3)t΂qmϜhl-c*kg_!\/u|}9G¿amƖmw ޝ{K fss[NN{ciMUP<|_hC=$k<=%~z*nB݄mIlsA{45cםFNj7fxyFOuo6~~,勍ߺRljtlRxX;v5%U# h!rn]abxF''jcĸ.:/\O@rkA(5j1Bp==BCS2jڷ<2 jô}nvm'DɦjHp}שZRP7.O-jH$)=,G)uRGwJ a~%("j6a1PMjt5HcEA*ɷdW_1lϰ Qn"Xb]eW00]J^UGZΖV0L޴"M0 F*% F't5QZ~043<(y?%^?+"-!G7y$ \6Ә}h`(c *6f^o"dgq=;RI 7ĦJcgJhj!$P6)jQXGܻAoW 7vm)/PxiF> ѠGIbc1&%Ä6iϕH ,5kv|P)3tq)l\ qe>-B*geq˳Ձ+[+,}%0gJӕxsQ>n9vJJrJSW"{s9Wa[58 m0L. ^lljv:ժXTTN\%0kԥ>K5 ǔoM?LHDUf󲾱X_kk7UYIp\fmyҦ;Xy7vs;۾ӳE^hS2u$oB|n mU!Cˬ vKPEzH54aS $]^-Z7X>qYAsE,E%Ex$~<1Bf Ը ߀۠o0^L*UFڲ/Xl4VjۈHYQnc.)@ pn?)M`H 1c&&C<6i2%%%% ݔ9KHvz3e]VZ fJcFC./ 1:*pS6o׀ i:uV#o/+}a$!S*0RaM|̘rpُpB&~ d"{_iilT)swZ}V gle/M04M J&1OM-ѭe/ 6RҌ&`t3xYS@ePM/CܐxL5İ6Ylg]n"q\ƒƃJArT9+A $b*C2;!!2١2;ن,9̶?Jacx!:uetWүxjWYw(\i r#[xuZ6N7'L7$U7@IZDEe<yTyrȣ<*{3S[QTfr<fS3uL|Tm)nDH4x7RP[|]w"q3<]YĿA橕l%aJqA'π8~FMm+c++_&q}{x uT#= *FGm?drKA}4~,Mʭ]=|yXvXSE}VTi$`58T'7''7'IʮDIZ.?E0ԄƞǨSii6R>d6 a0s`SovFì{~R;d׈@-^_d_`\/ī ~,?Rw5okw+߯;~kFӪ;QK;|Ppc]) >d >CLA<ǗGyxbQ.Iy 6jHB2~GEhc\F5} lJOLC>>1|mp.)C/n.8L ǰdq$MƲiP vۡ7^6\Owưu{e1j rKj)v;sHaXi#3̤d\FU QL0F_|[~Z)H+2) 2~+#yV>K.:P\ |Q `b.d g0vpUi 'U*09>Z-<-9(;A-0 +n%*'M?dD>ϳLApLineX . 4 5 6 8 ? @ G I O P R X b \ g h j m n p r o v w y z ))) )))))) iH/home/runner/work/rebar3/rebar3/_build/default/lib/relx/src/rlx_util.erlPKiR+relx/priv/templates/binUn@}߯n 9R!!) Ȧ$ZU}A^ǥ%wo66MsfgΜ|-Xk8yx6[AĒ3 g/X`V|&WP.L,r5v,A,K] ?g;ވis&n7iw;hDAoo*(*GeG'`N!O!b@ #M IL ?c"vJP[ ;%t2ő;*&r^QeUZCz&w޳LXęl8I7902 R]߶ , JDe ;u2#}UCw=MS]e v4Jo%c?Q2B,Ӏ1K4y>G84=wOaGyfx"A L^ $) [Xdv5" ?AM:lF!CoFqelU|rD?J]-K*c\N7#p9pwrI3: 5޻ii$+bg+ަq4+bGZ#vKoEPQ< c;Ͻ} UCf8ӈ AxAy|nurE&;pl0VŹuz 98q$#se/X*q.Pr=LHڋ~#jx~!6|b=‘ _X] \tKz+ 3[*vaLnI0E3=ƑHB 5R "Ե-aMR,:,YD;b@wGT/PKiRԦ relx/priv/templates/bin_windowsVێ6}W ͳݢnQMo>D"KM@"Rq7ߞᐔdYvXùH?AWlIZlZr!T)|VOEnHsu{u͜G972?1,𜩼ۯ_3|VEFFU<:mgΟE&Za} ԧ$YF9.{n.ڨLQn[^T%Zw} 4S"$> \C}ȅc25\ <]% ]M-!aK^:4HlCHxp`zÛHt1Ը!'} 몺]hbTbI]=f*M ́MQ\o(ߐrMz(>ӭx\2pH4Gޱ(*UD4݃\N2"&I6n; 1:v}6//~Q4.SI)j-KZH; CSbьr<ܾJK#o.R]ւԭJAbXɳMnp6 Bb,ˏ5·;^eENCjڙnr -9nkNA%>\5yh+f4ݮ)v A\%~/ `l_Ptnֹ91?&s 9A:"(\g'G Es&3z~\vQ\aC?l<'o"vdt{; 0M.BPKiR.ީ'"relx/priv/templates/bin_windows_psTmk0_qm͒10X_h~Y$D KF݆ew6fBl;=N4|yl #wwS?H?\n~3-q^ݰ7DІH=j@{]#`,Fo!uvu|B|Oycp\,18PKiRW̍$relx/priv/templates/builtin_hook_pid1n0 EwGNNY2P"HH;=~p tOy͹\JŞ51t#|C444ZrLyTKq-1j'Y)1`#LT1ۚVҷNi!GZRBnkj!bWrG#[jOѠ8{PKiR X18'relx/priv/templates/builtin_hook_statusSVO/J-ʉO-KQPJ,(LN,ϳ*LΈG)SPKiRi9 1relx/priv/templates/builtin_hook_wait_for_processmN1n0 Zɐ,5]ET}rtn 8ޑ~6w1EDԙx{U_r :&=fDx-scbWe%^0.1v!uhHf\TNV _NX '^¿2xp17x9?C h(𱟵`݇ BQ~Z~PKiRez2relx/priv/templates/builtin_hook_wait_for_vm_start 1 bOW@Dt)X(3;XNCNKQdg<3c51]sKUbQ_NUjNX2VPKiRn 78Drelx/priv/templates/erl_iniN-IKrK,VH̋2jksSmKJKK JK`PKiRG:grelx/priv/templates/erl_scriptEN P|߯PI`ٛ)]( < 3;; s.o FN= lޫ"nYPױ|6A TlT=~V%+]28[Pr+( ɭYq .h#=;L jv<!ksPKiR_{v* relx/priv/templates/extended_binYmo8_sKz pflvslrz=@KMD&"e' Iɲ-g*hL#a23A9 `!5АeDŽGrҙmzT/`I-XXEp׳HsL5{ !1\xj~\oFfܰoo.0(u LAp38DPP xNɋ#/Ő`j4݋Fq94LD IwxNE)5+cr12x5iplp,3b2Ed 6p0,mtuH2V7%kx4pyTdq֓\lA55iFKf d{HZ- 9#<~BB,;qOij ?(`y a-MXJ36uP&TH}jl2P^4 <"AlnڠTېIhs })5.$kҴpxkf XYFM$Q2aGhM9ko BݫqinjZ$V#H2yEndr=)Ӓcm6e$f886"b(Ӆ9vcS|D>CvtCL&бڑ*XyyV aWKg̿tAc :L-sŵ <ʷmXy2t NF!c܅utO,1^^.y|KV`~VR$xaZ]bw Qepr3$V6ǩZD<ڈQTBM5yVhs$KXXRv>*-i(éPm7kɠ*ۡYc 2şI0aB7?nKrU[7AX^] Kʌ «NTa*30tWpo,1wƉt;=gWkcaD^a&n'bmK_rp@/ǟϻsMLxf+܌nT9kzنJ "7\U#na/3ơ/x6Pv1UrGPi'u3H3vȂRsW+,|kRY| 3)ie\Xum '}3[6S80{-@Q bnߠ=:Ңʼnڲgט#Oßb1.-_ 8Ş;.P]GV£%{f 'ݗ?ħb 9V wzX-kQ7=5DrV2TJ[wzdMϱ@ye[-lM7Ur١v7-ޮT(>K9RVYmsH_Qt suu8Iu H*8`TV3`s,=3z>\kF==3 )E F8bLI8P,bu+ ›1`2Lx!=[3K b0$8+\" 1 SίA+P2{uPT9 K ?L8pY"Ik͆:=uO]wܷA9vw0zp sY +3 蔦"Go#UlFw#|k>c'3O/:oe¹;8ziջ4D8cAJ㈇w'"\;+WsFyNŔE8lEoZ*X٫^n2=L%Dn[D \1_dU)?.9FBk" f0fW[| XPP5>G Vi.`RFT lr&BzRϕfgS|1gRy(+&]| D/+,pNW-["J[LilA`19 jeZWlb01e1v[[y#M :V+f^AcwSdMy5L}Ÿ2_ Oo~;פ#kC(o!J#pZϙǒ%䰽3I~ݳZW,XUW~R# '̕܍B!zqh-:Zg!P{:1N7яR:0N`v/>WvsKG}cX/q0Iyj%Ra*jnw`HOJyɣAչ~J~{"u/rƥp%334Px0$)'w0^K^hf5cޕGsS0FЇpKELzxu]0ZihdFSLBܨ{*&"6-sEj⛅0ҫɈ)m4 G/ фDc $IHoUAh;m7rv,ҨR\;`9t` 'OpXzc rX[-Y|_SnNn*N ʹN @ːൽFl~G~A Et:w,ܫ|exfpu4DmtS1Ftd8T:)g*seb%'T &IS 2ĵI*(&zzE*#ӹfI"PO!!4 ~iiK'p>e OZĽ FUt׌EM?suD}FM3 >80LN;-+^W³GGײ);9K*ef췽=F_F7{G]ɤLv|:HIIX,}NLJQꇥwkٯ',lc?=(?Ώ@ 2m,\s](8 ]fYn?ӆlAa*obN]J ʮy/RI.B|CwbYq%q_T q:V NҶid <; \OVoV7?=.L :ݜ~"qZT3.,籔5}ѬĞUzhׇpYo>lleUEDf]"Ye c+nVFx;pD[(nVhbS2Zt~+9) &5LA),5bm{}897yo]5K d.sFh UwN;4Iֆҙi@#qE qqzߩoFLa vOQ;癀Z9hTHw!s޳d̶ݖǭ٘Էk@Ϩz]wN(ikŻ/y#ʠX,eۯ!X!vMfYsHbV֐1޽dɮc(}I%VT~fc]?x鵋^3Xí;gŻNC׏Xmp;lh ~`3kh]]'=k0<۷,Zrh;趣vpze1GQ]mikv8UvS -V ,X7tM<,]Wv,vuwj7tf*IԜ>i}cp9Z?5r~W=7gæs[]cp3 w^G$#e6J3>O\BZIfu 4NoWlGGWaH8]IvY۳ezuޮu0+"d$\f=n .7[o`af[ɇn$KֿA[N^I7yn 'ylEj,ԯf.@!(zysܹC>$$0ד9y-H16RБTCw,݅ F;'a5;prYsĕYbRVrR4F3Ss5~ud AaAē8r.+@Cl-"sѯLB8Sk EUb1#qM>sWS¾3"υPǀ!gS| oHp_lB ,#xtÂZ< 6}B`6fX5@(n;̩g]-heʬ-KYVɒjI ܨD?UWi;|P[K 7 d? %46s}VZN#ٽ`='1 ^)0X1n;KybV?&<$ǎrT|ְ [D*DtuPϭ~yj~JTN 64WhIup*/c \K@W2Ӡw6\@MC 8`gT&Ln٥! }w]Z-ӊGobX Z:>phɨEKd}?-P)` 1'+ܵ+ gCKtl]K ~WZbkXI S%E^[(]% Ļf5l_smr50#-8FL|o|&PyvH^GZrB&I4`Ͱ =JO(p_!DcDvl곝|Q=|R5v$_7[z\3 IFޟqn_ .O^MMD. Q3KO0MotdaHw B9"TD}! QmGd2R($­<>ѥ_,r7~Φh8<E B,QGFevmf>-B0: X l{KcLp;8:;Qm߇^[0iHjim?uKx=Oa;T=t3}jv9w9/ofM.W]ULF->MkӮNh8|:15> 5`j9:YGe&ta[* QyWRۥ7/;{{ë 4m'G7Gedĝƴ4EsM u5 M0'&\tJB4]9ךq5}0eh 3RU\mJ,By@ WKp!`rr bFsBc x`I| פIJ |1 ;)WH"NDȢg^1׮T31-"ϝ ˱GtѿLQ9 `5gKQ7Gm6߰V21E]p0Vl:Ϗ'GzS(PƸ߽LͫIc[ǖbBS<{rZj+f;_BAg{_NҘTJg& Y>Ě :˕kV ":Ɔq+/ǽekCB|kf-7sw !8N,65!>),fWgث욢=3JuCt7A4 z#lxj*瘂jy!MpzuDƕVNmwi6[geO'Z@뀅Y9t:tNjs\2aMY%K?E<$7UmC%!3$Lm|g^2S\8O8A`3yl7X_,P(X-31nB[W«ǖ\߀eC5zk /S8(u0ov@.8&,<@5&#;fly kèCSGs}*WSS=ħAoҔvfEɥZBCU ųܝrPbfA=<\N%25]k*Xp r dL\JeBW|9eiξ=v><??złINP+L+lf&g}l: "˱.n5 *U*-k$K/Pٯkq-*h{:lbi 8a5۫F57WhMܶ6\ٌp6̽0%e lVih?๑HμˋI(E~ccHrۡ[N[ v,á[ 5}&?_ Ay3ҽγ1EWx&X\|L2]tWe wQt4^lkҔkzfNe1%iQ%l63r*j/V]L,;bi "Hajd}ob[V4kJԃ l[+eyvc_o;W e{ŃB hM5Tֺ|BْfmBMDtCK4U[rtp&j37P'"Ng;+!,Q !7#3AnsJuda YC̦gã;/o D7-%ږ{O>܈:vb>A+S,4ǔ?yG3Y.w~y7zbوUCŪ'<Ё8M$MZ$SԵ} _I@"k* b:nZ1,572P]fwߒ3ՁB4Tl}c&2E:FyއBwW˴i ,'p>b)K6ښdo@쿴ۿ"S4s_MU.[kԆ'/ȴ6E 't=+E2Z[zӿTݏ@篘E͡@|1D8(՜QY6v$w;ٙ]k4o=/scZjz1z;-ڀ:=q g?Խe#wIj* z;w ưHiBfĿk值 hAW ỈcИS"VbZ1NOJF,#P+K(JR-;)Sy.ZJQ\Ʀ_)oY%_cqWK鴗k(*H KUf5dU2ZסH2yقic! kKpۖ)0q(ٽsWU a*%P;. JqRe錻}ZJFCqA&- e"` 'В^: vSK_vgy|Kfײ̐b`CJ$ev'}8 8RJN~_i "871$TBe:AxzOCΔնtIG„~Ȼ8y;$P;y=<mw ;̇e>4 t-+<[^AX˅ͱx^.|3#p_)]0rURdpi%orA &X>GŲ)8NWo>Lg4[|q[sPf{s=]^]]_0ͳQW%}B9MS>a@h&&+okՌJb=(_Uj̷rvB`N =*2*$4_wKSƛ<gk,n*>nPRJt6_^$#>zpdO] ]/uH \?ur- Ї%Y>a=eawtWAIKm3,`2H$YetZAmUtPuq_) "A`* ( 8[?bia}ȃ5c2"S%qs]"SZV#*@x} J4xck" k3 WO?$2F ء#U~Fߐۉc!}۶|F!mLE *łVl#K>P*T\iZ1:$NĽS,-~u.thI() :#js'"Xzn9Ku_CNqQH[ = fCm~#{3յ1t6~ Q[Z5e`C\C9f-K ,هkleBs UOH a67#Dx^"^N g@^CX)aÌ5Vt *h~>.,. #,@'@Z݂ZZdhMvyCOH`T{ouu[qfٞ){w/.zV!#zsh7ACg~Zco v\^Qw^TVIi^_s:*t᭢n 8q)-`]B.?wgw3SL.1~?q=ho԰;KPzRG-bzhj~zǣdJ"_ŷ bo*tsVo8 _AnX7~raw/AVdXJbGGKH䏤<8o+6,4gw *m6΃^[)W"c=0^Y1g SmYJS+/'ʷ8Q)ܟuSz $ISdKt  k~84^t謭AJzUU%;T'iH UxK$>jrx.kk}kX1|łhHw"Yu+uTh\^ŲEc1'|k+t :[ }aBn"q, hsk6ieU߸-q۹G #C[ I6ڧD8;BDa>}ZF_0'pط FQ@vUGg_fR}pWHX+l5TԴ< Tڄ~QeЁ?ۊD%-25 |\[$xt))|kuL=*cYUW$FgĈ9eɔmKvA=]KGQ7lnBiI5Hαlh†?Qs] x_ؑ?R*C;cHRjPykfhZ0ndDLش"R]qɛ8$ɑ3:L!7?λq~`pW"6iLτv7 Kh|êQ9hK?cW|~E?{aQZ8!+ w|V'Tpj c0YTM i g9 lUkػj E*BExȦ[+|O$Ex|yv7I(л) ϭ6v}3s?R|;yF]] ㋗W׬?LW۷v9OPKiR)|d5+relx/priv/templates/extended_bin_windows_psks9;BK.{wC^CeU9,s9I_wK\̌n{Ž<̤LBPe.H6咏C9f\8p(3Җ@$6 F$0vYw_S '@%&B 6 a":2 c:U(QM<.7`_qܳzչֵJ2.Z`cVjfa0q^i3ZS=vh-iyٴ8=>z S|X CȢ(‡4HUkAeRøoJ .@{Tlh݁{[EYqaleVx6=ʴ%Vz@}{swUZ> 뫳Eq&/!) J`A?R6&M)D 0Ҭ CG8 \X=5 x󾎇}NJP&"2b*Oy zJDC_:95)B#rLw|~=99o' i0 F`\VY``/߿w'\huR%`0A-B.䥭1w;f#U(5n2|>@90RDb1vU*T.foՙP!Z_eUVfۺS|U9hP[E;哐g*n>C\?GC-?mAjS_ՙ{)2txPeЀ?h F lzJ\(<// 1P\pvOr3ꑡ?ϾP-u ú eX|*YgU{A:%f~;b dۑcLcN09^|`{>mLuT`EGpy o% Q jrլ;s+,c:@n@E& {p0 :dTG ^g:wZMGoD2a?M3Pq۹<;7 V'rKRn![xXU ibyrEg'ș%*k6;fEqXe"!м?H-VcAm.J֛+R}2!H*&J nk+'rx?*LYI<<{ҏ15:K _ _pn'譒p?$=I Gɇrʀ }%%"ϼ%ѰNkďIl=YW(%q[+(&q\2U3Mio1lI\!BI +#B  bhѰ+/" OEqœѽ;kPol&v8ߚ˟ G̯'3^ tnsZDLmh9^M0Ab}nb|}ڝ]4OdıѢMktV8OH$54d)M aS8X6]ש/c&ݙeGivI_^ᙠ5AR_&CC &gJ= 6e܉?0 sp* y lG *hBOG8ekX4do}GS`iyt7{e/:_gpk&6N~8Ig{'XS8_O u\th`Rpq-߮,-VD: &YQx(tL8>aR ɡrc c0SOUܰ|UU9#š99#5OҌާ#=b~IJb3cK#qٿSQҏ&4B(q:0y), 0m]23=6XʄX?t4~ca􎼥juz/\#{5\HeWG4Y]9x8Xs:hѪD_UHtSf_M=`9,#ffj6.Epԓ:| h>~Fq1#쁾^{{̲r\4azѸ;D,Vs; LĠQlN.hC5bY&L" g%Ҥ\Y0Cbc/)GVİx Ԅ1G2`\!`2*D{XG> }% X!M+߮Ol"30JKK-:!3ݽִ=3Pbf2OS0HrNҲhqD`z~g'6\ mfFɰKb/ V*J K.3jx-bF\.Y׳,**^ ;bPM RAQJIeQ@WRDrVxr5mYcS7<}7eZ:CʴPxyi0m3j m+9LRѠ2Ԇ֗thAC41`I_[Hn31cDHCT)76g?ps޴OF%z 6\p]&t`$0dA[- `[(+:OW^/iq 8. qM;)vF7nM3`|yo KD(6 9~i= ,@~:J>D9¸Wfsp(w&^4q7EgTXuaZJȹanX^ сgҎΏ\Ȃ)ѷֆ5tKNM7qJ Knͨ;6Zjur%U6{@mpCgK> 7PKiR D7+relx/priv/templates/install_upgrade_escriptYKs6W d*5IIx68Nzp=%$@ɪb.|$ms(@`w?|],NzH$>@C? QƦdO1ÄN Ô,H:FϏl[4IRB(FIq%!`8%qBIݯoO?c FI&|&J8f<ò| 2eNDw4f$G}F_E.H O&LE "gEH("73gͮtή oбӱW>r+/ԩ5*8Ki9E>N 9$6LΆ iiM8g<@gmq02.`451JtxAm:ҤmMUVv˛L.I rd 1=Y/EVsZyʩ)sF X-j+~'ܺ:/UEwzܱj콯> j]ݔ}"k3[ՋWV/'[_O =Z3ϽA죃s@+ ԶaYzҖtlK8>;`ڼͺSlʢH_7ᐲ_3"d)jQ J뷛Ku$[ U yJӂ)X|%յ)"7G#W e')Gרj<. ! ^1KST )m2S8|(ry9T_[&jRRuBzmTb~-&BX0f Y?f wܦhKR,o]VT7Qt,ϣq,s=%MgwA0KxdFo<Eb ֐Ē0S͜},XHINu) ~ϖaWdLpj2^g| jBVGhL|lOton"ͤNy:/5# U-mbNzʳqĸQCW)'aHiXP7v]@mR9g&ޮ mjāӒK52f3tuV4n|Nm<ݿeR V0Xc%A3n JD-}_zrx8ɤҎEC26EWߣ KF}BuhOQ7܀5a77QPX|z@p5lsU, HH9> *K1342xG7"W)$JR>WcO(ۚ3M~AT<}ww- Ԇ.c*YwifeP.xOeu:!J#rWA8rɜu㰊O.:[`Y$FeebC"* ޏ#vr}`lׅmߢhT6uWHL򸷳Lrl%Q&b!T: ߢ$?}W\Q 'T _S*w"|߱{N93s7_l/NI*ynEƅ SRZu?BJﲎwTZQ:#YrnId"ł,DZb^6:`5}!7݋SMK]Ahtjq? EU[a*UEX[nmkhjfʢoٹnrk 3:CS-l[Np<:B5S5MG*WZ#QaUj}O7g-}Sj&U^]7;; DGlë0? /5 ۣjrg @?RJ$?=lw ͂:D Y-'nt$ NPLLWYpUzMkǟaja@Z ɭU\kB볣UKcns}+BRXn,R$$ޭ l+#1Bn9\{(s&9#k=9M!10Tk !`b a\$p0Y=14]DݔY&|`CR>>Bztd R˜W'Xx: eZ 2 ^tH}(Wfo:oȍ/ 6Vu _JUAF*pՊ &(tTP [Ef6ix+xrS'vq;l5D88w'+rpfBjsC=͑ NPKiRLG}( Trelx/priv/templates/nodetoolowo I1`<4Kݦ@[[PD,qw߾HCvF ||_^Zg7uŒ^cXɔGUՑg"Œs~>6l^d9a~;T0kзw,%({\RA& W,1Jp% >xΙT)`mUH~pD,9\qZ1hS Oa䊈 wpb0H#ꜩ<ڀ.B*7\L T|V,[us ;C0|gP$ m¹aʜP2ZDŽ;,g `TɃpd)kDфs$oH(9-$tdT }1_onMA@9-? ,ts\ te;lj'&&~TtH!eB(zW? e2B]1{D>H3%͌AO> mH2CI-YnqVV\7RGH3[|]|O'\xA[Cz,G+|:F!`[gE;hA<&62w|Q-d qIQg-r^Z*dZߕS[}X.I;-rf6^u;s Q iy !ObZA'C*WQ.?o<1Wܨ,>imijh[&ydSByt.1)7/]Z1#J~;ԗό\н`~:xm):`ͼQmRl^N!qlv-Xi6}1=a&{Rɵ8ń&րf(J$n;{b_9;dx@t9{Vk̖G,o+pj-Ps˫>}IQgW2֪y9e{,wY(H@x` s333]dnť-{҆wrVEꔇr!e4g? _oK̡ZS6|!lѻtj6t8 =,?l W%s +-F^K@%AP2؎R''W0&VRVRŬˡ"2BoR_1Ԛr;\c w-jcogvQPD E6i>w}˥某#W9sW5ec~W(W%wQ2M a\+? VKKq#u0'/>quPO-`C̪e/N &}ùY._4ḚTគAY<`[O~Υď"8Eg0EgȯӗC`2u&.&ɎmS]8͕G`Lwc57LLY ?DH%vd|;|Q*;wd ^Oy3cz-'䎷  "VGOxvz)_H'ia|+lDmMhWJ?[mj6Who\Jmه6Hvp (0-XEvaŴ17T|l ܟ+۹zk>uB[ũJ!Gσ/ܙ%*1SKԢТokƯO-W&[`= LYnY>@s?("C;s;6:6 y`uݹOUwSۮikQXsu\Yn *9p1ĪS G'SJT¥Gflu*qclÝkٶj&cL1Y9RǍTV]{ wK+ ^)`YAв@;?>8$%ֆEAW}F3fӴMZH{vMzw65m% {%t櫱jW` vAweO9KS8kJzBYf4(8%&e_'Xb27^?. ֚B{`m {!CT{ܡKA4ZnЦ=}[wӬi]uG'xܝ m` w= Ư^H092熧jxԟ|]9C a^ )[B꟢꟦|)mSֳ>*N4bsB~/h_.i~][1T׺f„Sg8"hMӾ'#*) MrTw_qFpٟk: bPKiRCkrelx/priv/templates/psutilXms7ίPLkK܏5%8e&6p`B!@!]%6ޕt'M2>iwЋD3^ķr8@oڢEBE9h-rEPXl421tJ /G׸o X5rͳ/ p0cDBK&T5$r_f֕d0D*rPŽs3hDR+thNZ6ob,%#ʐ֝n}GN<:v^nEDd֛ R~^\f*`j3霫)l|mn}jQ~I_c^;k: ?bXэytC>'Gs=o GV-7[ QԻI&`f 'lFKօJT8 ^tKa(3%ĭ {5yBiֿdK,$3RH[kBz"Oޤ&y!ҘgVI '$#P2>U*&Lg߄Ei\@:~9N؆q,ȒMZWw] άٳL7qZ;Se`KSY 'ekWlotҼ]Rxc Hᐸr4mc+򨄅= 7U`|ՍuJ=h8a\G+"Ls֌W ƐULY=!x(돌er}y}޵|_sUQA#(Ң 4 ܍_N,j {%XPѷ:iU$@VjGYQpԊ:0J<4t< 04w1ܹ,OGG}yD@@o~xחӮ;8x{wz)X'z˗PC7ZtQo|jpj A}qDY@0㸆=!\=>&NJB2 m[WIͩz:D&gOJ=h.7旷kuS??Ң:Sœ,p]'+/]LiM:9)j܃2dzΑ Gio(;ҝ-wb}ɄW]S%h&2e `x)<8DUD);/F+ar"`\ܔn* RiƺPKiR3l!relx/priv/templates/sys_configM=O0 RvvXbZU+|)R4 cAUW(WenۢBhuHj5# r"J& ie$xݪDpo[DZHT~An# Ax:<0ۻ}PKiR v;relx/priv/templates/vm_argsmSn1|W,KP$@Bپg$P>tgULX1+7 ȳ]^ߧsT:O ћU V8z_ 3+W"JԚkTs LI&ˊ j'l9|4A,z:*TQv&VAk8EJSIڰ8kIIAJޑ hYՃ/} 6/EMWiډnA«c"li:-?7-9{.3-v6iBёoj"Slv}:Y/9>So*_8nX0v&ᄌDmb^qP1T 2#[lLֹ֪| +cُ= : i*PhXKPLlwm+rprL?#;9 f-%pc]~6HG@- qu'э!g;Ns<.:SC؞%"!]ڨp[Yﮁi MJvJWXY=m{˾Pp0>OUHPKiREK!#/ssl_verify_fun/ebin/ssl_verify_fingerprint.beamwTM.;Фދ*D"B U"E. "MD:"">(`J9[9ߙ{ٳZC]576??C w vp#ah4O_Q'aaH?8 IB.N~0 sunNpy@'ܕ,S3Io 僀pD0+ se?PkMp vG98Ñp䟀Q>TfF8kF8x} #rA<<(q D$F `pF8!ݩd .߉W?-F-+҃!`:!A/$EQ7JHhj(ih2BM Z $$Z`=X J (~;v= AJ)9<:A` b@.H ZX C !_ 9P]SH ­tŇ㣅j#p, ?`2G0\Ch@%T8@-@>L8_{Ɓ= t_xm5qsqtttvt4uurq ԏ73V߶a|{iy@Gr1_@WF1H=\~?$ߡ=r,\SC3%Okѵ G{{a$B0g';8UwSR~HuBy0P\d0?W9CMG \aeT3zIiCI=tM(o<3R]srߩimffB*z5> =uYjQYLay{f i/4v'!UKTjȘڇe/ @_$'u'_(lznߖ)[n5ZBX ;*& [O&A:ˇꍋ܍ = (Y W웜nzGe d6A9(w׶˾agt ll|iZe@b~_e@y&a/1{bco:\.u 4sOi]wU7/k^X؎*Z?|ƴaz(<{8v* MrTvZ'-1;x 7m@MDl]@{sm:/GLSo~I-8NTwQϤ;2%<ˤ'WKHPs b ijȊά-^1y l_=I|Any` s͙ `dG|ս7"Ǧ^/NzB-x=qwԈGZ P" UJ2fɩ@r%owzNtxyH l4}sg6s.οD@nt<=l3=x "ۭk,BxQoU`zi%g6t AC{k7^M0l_ώEx1t&0pMaOvIy^bԸyu[ٴ_E{CnIgz>5c1K`1eAW޷*m(hxG}ķv3zʮHt0QnCx'pB J-u ccKGu_H WT+r&6({H Mu:7$Ůĭ%Ҡ;UCs\Kڡs;̧LmO}sփE%T oS*Hq"( x?!IT") N, cfN`GGPc6d붢^b2Uۡ t 'Ч':̤D-^>F?K,&~219ush 40e=eRED1nir(!+&:VËgڡjʏ`bmD]AG)F=oq2;/HQ ߗ>](K(<1Q;b2895ƾb42X3_K2V}WYP~=_7Li]0YIK o,ABK(E`Dr>][ >Ǔ ʶWeqdzQK6Wi)!eWw`@=P'Z }Vέ,тU4Xf_Dfo\ftų5Qj_|`&Fz BŠSJFa(=n9^B=qg2lcۊ}{"#4Cy$hY `᧌ѼI@ZI?Ҙ""2 !1/uiu-MNĆK\t5e&wAZXSD[ :hط%-~cx&2؜rXO7aXI4绖Vi/ +LEqkFQ$٫T!M&m\Hwb`[r'Wʃ_?DG2vg2{y&,-, P?#8Z`s+̆nZM]F68k)=Ii-8X{٢-uxckx>С)Y7ENuوNj]bNWz!=;FqËUoPw4e!C |ӐT\ZoV_~H >K>\vo.p*h񗱟ŭާ4;e+rtd»ݫ6 L+;rL%c=rZJHnې70)Bd,W`05QTnSKYoIPCi2o/>dZ=(-&r:~!$hŞB5q Yq"9a5Xz$dRvdJ<2FK0c;HIubL$"GPu< PBMГ7SQv~_VvK< iB;jgzy Dmv ,A,mw;(W"fku<*?n e zE2. ]uptQPZ.4"zI'Go?B%OʟLLq|h#NWa܍;.f>i0^%MK*.}5P{Y==P }TmRDR}{Y^?|Ig!!veYig*y%;=W$a/t&SmO+MKn yՈ(T6u ü~gg.ly=I"3 W5nU]/4ب1(^G4ze[0U'&,5tYL.P6f*Ҩse9v/hvp{0PZxN ]n0WW <aB'|.PAnbc2} O_틯 1b'Qw$],lrp? T6rɰXA0Ƥ vtOW˜?Nk,ِ~cIJ%}aO̍E5V-bΆBO-]UvZ"M ! 1g3RmG0NsJm>Z7V,^3yܡ0Bc a$ь31&XFcO+Iܖ{Bw;lźF~2Bjx[w^8+jWt.|\ˮ˃ $]E/8oŐ_ѿP}lWzIz)㝯/t;^L5:/5~mU?(kL|p[,d 8/U3_qN *q<~.rmS6Л_ԯ9ZT vIo\e`(m" vKe*eɾEe*+PWYjYBDDaEVㅤs%xf*F:$}SuW|m :f Aʲ?"I3s'Ƚ_ ў |+`i ~A/xs/%D̟6E.}/;5SORlɸ*{܅_W#T>S^:Mcm/yr- j59Z>1~LJw#Vxlh/`W, L[?)8WGdvtm% xi\a7l]إb+n>]w+H~֓::0t涢WrA[(uM@9p-<~LJduf-oIk4f1 v~B=g ~Z3Uԃ [N~) Ixf dlK&!$qq& ^ ߓ tX}uOւrf3q55Lu4 >e}%HK5#iW%sQMۺVIyqO^B/n}'M=$Vza_g@Hr賋ElHJatfBE"6iz`x&Xy#o 5`터9YҼ"=qIExh–B?9Z9yL Um>`1,6B3Wk֎Reܥ!BHW| l{#9.γw"җ?nrW 3}afGJa~-E=fIg};z=ͪqgyE0b2!bY~TOؖp8HBm$aX틧OdŞi F7tȎ%y ՕCl\lH'>hZ//q-Uw] eƧ'y r:oKo (to#.CR<._D˿.|_0~Kgd&o7DUۋxX ͘gO3bOX֭ ZͫF$,sDRQg}dbYEayjD?H#'aghH5R߫m ~`%բhZSQ1 y"E-+R/9{AGca$-9x]C>XQbD'XH—9z?Vαuv IIqsv8Yue_4Ui J}Gf\#ÌZ)uR2w:JV&fzf˂M+.@J\u-JbK b=/$lQZJ"jIRh3eزfQk5`Ѵ5sP7k\$0•v.?9lh ϩ%ct'hHMt6/[pĈM ݐU&2V4#mWmFn':l pO~MHXe29,Pƺu]?Lߖg`6}Wk(UW]gԝj<Ic8fD]>4jl:yÝd9CǖV }eT5Z8 ;MP]KpFb|GicE5`3#!)W PV)u7f6}f`H-[<|"~U6Uνͳ i3=[0ѥIj[LĮLMrPV\,%!HIǍbXmGh_^ng|2$> KzItS玸 [OOQ4e%ID%mkQT ''uJ±YRC r9NRby>#v7XˍqIR<ԧ$Ul:ZE[>.Ej5})-r{7{` mw#3y?kjL1A%trxØ)qƤ|1g4*1} e}cI2ϓ&kH `zDZ@M}S&[̺)vљxP2^wyZ>*޾1#2Npg=;LCtlf4I 1D H-/WJ+>~!otj}єÎm؛Q }m>|Bg/*ŧ<_ ਊBp>xl:C]`=_ Lddtud٦8 v%] 8a/:Tnr \+_IU+_[NY|n ceo5Pw8H3yöUzzgq,=! ,^c%FR'9' 8& ?#Z؁-;qٖJgٍ`Ѱ1)NSLKIE\/{xF40YBgcs) By`0>t!ȇ.bNm+fe䥰^BeedI.# I6yADI%!P\ T( & ( DPVPV%JQ( NC@B$R,J@+9 ɪuխ?K{JFk50NI?Yd? S[/dϲ,{P08FG%gyO)r3_C!d \$5FL\ @,8&xKA qs`V <%UDX5FAd v"J !ϡ 2H SPu0' %HFAA! ^s80i^f!iq+IO=A0Rfu^U0& !a j`>udQ k 05- JEb^`>m fS|"O:C(0ȒR :d a6)~jQA5RARm 'Pk=P")ni@ B*B@EDb>ZBCrO քP00lB $H #J8~:?8Ky ?s@hrr?©N 3^> A/ FhSfЊ; VPToiVةԬ%I%W xKCo'POCq "yfR_5FlcrPfg /7V)o// TZ.&8s$0ZD:ޞ.h<4KAg \s%#~ڤe$Cs6x:|OT*\ГbdBÂ02PG?'70?,Zya 2X?OwxA87|Gia1n8/ Т1a>gir2 CcQD׊g'OwYP/LCOzHO<౎ 0܃aE ,˻*[gb s%gGTXjY߿Ѻ_D9Eߌd<SSU}'rF8E}-=ed\kqSo+#nX1NjNKh~79߯>?K*ݒ@7;ٔLu7c}e[־ը[XZog|}Y4 ?8۩Yp~.؋⫘NJ0a]{zSα㥘YiQɨkYC$=^x̏ me}-{im ש> K~` Žգ^70[qNk5:VK(퍒Q[D.`h`ѡZxܲ)#iM.sy¼rz#z!qPY" |+[oNOr`ߞj8ځ$!AT[Lg5a;0onv NӪ\?I<u!M=1 s̼{j \˞m3H{Ai~FCc6Nd] _R>ԔhQ-X¡\ګw R$^^S:P:w+祯kGXMB@`Rz|#,6*U%>uE7dEA׼Rm\ɴFQC:gZ;: Œ.@2.6_hi,6,e3[#EȤ ,03 3ҦNd` ;j9ӀC[da؏ *_Ɩ:w9V[Yhcr')m5Ƃ7n5rc.uޒBoSsB]h>h) l(wxxyh_>ٗںg]z}|r e箱 ָ68P*IԞuJ 4Bݷm:sT%CԘ'cL|'bj#_7xs7yQv/%d?*vV2,#qWyqS&A) 5U#;RTvGzz+vץۻ˕Q&M؟}lpvwW1L=FL)AYnGXW/#UpcBq=ޚbs'Y|T[N@Jz^y$2nēaӯxʪ@*` |/5d:#k}]s˥!(LI+Y̛ E>ؑ̽.V >̵˧Q'Mw6)% b-;*K^XJIZKMɇѺ[rfoҭuRY->G6Z81r1gP"A+ <67F_w'edD@kL5*J#54jᤆN-tuC8t}ԯ55 i &TGڅoBDR(Ce,fݺ:m>e @z G]eiQ_O)e@Q} ]^L2Aƍ=|KSWHb8'v("K'4th%Pso scRxelU.T{pK|]=rVҶp7W%ga6&ASEqݚ֊Pe^1Uom5~uz}ϵd7;(ʵKAwӄ(zm|G $HU;౸J=:˲/փo 05:S}vBj[TezITkӸ䑻 +5Ps>[,-bLQ ~Ld-'輵rŏPA#CwgsOg#:_,k~7UQI'8;,D`k) Et|WGs].ec2_*Ku^  ~=$2n7WF?GeJ꺐o*8u#G 0{uN#\_gO q}8$܃W܈ϸ83s0l 27AVtlYQcwckQۦ7iJԢ~Oi]w]gTjŕ?H&DpdO߿?HW}ҡ#4pukdpFaL"0:6ϟ{8hhar?|N;<AOԞ9KoǗLlWE,ws֜ J9;i0bK=-e:zibۏ"~0X-V7M!p;+75T**&@^@/Fʹ9;l9dǨy 2ƔFoN莅$cxQHڕr(*NkɕI|^lTֻ:f g.vW=MLvW.Twc 5Գeʫyn>ݛWj; pʢ*]s.WgUs/z'&A#GyY%X}7LynLdC/Zb)Jp&V[0^K݈Y~J#"V `4=TfQWeVjx%adַ5-+ɍ.aȽN=<"QV߉#4u^0 ].l Gܯhv" =hk(=@tzb]Qw4c~sg)>3NȦ^sEHd `g'QʌۼdKsG0.>HVS!a&Jo[p7}'Qh*{W Hq"ug0m"^D_pdb|b:Y| $[Ã]7Q|CDHF(y1>6+i4)4ݞ=z]lߨR3^leE fohW&_ AUJXL H xn& {9>|Z|%Ō*kӻȢMIO{1"^B5o(q!#Elg끂b%4g}k;W%?WѡV~L I6\r 0*ZCE7ZTyy0™&ќ90NH|1ۜ웼B.E~ުgƞ:aM?+B*]^6EZ(B.k;{@.9 GiyF"ek< ݗ a,"o?L)kTVO5 `17hS#| s鋱5_P^<2/Pc^Ŭ$\[ל$oh[h[R5˩>q-t*ۆZ=6Z(6[]'߰^~Ze`m ,+ƚZZ:`шN-h_׽=pavS pS ᱳ7E0lS %:@/U!mdf?%w؏upL}f^[<!wG[uvI=kyi' 찤Oʧip u0q8ˠu<⌭*eec@>C8p4vΌMnj+H6{ 2!ίĄQ$&7m/v8ثLX2gW ]BEe> UWex5,Mc\l+&m}6 XȠR&äfH="~ c?xߣ= #VzBņql!˃g3f/ER>cGՃY.>)BszՍy>&1?߫~ x![ɍYvɦr~d)j9JY+aޞȥ_"['ܔ xycQف/O])h Uhm:Լ=&K\߳%}i2jj\x΍Ұ+.yqp@MTg0 Rh~| DnQTW0*@y/n56F5^zo NKٚݝSbsʘc+?:<>Q >vDMW[&b.wV͊O)>u#z+L?/a'ןPi_dh!fo) eK0Pޡx,Nc3&F#7[qN?c[^e&*U9WP[umiT q%z} `Ã$⢻a=k͜f0G M;}pZ%~( A6 揳M5ZM4w<S|lg rъqè נ;(VFi 3"&nFw4&J ++:paדdNL9˞n-cQ}goNov|“fұƸ55PeBX=nfW|LskLdz'*`.5q803'[[N`6jf?WI( fSXD~~2=,l> -av *.=n'rNQ )~(㉟ăyNyPv""Fv1H;5h* 0bis3)tILBfR0jƓSMuR0*'gxkLΜ?1?aWY;q($ `Iپ )VChg y g~pgdw&9p fs&k}p?-WeB;j0 y9o ⶼ k L["y%>80rQ\S]"3>Y,Ðijo]Ƅm63$e,n*{^Q$Ϫ5ЉUh\I–T4+v!fR '-\qP#6vieʆ6[ɉ)+_*,W9@-U2NRx5i5 ڵTK9 ,sJ`gAqO1PqEV;(E+`}ʦ`.ށgyﯾ朏[ɽ_A{C4j OQ**\K$|,IГThHY+qp$LѓMpI2LõDJqiA5L7rup3F"TW'𑘭%?3V[F)B!U'T#(,"S,]e`8BQꁚ 4L\\Qlȃ!A#;0 3` 3  EP-!A,aW$xRm#!`d gcu!l ٹs#h.xwAEtB;gNC!'xp&҃M[n&9{e/nÚ(fcw0ǁy0zy X`@E P &YFH,Fx#@ \ ՝3A2`{ 8=X"ԩV 3`; gvƛ4x@Ќަ|[لG`GcL4f\7SLyoMфΝ !3T*UJQ)vC|ӭQGhf74ǻ1΄5a]lLM͂t:-{RѫI % 7K.9~9*99$La=BhRuhIL^$)P0IƅZ=IZaF"rQ' RdzN"?B\NJOj$T,(4TBkQJ\O>0V1Z-W#HJG+O"`S1~"OoOݧ4zO];$t3ʊv= înT5E ?u,]5&Gnu#"[u* 8C3ڿݍ/;hLImkWi4Q';zCu5.`FڨӮUvYv᫟|ʳwϝx{u7;6W#Y=7__s!cكUz7l>p2Zд٥O8Ud:5mйaRי3U2ߺjWuOݰUZذ'>syu _[E%'pSvؖڶ +ۢ:4>pX]v±cf`>9 gOM'Yġ7-grCfz&"K'(q/lU6om7R^yɢO4t "T8Jyɘ t<3xQj;+MimvełTFaYE37mNzǶg &O 9<~\*I[ҡ'}ʹrSO~2W^-wLKW^=R8ش勄"Onʏψ2(*٪g^m-tMkk.fڜ6߲˼ӲS&ӭ**4GC/Zpe!^`{8Ͽ$%s逡5iA_n9Xc:ڝpon9v? 1}@w}ʼnW9C}٩ e+W.2{LPly_EڞuϭwڮRO{^/& Sx1 Jgͤ=+8yY1DqNIL\}~0×kfoLI̫_\Io~#?/Ֆ/ݿ ggGyoYSdqe٩S'L};P샥c HKmG'"g-/8kX*Ú7Nm:wjs:.0>  } rQCmQ' uE}P_484qƂ!~PKiRtU-2,ssl_verify_fun/ebin/ssl_verify_hostname.beamWy< 0}n&%KiQaØaf,%!K"%oiQ)*E" TBiHy_s;\\}]kcj Rur7@ >7Gr=^XsI&rP:#8Hs9D*b',Zdd"aѿMIl_` !xT< gq #oA,X& ?Vx&ACELPf|P(&(slfQƍrD^ qU;M0.P!g8|Vs#aDMd$<6!記b3Յ3șƂ3+Dj)qBBZA凪H2AxI*CD: I 1hHӁJCBA$%V 9@@J F\QدPrHQPHP fA%8$ .HGCzBPy8" l.r8 "{ AH3_! jy 6oۼӠ$ :JG?CB; P8a4HpV"$F C6MԔ6"Ac(;&B6S!UB@-P*bԁA.-tHU&2D6'Cz""u!P-e'n 0T-C8+!aM 5<60x~f DNm6"NA[!hM)HNaEF ,0UZ!J0,&B 7Po,L0WX#qpߠ4Q@ADB3 k& p1 2Ga1f&{N̋-Ih "!,C/,څ|IBBB!!!gw#b}aNB x'LAdo mBSsAZ~W[ x"eaˑb ,Oh}giDEys!(RXt7^`Hn ̋pTv ڰyKscFzܻ^Fw A~C6ҍ ˯yL5y- E\RxYf#郙jlOuh7PָyUavr_djɾG)%ٮB8t]Jdg:::rod3[å;SɨSyAuwMKU<2^ܡHp)jW?]ˑXT_R^⎢72l[mԞϣmV+F* ,+v+>PfشMcJ7~Ӭ~ -?;{˜c$r,>\s'hv24MSAk癏w=[wN6NxX'KQ~VRq ڎ Mό\ZCQzO[TPt\Ou[_slףs\ER'W6$G"oW;D;y_ˏ9>]G<ܛBci }!7D®6r=J:2oؐ!+Ck^i.Z41D3Jg\/z0ڐn^N0Կ _vN4ۥfΦVR.2>`])!\TV+Ǵ>OaUIxxݍdV(E'#::Vn/FEVAX~liѭ͓ Jҙx5Lw!a$<-ksẕ}KFwir|kZr53Եs+6__gSdr(߅CI>ͣ"\CR/ڼ˯kڬv[ ?%g pn ~V5\|tRd&4+ Dw\^xv)٩6 `[%yr \lĔbzGǮ;5Ǎ.zKhKiFgςϦ:]J>umQwy'wᗿϘ.FiųԺmMl2YU[߰  qeY uK:>F j9{g '+Eތ;/ư(vm SL(٫7nqfKʼnGdZg:VԐFRlcz4N_o]3EMsqݨfe%z $O>:k7d>ǹJNdx8gl #LhGX_5"ݩvî55Ne b9ׇʔ`­uc@{ mMNi[wΚ=fw|w{e[ىX8dq&8YT׉/WLH =Q^|qg?5 tqۺ(u^Hkl_:Fଦ8b\IrzU!&uU(צ4; 5 pIwc;v%uGħCv;WgL1it)]$Psst݃&u4Iۼ>?C,;`}4鑡7MGZƴ=(EC%+ 9y:jL\9\}C״龌8&&]a/ٱD,6a[o}ljQcb 7=QҴYgv8;sEF9NvѦ{wމ=jo0?*FAxBỉdžzG ^ǟ e_XRvln"% V5}')y߈HGo몝 h_ߘ$ TLI~35>sKUz6*k[3=P]Fpx ml*`_o:|[gbXÛ 3'JPw:JiN|ong!יᓁx7i t(NobGsl=yb2rædIU' 7 s6)K].\uR)6: ߔ˿.يZEJi_ك5nuwUGMJɸ`(qsO\sI:)ſ(Dƅ5j_'aJx‚J5 bAUGPvI iP7T5kѴ֡}N>xn9CZL-N}.sg1n Cm%юz1R[o #C1j ~&Y|'v}fqp2)sE|#\&7<[e?S.99{ Uy#KpZ 3 F zճK NM;I̪8le7\ZRa]ySLԖiMaׯñ'gq:~-S;gu>>yJq>ٰ=R*ZRdVV(XqWD*j惢Z5'ʒJǏd""( 83r^,(@~F5zgFYWZ2g\YѽU%[E Lc?KUlNJ%@.)p)y*7rMd?1[梍\t]jV]6b/{̟&˲L% 6Z"^䕡/om8I̞Zgj+%oN?Lp@V8>s }f35bWg+"J}.u*w#Hsddʜ#xWivIat׼/cyJ%f{8,{Ou[L?),8* .ܣܢ=QˆwoTJP4{,ÏaevpkA=߿I\;0e=}jq|).<2@vj] sR3{"2l-T ڃbm~DΔڽ;EA|Fp* (;븩v@y-]PXAbөWƜ%0H-#o3c,"M?U!!M:v_ж d3&UVx~ ,0s{#42Ɔ 7F?NxX`XwOpn_O{y]ՆTq SW}N`g0ߞd3V^lהm1[w6iuߐH,BLbu+ªG3Gm}iҳi ^ih=Hs?~$BYzj UąsuێQ+%5TJP[Kw6E:zK6[0RX-士i!չЃ*Z+".р_yɣ|Zv_/AgKGG'MoWvn!ydoM\VDp"%(:)K>PlhS(9/.}{9W/J_޺qM+LNE"F}}xFGڻ%v`v Lhld$?&əg.ԍԤW;cmUf\9R1嬕`F҈1(ۯ!⟥m9sI/摁kK9+M#ybuӛc®?t#9t}vͻ#/7L>4|~?_V-`dpgVgKݯ3?}E;!U]a9uխ؃d!O/v+<~c*!߹Zd٥gc8?Hn%vk2SjLHɷb@sWjk.$y]mvc{AnkldN|ы9jRN2qk,TS4(mz~X^@۟!,P ٠gkw>!sSHw`hM1KUtܑMmj3/o-#1@Q-t^Aeþ^BW]vB}B^tdhAry@*ЁrǴ89<8MLS3.5F.G*\Ky)a5\ H'TZMw) cJ2S5p8fѫ&qz[B5O⸦IܔcqTNK$&:\_Vy A7G|Qr=KFvv a9<ݰn5]S $[8xթ}e(si[1<h@?X>=<g_Zes4N_ 3`i`+u n7Q[ն[7_,!T' Q7Nas::GR _A 5TWE2/"  #sD6ܬ!L6nZB:;L6yEW~곓i5[wbKڦ;t;XPV/%/s"v 0vfw bTFH)x#<--rY@ QI 3S28I2+4(^g,,oèl^"+ cQ.M /+PeXI'&L51~exlvTxDLLWn)9S24RvA|]x@͕Lufs@Jaր^siVC(kE:3 zϪ[X!T0YZ+G*w*Ô|^Ң$^҈'p9$BipgM,S'3[ VM6&pUAd .O SX<->d>9׫TW9yv vd{ St'ߊʘJ` f 7[QM{wBSQ`ӧg0Mtnm@ ]ϵ ;15IV J 4^!:9WA&cQC]ն*k t, ?͓ ;6q$FUQtrZS|?k%@ 3tU_%,>K~[=t: uy$ y=_Gԋo0/aXAK .R`}7rȑ\jEnaS}G4?D'T&c&'!1Cq}JA[ x0^ mI$ZFʇ LϟC»D!-ACKhD.>Z A H3k!`,dK'랬~š^p]P&T #~$u} ~qI\pYp@[) {Aq=gϙԃIx+ؘcUB/8 wL0j랛-ugk47-%xQâ!jX>D C԰*q,1¸wD FD vvPQJyNjn9kVNw"&#q4y38jF 󣬏2id]P]Tq7n(ywޢxTTƕNIέE=LrYMe4;0## >e\K^&u{EUAQe)fL4 aW3&-а+eОb,xd,_B^Oqb6q$ÑS|nGZ9x!1xd_F?gYXyv̪ %+( 1Mş񸃾}㎴3 DvFk1x<ҦZt{^C6("◥mw22~c  $I$:nC Ih0& -'b1ކUNX(}Aw3;۩0:4ƌYfFZW3/L"GGdZԍ94!ꩅ@ndi!atm~UF g/.3 B0e~65u|3!HXƣ3^p`L ̓Ҭ >HL y 9 EzM fz!i00XW!‡pf~|#CDT>s=BG(%E"(QSζajx5HG |QANi5#Ԛʮ+焾=E,qJSqC{#OyZzKtgT}&g>x&kA_2˲e&g3|Eb%Uŋ[R Qd%a\ ?iBDįH0_;_cTБ~6czg*;0<]ՁJP$DΦCzJCkYc \YXT h*8dl\4HU:Gϫ4nx( tIUQ:=Ng555}k"u&_ ;7oh ezM|<$[KJu*yyIk^nL52s}|x"Id"݈DG,wF"8G/<| Cp?a 8pL3iHxU`r'|I.x_GjH&Tz'B kT-;@sƐgٗq|}0 Bm"a@'L FJ#ߋzҎLhaaA Y4na= EfGO E !/s4LPp@e CP㩵 SkA`0ʖcF@NH~1@@G9`- 8 /*;p \h&  0z-9&P(M5 D`!NZXS^ p\ p*@yOyy ͂0x=V&~P4=IRPiy`LZ !O3!xr @@2 Z;5~jk@~IB2 vt c9:6S,У sN#$!B ?JuBCA%51d@A9h8A_JjעG#RA|},^Gk0LT@` *8RX>I?dc^*T녨>E{$,`*vcpB9 +h5&ŪSurrrvrz9a /b{z%/1%Mgdͅ{=h&a03cn㜠~ُ󞠂XfbQGU ?LJI.nj<`^Mz?AG^ԋt;Ry}q7^ƇL$}d=}d|/d;Nzb2ge&z:0GoO/;Ǒzc$;]^UGɁ77F b85kO"J(ҲO$`n)ǻZt&R"eQf@no@cD>@7 ] V}R˯mڮ MѲywuF .'j뙙ad?mly]:kʆpc"ehG |m= i\]|zDMs?+G]p[!pâ K&O\&q~Z㯕X^ -Xv}:A=PP>H^_':īt4dKMKV8xT!nsqw!ux]G+b>nOn.[rЯqe7Zߕ~;\)gN]oC7l C_kX75/s_U(cDem(c-I[,z?‡T`^*t~eff_fEy0;2U4a~'bZ )cEa}nW4^b_TD.˯KQ(i& VނVOLTW8xJ P1W`SYpC:B ȟ'?}fi&i]JK(pyCoxL!xwI| urR۟YܪX 1$::ʨ⤈b;o3s>@zD7aI<:v-8 X$[d^p |ٲ_-]<[ W&L a*AFH DقKYD) SVnO->Oj’fϨ54i5qgYג6}⺈(VxԱv wc8է6-եHP~]#(e!}4)3KuGhյo2ފt$˳N|YN蝄G¡e(oE+'$CaQ[[`zMi}TF_R0y>v䍳,?mT-ޮ7\?-;hQwU)-{h>*?.3N> ן=?{&2M7pM O[-RZQ+gL)WXzp/T|nC(rBԫ_`cXl⺞eW5o.R61͏rO/n辈BǨ1)SlFQiv⪨8LZfP-ϸ7uĻ}L˕)k|vιo xćٞA"apو-R *NHӡ4XFP{PLo^4NaeͻC0GYolZnYIA*_pO5ym7F.~ dF[5_D[%i,GUt'^/K&vݬ\@)/]F5'鯍fO6i/Űt]& J0[aCo7_3,W rm'hc.~E`Mf )={IM1+50J?Tql7~b{'rs*KYFlB)S3f}WX^]zd=q%_a\ateYO^ueY0< $$23'"_,h G>lMFo4~Dޅ?i`׀)_r@Q5Epsq p!k"[9*$)2⼎M]K[Zʚq'EjC1,:(*+|ȧn?ĦYM) t۬OV-('D¯CPc]8l,E3as] qfVE:3as+F=;8)?at|y7|޹0㌃˂,x^W^Noe 6hM,tKm‡'-i_/\4ݢ_}]"Cح(F#EXN%ug~+ Cz|$sxo7ofks\Y#u I&%ao|4n9̨v) :QbsQ/MӒt}< 9C("e=02Rܪn 8|Rhhg'y=a|BA?-FȚPnjH3ecF,qwQ,Og>yݭV ues'DW"!jˤS?[p˽.&K뾾u3y.+tm7}f,u>DpL mը>'d}NQ/.=Asr-%L\!S(f{ X>W݊|hnJ֢oh}`FC% t}`|7",3OB9"d@~Iinsj&3WpJxf97ӽ=F rR8߇4)&yMoĸN|8JF?10+_Ge$fC>(4{;6g AJO *oEfHz\BkNIvsIݓАߪl/ 3qL:!]L[2lΨ]ѿ5P#As.9.|kMݤ6Qw; _Udr D~)%x\Ҹ+k(REYz9;<DY,Aw_Ar2jt$Δى(X~*;!@C|P'Ͽ QRjϴCg -#72t6Bnߴ;]L7 n=oMN'U-uf]dn!%^L4L8G頤ЮPkp̙r/UuJp4 Pkv%uq !ypn]lыܑhVZ ܓuoPށ- (D1אf>S,|+(iۃ5྅8swrYO3,6n1=lFDqƬٓ-YKJxJb~%XU1%>IN>(QkiF^ȋyErkĀPE*_zi/93jWAw DTݙ^Xک60 a\1R#q!1X63ecz1{O#Hrëxx7)(^'gjY'Roh3~.'2MK@+MQoḴ W <&Q;c=3[$B._2Ah(Ƴ ttVtA;w0R-\AX+ \d{-XhSddhu`YقX$.;oIp='ݍQ|֟9Id S3Z 12L%馋>{ 9n6`&v7:~hDGh[$;kny)U/VD9d^s{ŋ'K%cP^Lh4ZJgJ|`߳)I5r@˕F}CEac HsנZs@ݞ@Yu0 RrSdt: Y[GHő!#,?DlkQjuRn v( րC h<%W;y="&ݧ3%}Wn m^V74 _1qo's360l"nFTmٽ(#U*/ >K1:P/r^+3˓O_# Z?EႯf{$<:{n[r䁅 ef:پab{4~^qgy!WE iq S1:w\6ghAXIDh>)nM =ַK:K6Ϛ 5Y Z`##mTH~-Sw:IǽlEQZ: g~4UL )Dt9ʸLNoBM,kS? C Wz0p5}R._l=WkmVJ/ dcx=@}j|uq]Qk.ŋ thzN6'5}\}TWy]|^Hz+L4<[>/pBż-hmW:/8OsSnO&058_n GΘn99"{:x:nT36CW/g:sh%i~Kr*hmG)޾+<7"*ǚjŸjxWۤVf;)T-1vC/Ya=MD{|Y^tjZO`;_B|-n]/r~yrigj \c)ƦǢ/c8v,o m:r_ | "p']lwQ4+_AA|l|ȶׄ:BG%AvTfF`_o&E2_2~1IW1@Da;Xűe\ 1OC\qT*J M\#/B:xtA?<}OBfYOr>)5)I+rUQ?Ł?Ŷ)5q~oّӰ3zszsFPU,pG#ƜLNД)O醱ii;R:ן0OfCT;F=g96 Ϫ\N1 A;%t7Bs|{siݛa <  eZB),DkX"GVP'uH( (YJjD⬯czAgr31xɰg#3)Ϥ:|hٸ,_=gcW%)H[䁐eSu pܦuJRMSWƥSYO5^P e%lU"BR|s?*4/1z b\¹hX~."&[6aL2Qf ֬-e˹RY^bؿbUf4̟-$a|[]%\˒>V-xE5y&2g{j4m$Tbs8R԰Zɜz*t%ST2&n"Ղ3،@3)vSKJ.]ȋͩWTNNK_e6&p\&ĦkMD 6Z.% tCc1[?*ȁdEW #tu)\\:pĥ kZ2쯁5;lHS ·' tns8 #QmUo&kK00u;fc65 jv˶~sr/ퟷ:cVF5f<&'X5'rYwIw5RYiEɟƊM|/ѓ\d=p%A.Iոӗ+gI %$ |%^晘4wOY6/sl^'COe0Q&pL^'H:p_:yUpɫ*yM')|bqT,''8}7@A3+2j;1G6c%F϶7piJ6j)!uv`g(GhKd~*S@0D՘FL.=@Fh#A8(S6+aeDG0+L7_D u:BUӐ_C8MTj1mb@t.`KDǃ F:N&j|TW:%],( Y .")KDAc/Ł_fD@(P U l_#m3PE2~G’@jCBhP t. dM[t_ė .{5c-w/Ϝ#BjI\S班90H$d9/W.Gq7m4^x7ؔewWZgK,+mW:'q^[ڕ Y}bDojQQ#FHç6f V=;J|_hѺo_Yk_nN~_+N x?/yl&6qKZ"V%O.6hqbk7YYm}o S-8` ^Yq aa#yvd^JӣSZ^CY ?iyk ^#ziU*&sK߻pVl誂rkؒz<_WFIm*[H1$K?ҍ_-h_Hb9˺JÛj*]T~*eY͜yu9ʻVm+:f,t5{|xJy™NΗ *ʽFZ}jZ}c 606qaNZI2_(w5TC?-?Uv¬ĕ<!ݳXkۺZc/'#1qִIT&| *䦗ǹX/9pIvl:uFW^"먅 ]u I!/$F˴9?Ih8p>6PW'N}Lq G>M\씓*8QU=5[fuB+f˴66%ӶԸm5Gwcӊ^mipQSf#=fz"jesWw]I6_J2;RijUՊdk#{wϿ]PC\绡H1aɗ{ŁKh:NC;@{Uսpu;KE/-,}w3ع>pp]7Jj.UBFV;3:[]Cige5JN8UK:۠ѱ,Y93sٗZdAȜc&Ɖ~#Vt/ݢA.X~ʥ%M7CK߆>{|6KڤVU/2(7m4-?_Dv=ڟi@IYO.717m\ }A1PXVKDdIO2~QG?k2瀞l~[&1W^Uc{żIDv=m=TVe /P2`N$ۨiDzWK;5[8T<ۡEꝐR3BsM6*^~|J<ѱoyyO蓌qՒYǚ9O&zvmj{F(PJqC(佔KhKl?)Rh5xO {m<^bmww8;Oj[r6>վ?{Kl;=-x5rG-˳sƍ?Տcc#x}<o7[xk#wƻx|~~ ~=><zPKiR߯ٝ (ssl_verify_fun/ebin/ssl_verify_util.beamU{TgA]c;n*h 1"TCJd&a0g&GkX۪Smk[Pl}mճQ[تGwTRkEYQwk)Ι|w~͝/J 5͵ l%AGxU$Ga5eXw9NIr UK" @: Dq[B&la$*Ô c}8\!szجV.b:]8 f́1­-Ĕ@Xr v,A nNo1 vf/puU(8­"֩.@F14DHOU<4>=Vwૢ ZA5 Q ED _-T qDbpL_@SV @r@!;jK|[ 0:jKv¯a_L5XѨ6nr" =úw!&9wc}$P,AC]'% yy7-QpDG ^ڕ@+ մTS%5m<|d _` 7m+&ZV>ȆťM#97}@|{az?w;Ï}9~lkaXc޷ 5[.>2kUQh"/3 {BuCwX~d ]t?b6cǓ﷮Lzv& 5>w<(uĶhc@оb̈́';&aɓN֎|m:BfNU߆7]/VWs=K͟%h\Эjwl`q)Kx\}5?}啽Oپm?E뺨===5kksw[wX|gU%h)xu .}Yk[cq륞K:25Gv4k`n6}TxȾ~ ɿqvaoO"o\|xGms(?A?;5:֪1Xxi_PKiR'bbmustache/ebin/bbmustache.appPKiR+P3"bbmustache/ebin/bbmustache.beamPKiR(- certifi/ebin/certifi.appPKiR<*a!certifi/ebin/certifi.beamPKiRcf/ebin/cf.appPKiR{gZ2hcf/ebin/cf.beamPKiRc$,MRcf/ebin/cf_term.beamPKiR4\ " cth_readable/ebin/cth_readable.appPKiRsI'L1 cth_readable/ebin/cth_readable_compact_shell.beamPKiRgE,,7cth_readable/ebin/cth_readable_failonly.beamPKiRF)I +:1cth_readable/ebin/cth_readable_helpers.beamPKiRXeV 1?cth_readable/ebin/cth_readable_lager_backend.beamPKiRo*EHcth_readable/ebin/cth_readable_nosasl.beamPKiRw|QAD)Lcth_readable/ebin/cth_readable_shell.beamPKiRTU`S-Tcth_readable/ebin/cth_readable_transform.beamPKiR< Wcth_readable/ebin/cthr.beamPKiR+d' _erlware_commons/ebin/ec_assoc_list.beamPKiRPN $derlware_commons/ebin/ec_cmd_log.beamPKiRv! lerlware_commons/ebin/ec_cnv.beamPKiR *=x $Grerlware_commons/ebin/ec_compile.beamPKiRRpQ &K!xerlware_commons/ebin/ec_date.beamPKiRIDd!erlware_commons/ebin/ec_dict.beamPKiR'@erlware_commons/ebin/ec_dictionary.beamPKiR56!*erlware_commons/ebin/ec_file.beamPKiR6E%erlware_commons/ebin/ec_gb_trees.beamPKiR5Ϫ $'erlware_commons/ebin/ec_git_vsn.beamPKiR`"erlware_commons/ebin/ec_lists.beamPKiR`&p$=erlware_commons/ebin/ec_orddict.beamPKiRQ2#verlware_commons/ebin/ec_plists.beamPKiR ,#erlware_commons/ebin/ec_rbdict.beamPKiR+ 0#eerlware_commons/ebin/ec_semver.beamPKiRN!*erlware_commons/ebin/ec_semver_parser.beamPKiRxRO[ !Serlware_commons/ebin/ec_talk.beamPKiR>QW erlware_commons/ebin/ec_vsn.beamPKiR'Y(erlware_commons/ebin/erlware_commons.appPKiRO*p (_eunit_formatters/ebin/binomial_heap.beamPKiR**!eunit_formatters/ebin/eunit_formatters.appPKiR)/)#eunit_formatters/ebin/eunit_progress.beamPKiRܒ>getopt/ebin/getopt.appPKiRs` v?getopt/ebin/getopt.beamPKiRtproviders/ebin/provider.beamPKiRJrproviders/ebin/providers.appPKiR=3)t3providers/ebin/providers.beamPKiRU ֭ "providers/ebin/providers_topo.beamPKiR>.rebar/ebin/cth_fail_fast.beamPKiR3E~ rebar/ebin/cth_retry.beamPKiRvcrebar/ebin/r3.beamPKiRErebar/ebin/r3_hex_api.beamPKiR;&8rebar/ebin/r3_hex_api_key.beamPKiRkLQ"urebar/ebin/r3_hex_api_package.beamPKiRoLl8(rebar/ebin/r3_hex_api_package_owner.beamPKiRY`"rebar/ebin/r3_hex_api_release.beamPKiRHd2rebar/ebin/r3_hex_api_user.beamPKiRVHh rebar/ebin/r3_hex_core.beamPKiR Ci_xrebar/ebin/r3_hex_erl_tar.beamPKiR.!(nrebar/ebin/r3_hex_filename.beamPKiR @qrebar/ebin/r3_hex_http.beamPKiRP"!vrebar/ebin/r3_hex_http_httpc.beamPKiRȏC3R{rebar/ebin/r3_hex_pb_names.beamPKiRw87j!җrebar/ebin/r3_hex_pb_package.beamPKiRs4' rebar/ebin/r3_hex_pb_signed.beamPKiRqL"?"rebar/ebin/r3_hex_pb_versions.beamPKiR%  rebar/ebin/r3_hex_registry.beamPKiRӮLV}Xrebar/ebin/r3_hex_repo.beamPKiR'v0rebar/ebin/r3_hex_tarball.beamPKiR5 0 5rebar/ebin/r3_safe_erl_term.beamPKiR)w[Nrebar/ebin/rebar.appPKiREy Rrebar/ebin/rebar3.beamPKiRQW$frebar/ebin/rebar_agent.beamPKiR{vN{rebar/ebin/rebar_api.beamPKiRDt0t*"Trebar/ebin/rebar_app_discover.beamPKiRc*[rebar/ebin/rebar_app_info.beamPKiRu6O6rebar/ebin/rebar_app_utils.beamPKiRk 4#rebar/ebin/rebar_base_compiler.beamPKiR&`2rebar/ebin/rebar_compiler.beamPKiR`&50"rebar/ebin/rebar_compiler_dag.beamPKiRJY|"rebar/ebin/rebar_compiler_epp.beamPKiR).Q 8"rebar/ebin/rebar_compiler_erl.beamPKiR[ "7rebar/ebin/rebar_compiler_mib.beamPKiRZskP "Erebar/ebin/rebar_compiler_xrl.beamPKiR&D}| "Krebar/ebin/rebar_compiler_yrl.beamPKiRE;Qrebar/ebin/rebar_config.beamPKiRLnJ\crebar/ebin/rebar_core.beamPKiR n&40%lrebar/ebin/rebar_dialyzer_format.beamPKiR#rebar/ebin/rebar_digraph.beamPKiR [k;Trebar/ebin/rebar_dir.beamPKiR?QL rebar/ebin/rebar_dist_utils.beamPKiRf  2rebar/ebin/rebar_env.beamPKiRQ\0W#rebar/ebin/rebar_erlc_compiler.beamPKiRVjrP rebar/ebin/rebar_fetch.beamPKiRwG8+ rebar/ebin/rebar_file_utils.beamPKiRvX&"rebar/ebin/rebar_git_resource.beamPKiR'XP )rebar/ebin/rebar_git_subdir_resource.beamPKiR~y "rebar/ebin/rebar_hex_repos.beamPKiRS8!0rebar/ebin/rebar_hg_resource.beamPKiR*! F@rebar/ebin/rebar_hooks.beamPKiR-#Jrebar/ebin/rebar_httpc_adapter.beamPKiR| Orebar/ebin/rebar_log.beamPKiR«Wrebar/ebin/rebar_opts.beamPKiR^ frebar/ebin/rebar_otp_app.beamPKiR 0Z,+trebar/ebin/rebar_packages.beamPKiRhÞfrebar/ebin/rebar_parallel.beamPKiR"rebar/ebin/rebar_paths.beamPKiRC ("1rebar/ebin/rebar_pkg_resource.beamPKiRV [rebar/ebin/rebar_plugins.beamPKiR rebar/ebin/rebar_prv_alias.beamPKiR}մ 'rebar/ebin/rebar_prv_app_discovery.beamPKiRCrebar/ebin/rebar_prv_as.beamPKiRj. &rebar/ebin/rebar_prv_bare_compile.beamPKiR 7b rebar/ebin/rebar_prv_clean.beamPKiRMs9_%rebar/ebin/rebar_prv_common_test.beamPKiR Oj,9!}%rebar/ebin/rebar_prv_compile.beamPKiR|M 5&Erebar/ebin/rebar_prv_cover.beamPKiR#-T erebar/ebin/rebar_prv_deps.beamPKiR&T ##prebar/ebin/rebar_prv_deps_tree.beamPKiRq ^(@"xrebar/ebin/rebar_prv_dialyzer.beamPKiR)l rebar/ebin/rebar_prv_do.beamPKiRcϻ< Hjrebar/ebin/rebar_prv_edoc.beamPKiR<#$rebar/ebin/rebar_prv_escriptize.beamPKiR)($=`rebar/ebin/rebar_prv_eunit.beamPKiR%d]X"srebar/ebin/rebar_prv_get_deps.beamPKiR* rebar/ebin/rebar_prv_help.beamPKiRH GL(&vrebar/ebin/rebar_prv_install_deps.beamPKiR} ' rebar/ebin/rebar_prv_local_install.beamPKiR{=2 ' rebar/ebin/rebar_prv_local_upgrade.beamPKiRd %# rebar/ebin/rebar_prv_lock.beamPKiRLMe T* rebar/ebin/rebar_prv_new.beamPKiR "5 rebar/ebin/rebar_prv_packages.beamPKiRe JW @ rebar/ebin/rebar_prv_path.beamPKiR(zD !WM rebar/ebin/rebar_prv_plugins.beamPKiRE{JN3 p)-U rebar/ebin/rebar_prv_plugins_upgrade.beamPKiRE!_ rebar/ebin/rebar_prv_release.beamPKiRoc rebar/ebin/rebar_prv_relup.beamPKiRumD dg rebar/ebin/rebar_prv_report.beamPKiR@ g@p rebar/ebin/rebar_prv_repos.beamPKiRk 20(ADt rebar/ebin/rebar_prv_shell.beamPKiRwD rebar/ebin/rebar_prv_state.beamPKiRu rebar/ebin/rebar_prv_tar.beamPKiRW rebar/ebin/rebar_prv_unlock.beamPKiR@30{ u rebar/ebin/rebar_prv_update.beamPKiR$8r!. rebar/ebin/rebar_prv_upgrade.beamPKiRƨ~+d! rebar/ebin/rebar_prv_version.beamPKiR[J!d!# rebar/ebin/rebar_prv_xref.beamPKiR< $ rebar/ebin/rebar_relx.beamPKiR rebar/ebin/rebar_resource.beamPKiR8y8! rebar/ebin/rebar_resource_v2.beamPKiRnl0 rebar/ebin/rebar_state.beamPKiR;T` rebar/ebin/rebar_string.beamPKiRDHH/ rebar/ebin/rebar_templater.beamPKiRC < rebar/ebin/rebar_uri.beamPKiR h<%t2E rebar/ebin/rebar_user.beamPKiR9`d rebar/ebin/rebar_utils.beamPKiR^,*ߝ rebar/priv/templates/LICENSEPKiR` oI ŭ rebar/priv/templates/MakefilePKiRtI6<o rebar/priv/templates/README.mdPKiRTnr rebar/priv/templates/app.erlPKiRZX40!˲ rebar/priv/templates/app.templatePKiR^[o% rebar/priv/templates/app_rebar.configPKiR3)Zl# rebar/priv/templates/cmake.templatePKiR\%+ rebar/priv/templates/escript.templatePKiRSk&_ rebar/priv/templates/escript_README.mdPKiR5B$ rebar/priv/templates/escript_mod.erlPKiR)Է rebar/priv/templates/escript_rebar.configPKiRee rebar/priv/templates/gitignorePKiRBW !N rebar/priv/templates/lib.templatePKiR!! rebar/priv/templates/mod.erlPKiRxɜ$ۺ rebar/priv/templates/otp_app.app.srcPKiRhc$ rebar/priv/templates/otp_lib.app.srcPKiRuo rebar/priv/templates/plugin.erlPKiR%6$/ rebar/priv/templates/plugin.templatePKiRy{l%f rebar/priv/templates/plugin_README.mdPKiRU֊! rebar/priv/templates/provider.erlPKiR(#%! rebar/priv/templates/rebar.configPKiRޅ@)%" rebar/priv/templates/release.templatePKiR24& rebar/priv/templates/relx_rebar.configPKiRhIY rebar/priv/templates/sup.erlPKiRGa$" rebar/priv/templates/sys.configPKiRAEK@& rebar/priv/templates/umbrella.templatePKiR\2-:u rebar/priv/templates/vm.argsPKiRvAl ; relx/ebin/relx.appPKiRz$ relx/ebin/relx.beamPKiR*L , relx/ebin/rlx_app_info.beamPKiRt]C relx/ebin/rlx_assemble.beamPKiRV_l2DEb relx/ebin/rlx_config.beamPKiR0? relx/ebin/rlx_file_utils.beamPKiR20X relx/ebin/rlx_log.beamPKiR^mƻ=Qa relx/ebin/rlx_overlay.beamPKiR^fW*H3T+ relx/ebin/rlx_release.beamPKiRU relx/ebin/rlx_relup.beamPKiR,&m relx/ebin/rlx_resolve.beamPKiRLG. ;e relx/ebin/rlx_state.beamPKiRFL relx/ebin/rlx_string.beamPKiRzA(2 relx/ebin/rlx_tar.beamPKiRvx&01 relx/ebin/rlx_util.beamPKiR+h relx/priv/templates/binPKiRԦ  relx/priv/templates/bin_windowsPKiR.ީ'" relx/priv/templates/bin_windows_psPKiRW̍$ relx/priv/templates/builtin_hook_pidPKiR X18' relx/priv/templates/builtin_hook_statusPKiRi9 1  relx/priv/templates/builtin_hook_wait_for_processPKiRez20 relx/priv/templates/builtin_hook_wait_for_vm_startPKiRn 78D relx/priv/templates/erl_iniPKiRG:gV relx/priv/templates/erl_scriptPKiR_{v* H relx/priv/templates/extended_binPKiR 5u *(J relx/priv/templates/extended_bin_windowsPKiR)|d5+TX relx/priv/templates/extended_bin_windows_psPKiR D7+i relx/priv/templates/install_upgrade_escriptPKiRLG}( Tw relx/priv/templates/nodetoolPKiRCkN relx/priv/templates/psutilPKiR3l! relx/priv/templates/sys_configPKiR v;" relx/priv/templates/vm_argsPKiREK!#/ ssl_verify_fun/ebin/ssl_verify_fingerprint.beamPKiRo&ŭ ssl_verify_fun/ebin/ssl_verify_fun.appPKiRE"$&4 ssl_verify_fun/ebin/ssl_verify_fun_cert_helpers.beamPKiRze 14 ssl_verify_fun/ebin/ssl_verify_fun_encodings.beamPKiRtU-2, ssl_verify_fun/ebin/ssl_verify_hostname.beamPKiRl$ '& ssl_verify_fun/ebin/ssl_verify_pk.beamPKiRXw*a/ssl_verify_fun/ebin/ssl_verify_string.beamPKiR߯ٝ ( >ssl_verify_fun/ebin/ssl_verify_util.beamPKBEejabberd-21.12/rel/0000755000232200023220000000000014154362354014414 5ustar debalancedebalanceejabberd-21.12/rel/sys.config0000644000232200023220000000016114154362354016417 0ustar debalancedebalance[{ejabberd, [{config, "etc/ejabberd/ejabberd.yml"}, {log_path, "var/log/ejabberd/ejabberd.log"}]}]. ejabberd-21.12/rel/vm.args0000644000232200023220000000176214154362354015722 0ustar debalancedebalance## Name of the node -sname ejabberd@localhost ## Cookie for distributed erlang #-setcookie ejabberd -mnesia dir \"var/lib/ejabberd\" ## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive ## (Disabled by default..use with caution!) ##-heart ## Enable kernel poll and a few async threads ##+K true ##+A 5 ## Increase number of concurrent ports/sockets ##-env ERL_MAX_PORTS 4096 ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10 # +B [c | d | i] # Option c makes Ctrl-C interrupt the current shell instead of invoking the emulator break # handler. Option d (same as specifying +B without an extra option) disables the break handler. # Option i makes the emulator ignore any break signal. # If option c is used with oldshell on Unix, Ctrl-C will restart the shell process rather than # interrupt it. # Disable the emulator break handler # it easy to accidentally type ctrl-c when trying # to reach for ctrl-d. ctrl-c on a live node can # have very undesirable results ##+Bi ejabberd-21.12/rel/files/0000755000232200023220000000000014154362354015516 5ustar debalancedebalanceejabberd-21.12/rel/files/install_upgrade.escript0000644000232200023220000000325614154362354022274 0ustar debalancedebalance#!/usr/bin/env escript %%! -noshell -noinput %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et -define(TIMEOUT, 60000). -define(INFO(Fmt,Args), io:format(Fmt,Args)). main([NodeName, Cookie, ReleasePackage]) -> TargetNode = start_distribution(NodeName, Cookie), {ok, Vsn} = rpc:call(TargetNode, release_handler, unpack_release, [ReleasePackage], ?TIMEOUT), ?INFO("Unpacked Release ~p~n", [Vsn]), {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, check_install_release, [Vsn], ?TIMEOUT), {ok, OtherVsn, Desc} = rpc:call(TargetNode, release_handler, install_release, [Vsn], ?TIMEOUT), ?INFO("Installed Release ~p~n", [Vsn]), ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT), ?INFO("Made Release ~p Permanent~n", [Vsn]); main(_) -> init:stop(1). start_distribution(NodeName, Cookie) -> MyNode = make_script_node(NodeName), {ok, _Pid} = net_kernel:start([MyNode, shortnames]), erlang:set_cookie(node(), list_to_atom(Cookie)), TargetNode = make_target_node(NodeName), case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of {true, pong} -> ok; {_, pang} -> io:format("Node ~p not responding to pings.\n", [TargetNode]), init:stop(1) end, TargetNode. make_target_node(Node) -> [_, Host] = string:tokens(atom_to_list(node()), "@"), list_to_atom(lists:concat([Node, "@", Host])). make_script_node(Node) -> list_to_atom(lists:concat([Node, "_upgrader_", os:getpid()])). ejabberd-21.12/rel/files/erl0000755000232200023220000000213614154362354016230 0ustar debalancedebalance#!/bin/sh ## This script replaces the default "erl" in erts-VSN/bin. This is necessary ## as escript depends on erl and in turn, erl depends on having access to a ## bootscript (start.boot). Note that this script is ONLY invoked as a side-effect ## of running escript -- the embedded node bypasses erl and uses erlexec directly ## (as it should). ## ## Note that this script makes the assumption that there is a start_clean.boot ## file available in $ROOTDIR/release/VSN. # Determine the abspath of where this script is executing from. ERTS_BIN_DIR=$(cd ${0%/*} && pwd) # Now determine the root directory -- this script runs from erts-VSN/bin, # so we simply need to strip off two dirs from the end of the ERTS_BIN_DIR # path. ROOTDIR=${ERTS_BIN_DIR%/*/*} # Parse out release and erts info START_ERL=`cat $ROOTDIR/releases/start_erl.data` ERTS_VSN=${START_ERL% *} APP_VSN=${START_ERL#* } BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin EMU=beam PROGNAME=`echo $0 | sed 's/.*\\///'` CMD="$BINDIR/erlexec" export EMU export ROOTDIR export BINDIR export PROGNAME exec $CMD -boot $ROOTDIR/releases/$APP_VSN/start_clean ${1+"$@"} ejabberd-21.12/rel/setup-dev.sh0000755000232200023220000000177114154362354016675 0ustar debalancedebalanceecho -n "===> Preparing dev configuration files: " PWD_DIR=`pwd` REL_DIR=$PWD_DIR/_build/dev/rel/ejabberd/ CON_DIR=$REL_DIR/etc/ejabberd/ [ -z "$REL_DIR_TEMP" ] && REL_DIR_TEMP=$REL_DIR CON_DIR_TEMP=$REL_DIR_TEMP/etc/ejabberd/ BIN_DIR_TEMP=$REL_DIR_TEMP/bin/ cd $CON_DIR_TEMP sed -i "s|# certfiles:|certfiles:\n - $CON_DIR/cert.pem|g" ejabberd.yml.example sed -i "s|certfiles:|ca_file: $CON_DIR/ca.pem\ncertfiles:|g" ejabberd.yml.example sed -i 's|^acl:$|acl:\n admin: [user: admin]|g' ejabberd.yml.example [ ! -f "$CON_DIR/ejabberd.yml" ] \ && echo -n "ejabberd.yml " \ && mv ejabberd.yml.example ejabberd.yml sed -i "s|#' POLL|EJABBERD_BYPASS_WARNINGS=true\n\n#' POLL|g" ejabberdctl.cfg.example [ ! -f "$CON_DIR/ejabberdctl.cfg" ] \ && echo -n "ejabberdctl.cfg " \ && mv ejabberdctl.cfg.example ejabberdctl.cfg echo "" echo "===> Some example ways to start this ejabberd dev:" echo " _build/dev/rel/ejabberd/bin/ejabberd console" echo " _build/dev/rel/ejabberd/bin/ejabberdctl live" ejabberd-21.12/rel/reltool.config.script0000644000232200023220000001013214154362354020563 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeniy Khramtsov %%% @copyright (C) 2013-2021, Evgeniy Khramtsov %%% @doc %%% %%% @end %%% Created : 8 May 2013 by Evgeniy Khramtsov %%%------------------------------------------------------------------- TopDir = filename:join(filename:dirname(SCRIPT), ".."), GetDeps = fun(Config, GetDepsFun) -> case catch rebar_config:consult_file(Config) of {ok, Data} -> case lists:keyfind(deps, 1, Data) of {deps, Deps} -> lists:map(fun({Dep, _, _}) -> [Dep, GetDepsFun(filename:join([TopDir, "deps", Dep, "rebar.config"]), GetDepsFun)] end, Deps); _ -> [] end; _ -> [] end end, Vars = case file:consult(filename:join([TopDir, "vars.config"])) of {ok, Terms} -> Terms; _Err -> [] end, RequiredOTPApps = [sasl, crypto, public_key, ssl, mnesia, inets, compiler, asn1, syntax_tools, os_mon, xmerl], ConfiguredOTPApps = lists:flatmap( fun({tools, true}) -> [tools, runtime_tools]; ({odbc, true}) -> [odbc]; (_) -> [] end, Vars), OTPApps = RequiredOTPApps ++ ConfiguredOTPApps, DepApps = lists:usort(lists:flatten(GetDeps(filename:join(TopDir, "rebar.config"), GetDeps))), Sys = [{lib_dirs, []}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, {rel, "ejabberd", proplists:get_value(vsn, Vars), [ kernel, stdlib, ejabberd ] ++ OTPApps ++ DepApps}, {rel, "start_clean", "", [ kernel, stdlib ]}, {boot_rel, "ejabberd"}, {profile, embedded}, {incl_cond, exclude}, {excl_archive_filters, [".*"]}, %% Do not archive built libs {excl_sys_filters, ["^bin/.*", "^erts.*/bin/(dialyzer|typer)", "^erts.*/(doc|info|include|lib|man|src)"]}, {excl_app_filters, ["\.gitignore"]}, {app, stdlib, [{incl_cond, include}]}, {app, kernel, [{incl_cond, include}]}, {app, ejabberd, [{incl_cond, include}, {lib_dir, ".."}]}] ++ lists:map( fun(App) -> {app, App, [{incl_cond, include}, {lib_dir, "../deps/" ++ atom_to_list(App)}]} end, DepApps) ++ lists:map( fun(App) -> {app, App, [{incl_cond, include}]} end, OTPApps). Overlay = [ {mkdir, "var/log/ejabberd"}, {mkdir, "var/lib/ejabberd"}, {mkdir, "etc/ejabberd"}, {mkdir, "doc"}, {template, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, {template, "../ejabberdctl.template", "bin/ejabberdctl"}, {copy, "../ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, {copy, "../ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}, {copy, "../inetrc", "etc/ejabberd/inetrc"}, {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"} ], Config = [{sys, Sys}, {overlay_vars, "../vars.config"}, {target_dir, "ejabberd"}, {overlay, Overlay}], %%io:format("ejabberd release:~n ~p~n", [Config]), Config. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: ejabberd-21.12/rel/vm.args.mix0000644000232200023220000000066214154362354016514 0ustar debalancedebalance## Customize flags given to the VM: https://erlang.org/doc/man/erl.html ## -mode/-name/-sname/-setcookie are configured via env vars, do not set them here -boot ../releases/<%= @version %>/start_clean -boot_var RELEASE_LIB ../lib ## Number of dirty schedulers doing IO work (file, sockets, and others) ##+SDio 5 ## Increase number of concurrent ports/sockets ##+Q 65536 ## Tweak GC to run more often ##-env ERL_FULLSWEEP_AFTER 10 ejabberd-21.12/ejabberdctl.template0000755000232200023220000002561614154362354017645 0ustar debalancedebalance#!/bin/sh # define default configuration POLL=true ERL_MAX_PORTS=32000 ERL_PROCESSES=250000 ERL_MAX_ETS_TABLES=1400 FIREWALL_WINDOW="" INET_DIST_INTERFACE="" ERLANG_NODE=ejabberd@localhost # define default environment variables [ -z "$SCRIPT" ] && SCRIPT=$0 SCRIPT_DIR="$(cd "$(dirname "$SCRIPT")" && pwd -P)" ERL="{{erl}}" IEX="{{bindir}}/iex" EPMD="{{epmd}}" INSTALLUSER="{{installuser}}" # check the proper system user is used case $(id -un) in "$INSTALLUSER") EXEC_CMD="as_current_user" ;; root) if [ -n "$INSTALLUSER" ] ; then EXEC_CMD="as_install_user" else EXEC_CMD="as_current_user" echo "WARNING: It is not recommended to run ejabberd as root" >&2 fi ;; *) if [ -n "$INSTALLUSER" ] ; then echo "ERROR: This command can only be run by root or the user $INSTALLUSER" >&2 exit 7 else EXEC_CMD="as_current_user" fi ;; esac # parse command line parameters while [ $# -gt 0 ]; do case $1 in -n|--node) ERLANG_NODE_ARG=$2; shift 2;; -s|--spool) SPOOL_DIR=$2; shift 2;; -l|--logs) LOGS_DIR=$2; shift 2;; -f|--config) EJABBERD_CONFIG_PATH=$2; shift 2;; -c|--ctl-config) EJABBERDCTL_CONFIG_PATH=$2; shift 2;; -d|--config-dir) ETC_DIR=$2; shift 2;; -t|--no-timeout) NO_TIMEOUT="--no-timeout"; shift;; *) break;; esac done # define ejabberd variables if not already defined from the command line : "${ETC_DIR:="{{sysconfdir}}/ejabberd"}" : "${LOGS_DIR:="{{localstatedir}}/log/ejabberd"}" : "${SPOOL_DIR:="{{localstatedir}}/lib/ejabberd"}" : "${EJABBERD_CONFIG_PATH:="$ETC_DIR/ejabberd.yml"}" : "${EJABBERDCTL_CONFIG_PATH:="$ETC_DIR/ejabberdctl.cfg"}" # Allows passing extra Erlang command-line arguments in vm.args file : "${VMARGS:="$ETC_DIR/vm.args"}" [ -f "$EJABBERDCTL_CONFIG_PATH" ] && . "$EJABBERDCTL_CONFIG_PATH" [ -n "$ERLANG_NODE_ARG" ] && ERLANG_NODE="$ERLANG_NODE_ARG" [ "$ERLANG_NODE" = "${ERLANG_NODE%.*}" ] && S="-s" : "${EJABBERD_DOC_PATH:="{{docdir}}"}" : "${EJABBERD_LOG_PATH:="$LOGS_DIR/ejabberd.log"}" # define erl parameters ERLANG_OPTS="+K $POLL +P $ERL_PROCESSES $ERL_OPTIONS" if [ -n "$FIREWALL_WINDOW" ] ; then ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_listen_min ${FIREWALL_WINDOW%-*} inet_dist_listen_max ${FIREWALL_WINDOW#*-}" fi if [ -n "$INET_DIST_INTERFACE" ] ; then INET_DIST_INTERFACE2=$("$ERL" -noshell -eval 'case inet:parse_address("'$INET_DIST_INTERFACE'") of {ok,IP} -> io:format("~p",[IP]); _ -> ok end.' -s erlang halt) if [ -n "$INET_DIST_INTERFACE2" ] ; then ERLANG_OPTS="$ERLANG_OPTS -kernel inet_dist_use_interface $INET_DIST_INTERFACE2" fi fi # if vm.args file exists in config directory, pass it to Erlang VM [ -f "$VMARGS" ] && ERLANG_OPTS="$ERLANG_OPTS -args_file $VMARGS" ERL_LIBS={{libdir}} ERL_CRASH_DUMP="$LOGS_DIR"/erl_crash_$(date "+%Y%m%d-%H%M%S").dump ERL_INETRC="$ETC_DIR"/inetrc # define ejabberd parameters EJABBERD_OPTS="$EJABBERD_OPTS\ $(sed '/^log_rotate_size/!d;s/:[ \t]*\([0-9]\{1,\}\).*/ \1/;s/:[ \t]*\(infinity\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")\ $(sed '/^log_rotate_count/!d;s/:[ \t]*\([0-9]*\).*/ \1/;s/^/ /' "$EJABBERD_CONFIG_PATH")" [ -n "$EJABBERD_OPTS" ] && EJABBERD_OPTS="-ejabberd $EJABBERD_OPTS" EJABBERD_OPTS="-mnesia dir \"$SPOOL_DIR\" $MNESIA_OPTIONS $EJABBERD_OPTS -s ejabberd" # export global variables export EJABBERD_CONFIG_PATH export EJABBERD_LOG_PATH export EJABBERD_DOC_PATH export EJABBERD_PID_PATH export ERL_CRASH_DUMP export ERL_EPMD_ADDRESS export ERL_INETRC export ERL_MAX_PORTS export ERL_MAX_ETS_TABLES export CONTRIB_MODULES_PATH export CONTRIB_MODULES_CONF_DIR export ERL_LIBS # run command either directly or via su $INSTALLUSER exec_cmd() { case $EXEC_CMD in as_install_user) su -s /bin/sh -c '"$0" "$@"' "$INSTALLUSER" -- "$@" ;; as_current_user) "$@" ;; esac } exec_erl() { NODE=$1; shift exec_cmd "$ERL" ${S:--}name "$NODE" $ERLANG_OPTS "$@" } exec_iex() { NODE=$1; shift exec_cmd "$IEX" -${S:--}name "$NODE" --erl "$ERLANG_OPTS" "$@" } # usage debugwarning() { if [ "$OSTYPE" != "cygwin" ] && [ "$OSTYPE" != "win32" ] ; then if [ "a$TERM" = "a" ] || [ "$TERM" = "dumb" ] ; then echo "Terminal type not supported." echo "You may have to set the TERM environment variable to fix this." exit 8 fi fi if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then echo "--------------------------------------------------------------------" echo "" echo "IMPORTANT: we will attempt to attach an INTERACTIVE shell" echo "to an already running ejabberd node." echo "If an ERROR is printed, it means the connection was not successful." echo "You can interact with the ejabberd node if you know how to use it." echo "Please be extremely cautious with your actions," echo "and exit immediately if you are not completely sure." echo "" echo "To detach this shell from ejabberd, press:" echo " control+c, control+c" echo "" echo "--------------------------------------------------------------------" echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" read -r input echo "" fi } livewarning() { if [ "$EJABBERD_BYPASS_WARNINGS" != "true" ] ; then echo "--------------------------------------------------------------------" echo "" echo "IMPORTANT: ejabberd is going to start in LIVE (interactive) mode." echo "All log messages will be shown in the command shell." echo "You can interact with the ejabberd node if you know how to use it." echo "Please be extremely cautious with your actions," echo "and exit immediately if you are not completely sure." echo "" echo "To exit this LIVE mode and stop ejabberd, press:" echo " q(). and press the Enter key" echo "" echo "--------------------------------------------------------------------" echo "To bypass permanently this warning, add to ejabberdctl.cfg the line:" echo " EJABBERD_BYPASS_WARNINGS=true" echo "Press return to continue" read -r input echo "" fi } help() { echo "" echo "Commands to start an ejabberd node:" echo " start Start an ejabberd node in server mode" echo " debug Attach an interactive Erlang shell to a running ejabberd node" echo " iexdebug Attach an interactive Elixir shell to a running ejabberd node" echo " live Start an ejabberd node in live (interactive) mode" echo " iexlive Start an ejabberd node in live (interactive) mode, within an Elixir shell" echo " foreground Start an ejabberd node in server mode (attached)" echo "" echo "Optional parameters when starting an ejabberd node:" echo " --config-dir dir Config ejabberd: $ETC_DIR" echo " --config file Config ejabberd: $EJABBERD_CONFIG_PATH" echo " --ctl-config file Config ejabberdctl: $EJABBERDCTL_CONFIG_PATH" echo " --logs dir Directory for logs: $LOGS_DIR" echo " --spool dir Database spool dir: $SPOOL_DIR" echo " --node nodename ejabberd node name: $ERLANG_NODE" echo "" } # dynamic node name helper uid() { uuid=$(uuidgen 2>/dev/null) [ -z "$uuid" ] && [ -f /proc/sys/kernel/random/uuid ] && uuid=$(cat /proc/sys/kernel/random/uuid) [ -z "$uuid" ] && uuid=$(printf "%X" "${RANDOM:-$$}$(date +%M%S)") uuid=$(printf '%s' $uuid | sed 's/^\(...\).*$/\1/') [ $# -eq 0 ] && echo "${uuid}-${ERLANG_NODE}" [ $# -eq 1 ] && echo "${uuid}-${1}-${ERLANG_NODE}" [ $# -eq 2 ] && echo "${uuid}-${1}@${2}" } # stop epmd if there is no other running node stop_epmd() { "$EPMD" -names 2>/dev/null | grep -q name || "$EPMD" -kill >/dev/null } # make sure node not already running and node name unregistered # if all ok, ensure runtime directory exists and make it current directory check_start() { "$EPMD" -names 2>/dev/null | grep -q " ${ERLANG_NODE%@*} " && { pgrep -f "$ERLANG_NODE" >/dev/null && { echo "ERROR: The ejabberd node '$ERLANG_NODE' is already running." exit 4 } pgrep beam >/dev/null && { echo "ERROR: The ejabberd node '$ERLANG_NODE' is registered," echo " but no related beam process has been found." echo "Shutdown all other erlang nodes, and call 'epmd -kill'." exit 5 } "$EPMD" -kill >/dev/null } } # allow sync calls wait_status() { # args: status try delay # return: 0 OK, 1 KO timeout="$2" status=4 while [ "$status" -ne "$1" ] ; do sleep "$3" timeout=$((timeout - 1)) if [ $timeout -eq 0 ] ; then status="$1" else exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ -extra "$ERLANG_NODE" $NO_TIMEOUT status > /dev/null status="$?" fi done [ $timeout -gt 0 ] } # ensure we can change current directory to SPOOL_DIR [ -d "$SPOOL_DIR" ] || exec_cmd mkdir -p "$SPOOL_DIR" cd "$SPOOL_DIR" || { echo "ERROR: can not access directory $SPOOL_DIR" exit 6 } # main case $1 in start) check_start exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -detached ;; foreground) check_start exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput ;; foreground-quiet) check_start exec_erl "$ERLANG_NODE" $EJABBERD_OPTS -noinput -ejabberd quiet true ;; live) livewarning check_start exec_erl "$ERLANG_NODE" $EJABBERD_OPTS ;; debug) debugwarning exec_erl "$(uid debug)" -hidden -remsh "$ERLANG_NODE" ;; etop) exec_erl "$(uid top)" -hidden -node "$ERLANG_NODE" -s etop \ -s erlang halt -output text ;; iexdebug) debugwarning exec_iex "$(uid debug)" --remsh "$ERLANG_NODE" ;; iexlive) livewarning exec_iex "$ERLANG_NODE" --erl "$EJABBERD_OPTS" --app ejabberd ;; ping) PEER=${2:-$ERLANG_NODE} [ "$PEER" = "${PEER%.*}" ] && PS="-s" exec_cmd "$ERL" ${PS:--}name "$(uid ping "$(hostname $PS)")" $ERLANG_OPTS \ -noinput -hidden -eval 'io:format("~p~n",[net_adm:ping('"'$PEER'"')])' \ -s erlang halt -output text ;; started) wait_status 0 30 2 # wait 30x2s before timeout ;; stopped) wait_status 3 30 2 && stop_epmd # wait 30x2s before timeout ;; *) exec_erl "$(uid ctl)" -hidden -noinput -s ejabberd_ctl \ -extra "$ERLANG_NODE" $NO_TIMEOUT "$@" result=$? case $result in 2|3) help;; *) :;; esac exit $result ;; esac ejabberd-21.12/configure.bat0000644000232200023220000000045714154362354016311 0ustar debalancedebalance @if "x%1"=="x--help" goto usage @set arg=dynamic @if "x%1"=="x--static" set arg=static @echo Configuring for %arg% build... erlc configure.erl erl -s configure -env arg %arg% -noshell @goto end :usage @echo Usage: configure.bat @echo or configure.bat --static @echo or configure.bat --help :end ejabberd-21.12/mix.lock0000644000232200023220000002217114154362354015304 0ustar debalancedebalance%{ "artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"}, "base64url": {:hex, :base64url, "1.0.1", "f8c7f2da04ca9a5d0f5f50258f055e1d699f0e8bf4cfdb30b750865368403cf6", [:rebar3], [], "hexpm", "f9b3add4731a02a9b0410398b475b33e7566a695365237a6bdee1bb447719f5c"}, "cache_tab": {:hex, :cache_tab, "1.0.29", "6c161988620b788d8df28c8f6af557571609c8e4b671dbadab295a4722cd501b", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a02a638021cce91ed1a8628dcbb4795bf5c01c9d11db8c613065923142824ce9"}, "distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"}, "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, "eimp": {:hex, :eimp, "1.0.21", "2e918a5dc9a1959ef8713a2360499e3baeee64cfd7881bd9d1f361ca9ddf07e8", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "998f58538f58aa0cff103414994d7ce56dc253e6576cd6fb40c1ead64aa73a28"}, "epam": {:hex, :epam, "1.0.12", "2a5625d4133bca4b3943791a3f723ba764455a461ae9b6ba5debb262efcf4b40", [:rebar3], [], "hexpm", "54c166c4459cef72f2990a3d89a8f0be27180fe0ab0f24b28ddcc3b815f49f7f"}, "esip": {:hex, :esip, "1.0.45", "2f21fb9750f7a505e6bbd43f6d48b0e879b808aba6c2224686c83f2bcd7a34bf", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stun, "1.0.47", [hex: :stun, repo: "hexpm", optional: false]}], "hexpm", "1f1eae69f2bd8d75f42c048409eabb4e3dc71ab6412fc5d998edbdade6ad5f75"}, "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, "ezlib": {:hex, :ezlib, "1.0.10", "c1c24eb18944cfde55f0574e9922d5b0392fa864282f769f82b2ea15e54f6003", [:rebar3], [], "hexpm", "1d317f1d85373686199eb3b4164d3477e95033ac68e45a95ba18e7b7a8c23241"}, "fast_tls": {:hex, :fast_tls, "1.1.13", "828cdc75e1e8fce8158846d2b971d8b4fe2b2ddcc75b759e88d751079bf78afd", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "d1f422af40c7777fe534496f508ee86515cb929ad10f7d1d56aa94ce899b44a0"}, "fast_xml": {:hex, :fast_xml, "1.1.48", "d41d14015227999a2367264cc97ac1e6770285aab1dc69545ac4f822be01a2d2", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "afcf9b808c77599395d4bd22ed4560b3d82aa1a24ff5b65f3930fe72a423b3cf"}, "fast_yaml": {:hex, :fast_yaml, "1.0.32", "43f53a2c8572f2e4d66cd4e787fc6761b1c65b9132e42c511d8b9540b0989d65", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "7258e322739ff0824237ebe44cd158e0bf52cd27a15fe731cf92f4b4c70b913e"}, "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, "jiffy": {:hex, :jiffy, "1.0.5", "a69b58faf7123534c20e1b0b7ae97ac52079ca02ed4b6989b4b380179cd63a54", [:rebar3], [], "hexpm", "b617a53f46ae84f20d0c38951367dc947a2cf8cff922aa5c6ac6b64b8b052289"}, "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, "lager": {:hex, :lager, "3.9.2", "4cab289120eb24964e3886bd22323cb5fefe4510c076992a23ad18cf85413d8c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "7f904d9e87a8cb7e66156ed31768d1c8e26eba1d54f4bc85b1aa4ac1f6340c28"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mqtree": {:hex, :mqtree, "1.0.14", "d201a79b51a9232b80e764b4b77a866f7c30a90c7ac6205d71f391eb3ea7eb31", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8626dac5e862b575eaf4836f0fc1be5a7c8435c378c5a309e34ee012d48b6f6e"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, "p1_acme": {:hex, :p1_acme, "1.0.16", "88b84cc24c9b6eb87204ea53969ccd9b524dcd4142de632441fdd2859ccab778", [:rebar3], [{:base64url, "1.0.1", [hex: :base64url, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jiffy, "1.0.5", [hex: :jiffy, repo: "hexpm", optional: false]}, {:jose, "1.11.1", [hex: :jose, repo: "hexpm", optional: false]}, {:yconf, "1.0.12", [hex: :yconf, repo: "hexpm", optional: false]}], "hexpm", "ec0ef380a7345c38b57899733f6fece97c337a3d44fd02cc8898f6a2491a38a8"}, "p1_mysql": {:hex, :p1_mysql, "1.0.19", "22f1be58397780a7d580a954e7af66cde32a29dee1a24ab2aa196272fc654a4a", [:rebar3], [], "hexpm", "88f6cdb510e8959c14b6ae84ccda04967e3de239228f859d8341da67949622b1"}, "p1_oauth2": {:hex, :p1_oauth2, "0.6.10", "09ba1fbd447b1f480b223903e36d0415f21be592a1b00db964eea01285749028", [:rebar3], [], "hexpm", "c79cb61ababee4a8c85409b7f4932035797c093aeef1f9f53985e512b26f2a64"}, "p1_pgsql": {:hex, :p1_pgsql, "1.1.12", "10ae79eeb35ea98c0424a8b6420542fef9e4469eb12ccf41475d10840c291e68", [:rebar3], [], "hexpm", "32203f779e01cf0353270df24833a1d831ad7cb3e3e8e35a7556dfa1f40948d5"}, "p1_utils": {:hex, :p1_utils, "1.0.23", "7f94466ada69bd982ea7bb80fbca18e7053e7d0b82c9d9e37621fa508587069b", [:rebar3], [], "hexpm", "47f21618694eeee5006af1c88731ad86b757161e7823c29b6f73921b571c8502"}, "pkix": {:hex, :pkix, "1.0.8", "98ea05243847fd4504f7c7a0cd82cecd1010ac327a082e1c674c5384006eae75", [:rebar3], [], "hexpm", "399508819501fab9d2e586dfa601b5ee3ef22b5612d3db58204dd2d089ef45d7"}, "stringprep": {:hex, :stringprep, "1.0.27", "02808c7024bc6285ca6a8a67e7addfc16f35dda55551a582c5181d8ea960e890", [:rebar3], [{:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "a5967b1144ca8002a58a03d16dd109fbd0bcdb82616cead2f983944314af6a00"}, "stun": {:hex, :stun, "1.0.47", "fae94c0dc7415263297e8f07f286f3355d327d8bf78b1b0743c9a5a492381f71", [:rebar3], [{:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "377d8487f4add85f6bc6ecdebdb4dcbcbe890e9462f27d6d31f3db1cf9b2cc9b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, "xmpp": {:hex, :xmpp, "1.5.6", "09259177a39c880d682817932f4da0537c471160fd43aa891ea9cb71cf827b52", [:rebar3], [{:ezlib, "1.0.10", [hex: :ezlib, repo: "hexpm", optional: false]}, {:fast_tls, "1.1.13", [hex: :fast_tls, repo: "hexpm", optional: false]}, {:fast_xml, "1.1.48", [hex: :fast_xml, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:p1_utils, "1.0.23", [hex: :p1_utils, repo: "hexpm", optional: false]}, {:stringprep, "1.0.27", [hex: :stringprep, repo: "hexpm", optional: false]}], "hexpm", "59b7317c4077d3384f9a891e0517a591cdbd44a323260b835eafbede4f4eb12e"}, "yconf": {:hex, :yconf, "1.0.12", "78c119d39bb805207fcb7671cb884805d75ee89c9ec98632b678f90a597dee2c", [:rebar3], [{:fast_yaml, "1.0.32", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "12faa51c281e95bcb6abf185fd034a242209621a7bb04b6cc411c867b192e207"}, } ejabberd-21.12/README.md0000644000232200023220000001570414154362354015120 0ustar debalancedebalanceejabberd Community Edition ========================== [![CI](https://github.com/processone/ejabberd/actions/workflows/ci.yml/badge.svg)](https://github.com/processone/ejabberd/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/github/processone/ejabberd/badge.svg?branch=master "Coverage in coveralls.io")](https://coveralls.io/github/processone/ejabberd?branch=master) [![Translation status](https://hosted.weblate.org/widgets/ejabberd/-/ejabberd-po/svg-badge.svg "Translation status in Weblate")](https://hosted.weblate.org/projects/ejabberd/ejabberd-po/) [![Hex version](https://img.shields.io/hexpm/v/ejabberd.svg "Hex version")](https://hex.pm/packages/ejabberd) ejabberd is a distributed, fault-tolerant technology that allows the creation of large-scale instant messaging applications. The server can reliably support thousands of simultaneous users on a single node and has been designed to provide exceptional standards of fault tolerance. As an open source technology, based on industry-standards, ejabberd can be used to build bespoke solutions very cost effectively. Key Features ------------ - **Cross-platform** ejabberd runs under Microsoft Windows and Unix-derived systems such as Linux, FreeBSD and NetBSD. - **Distributed** You can run ejabberd on a cluster of machines and all of them will serve the same XMPP domain(s). When you need more capacity you can simply add a new cheap node to your cluster. Accordingly, you do not need to buy an expensive high-end machine to support tens of thousands concurrent users. - **Fault-tolerant** You can deploy an ejabberd cluster so that all the information required for a properly working service will be replicated permanently on all nodes. This means that if one of the nodes crashes, the others will continue working without disruption. In addition, nodes also can be added or replaced ‘on the fly’. - **Administrator-friendly** ejabberd is built on top of the Open Source Erlang. As a result you do not need to install an external database, an external web server, amongst others because everything is already included, and ready to run out of the box. Other administrator benefits include: - Comprehensive documentation. - Straightforward installers for Linux. - Docker packaging to help with deploy / development on Linux, Windows or MacOS. - Deb and RPM packaging to support most Linux distributions. - Web administration. - Shared roster groups. - Command line administration tool. - Can integrate with existing authentication mechanisms. - Capability to send announce messages. - **Internationalized** ejabberd leads in internationalization. Hence it is very well suited in a globalized world. Related features are: - Translated to 25 languages. - Support for IDNA. - **Open Standards** ejabberd is the first Open Source XMPP server claiming to fully comply to the XMPP standard. - Fully XMPP-compliant. - XML-based protocol. - Many protocols supported. Additional Features ------------------- Moreover, ejabberd comes with a wide range of other state-of-the-art features: - **Modularity** - Load only the modules you want. - Extend ejabberd with your own custom modules. - **Security** - SASL and STARTTLS for c2s and s2s connections. - STARTTLS and Dialback s2s connections. - Web Admin accessible via HTTPS secure access. - **Databases** - Internal database for fast deployment (Mnesia). - Native MySQL support. - Native PostgreSQL support. - ODBC data storage support. - Microsoft SQL Server support. - **Authentication** - Internal authentication. - PAM, LDAP and ODBC. - External authentication script. - **Others** - Support for virtual hosting. - Compressing XML streams with Stream Compression (XEP-0138). - Statistics via Statistics Gathering (XEP-0039). - IPv6 support both for c2s and s2s connections. - Multi-User Chat module with support for clustering and HTML logging. - Users Directory based on users vCards. - Publish-Subscribe component with support for Personal Eventing. - Support for web clients: HTTP Polling and HTTP Binding (BOSH). - Component support: interface with networks such as AIM, ICQ and MSN. Quickstart guide ---------------- ### 0. Requirements To compile ejabberd you need: - GNU Make. - GCC. - Libexpat ≥ 1.95. - Libyaml ≥ 0.1.4. - Erlang/OTP ≥ 19.3. - OpenSSL ≥ 1.0.0. - Zlib ≥ 1.2.3, for Stream Compression support (XEP-0138). Optional. - PAM library. Optional. For Pluggable Authentication Modules (PAM). - ImageMagick's Convert program and Ghostscript fonts. Optional. For CAPTCHA challenges. - Elixir ≥ 1.10.3. Optional. Alternative to build ejabberd If your system splits packages in libraries and development headers, you must install the development packages also. ### 1. Compile and install on *nix systems To compile ejabberd, execute the following commands. The first one is only necessary if your source tree didn't come with a `configure` script (In this case you need autoconf installed). ./autogen.sh ./configure make To install ejabberd, run this command with system administrator rights (root user): sudo make install These commands will: - Install the configuration files in `/etc/ejabberd/` - Install ejabberd binary, header and runtime files in `/lib/ejabberd/` - Install the administration script: `/sbin/ejabberdctl` - Install ejabberd documentation in `/share/doc/ejabberd/` - Create a spool directory: `/var/lib/ejabberd/` - Create a directory for log files: `/var/log/ejabberd/` ### 2. Start ejabberd You can use the `ejabberdctl` command line administration script to start and stop ejabberd. For example: ejabberdctl start For detailed information please refer to the [ejabberd Documentation](https://docs.ejabberd.im) ### 3. Use ejabberd locally Alternatively, you can setup ejabberd without installing in your system: ./configure --with-rebar=rebar3 make dev Or, if you have Elixir available and plan to develop Elixir code: ./configure --with-rebar=mix make dev Check the full list of targets: make help Development ----------- In order to assist in the development of ejabberd, and particularly the execution of the test suite, a Vagrant environment is available at https://github.com/processone/ejabberd-vagrant-dev. To start ejabberd in development mode from the repository directory, you can type a command like: EJABBERD_CONFIG_PATH=ejabberd.yml erl -pa ebin -pa deps/*/ebin -pa test -pa deps/elixir/lib/*/ebin/ -s ejabberd Translation ----------- Using any gettext editor, you can improve the translation files found in `priv/msgs/*.po`, and then submit your changes. Alternatively, a simple way to improve translations is using our Weblate project: https://hosted.weblate.org/projects/ejabberd/ejabberd-po/ Links ----- - Documentation: https://docs.ejabberd.im - Community site: https://www.ejabberd.im - ejabberd commercial offering and support: https://www.process-one.net/en/ejabberd ejabberd-21.12/vars.config.in0000644000232200023220000000402214154362354016377 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %% Macros {roster_gateway_workaround, @roster_gateway_workaround@}. {full_xml, @full_xml@}. {db_type, @db_type@}. {debug, @debug@}. {new_sql_schema, @new_sql_schema@}. %% Ad-hoc directories with source files {tools, @tools@}. %% Dependencies {odbc, @odbc@}. {mysql, @mysql@}. {pgsql, @pgsql@}. {sqlite, @sqlite@}. {pam, @pam@}. {zlib, @zlib@}. {redis, @redis@}. {elixir, @elixir@}. {stun, @stun@}. {sip, @sip@}. {lua, @lua@}. %% Version {vsn, "@PACKAGE_VERSION@"}. %% Variables for overlay template files {description, "@PACKAGE_NAME@"}. %% Platform-specific installation paths {release, true}. {release_dir, "${SCRIPT_DIR%/*}"}. {sysconfdir, "{{release_dir}}/etc"}. {installuser, "@INSTALLUSER@"}. {erl, "{{release_dir}}/{{erts_vsn}}/bin/erl"}. {epmd, "{{release_dir}}/{{erts_vsn}}/bin/epmd"}. {localstatedir, "{{release_dir}}/var"}. {libdir, "{{release_dir}}/lib"}. {docdir, "{{release_dir}}/doc"}. {latest_deps, @latest_deps@}. {system_deps, @system_deps@}. {ldflags, "@LDFLAGS@"}. {cflags, "@CFLAGS@"}. {cppflags, "@CPPFLAGS@"}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: ejabberd-21.12/ejabberdctl.cfg.example0000644000232200023220000001215014154362354020205 0ustar debalancedebalance# # In this file you can configure options that are passed by ejabberdctl # to the erlang runtime system when starting ejabberd # #' POLL: Kernel polling ([true|false]) # # The kernel polling option requires support in the kernel. # Additionally, you need to enable this feature while compiling Erlang. # # Default: true # #POLL=true #. #' ERL_MAX_PORTS: Maximum number of simultaneously open Erlang ports # # ejabberd consumes two or three ports for every connection, either # from a client or from another XMPP server. So take this into # account when setting this limit. # # Default: 65536 (or 8196 on Windows) # Maximum: 268435456 # #ERL_MAX_PORTS=65536 #. #' FIREWALL_WINDOW: Range of allowed ports to pass through a firewall # # If ejabberd is configured to run in cluster, and a firewall is blocking ports, # it's possible to make Erlang use a defined range of port (instead of dynamic # ports) for node communication. # # Default: not defined # Example: 4200-4210 # #FIREWALL_WINDOW= #. #' INET_DIST_INTERFACE: IP address where this Erlang node listens other nodes # # This communication is used by ejabberdctl command line tool, # and in a cluster of several ejabberd nodes. # # Default: 0.0.0.0 # #INET_DIST_INTERFACE=127.0.0.1 #. #' ERL_EPMD_ADDRESS: IP addresses where epmd listens for connections # # This environment variable may be set to a comma-separated # list of IP addresses, in which case the epmd daemon # will listen only on the specified address(es) and on the # loopback address (which is implicitly added to the list if it # has not been specified). The default behaviour is to listen on # all available IP addresses. # # Default: 0.0.0.0 # #ERL_EPMD_ADDRESS=127.0.0.1 #. #' ERL_PROCESSES: Maximum number of Erlang processes # # Erlang consumes a lot of lightweight processes. If there is a lot of activity # on ejabberd so that the maximum number of processes is reached, people will # experience greater latency times. As these processes are implemented in # Erlang, and therefore not related to the operating system processes, you do # not have to worry about allowing a huge number of them. # # Default: 262144 # Maximum: 268435456 # #ERL_PROCESSES=262144 #. #' ERL_MAX_ETS_TABLES: Maximum number of ETS and Mnesia tables # # The number of concurrent ETS and Mnesia tables is limited. When the limit is # reached, errors will appear in the logs: # ** Too many db tables ** # You can safely increase this limit when starting ejabberd. It impacts memory # consumption but the difference will be quite small. # # Default: 2053 # #ERL_MAX_ETS_TABLES=2053 #. #' ERL_OPTIONS: Additional Erlang options # # The next variable allows to specify additional options passed to erlang while # starting ejabberd. Some useful options are -noshell, -detached, -heart. When # ejabberd is started from an init.d script options -noshell and -detached are # added implicitly. See erl(1) for more info. # # It might be useful to add "-pa /usr/local/lib/ejabberd/ebin" if you # want to add local modules in this path. # # Default: "" # #ERL_OPTIONS="" #. #' ERLANG_NODE: Erlang node name # # The next variable allows to explicitly specify erlang node for ejabberd # It can be given in different formats: # ERLANG_NODE=ejabberd # Lets erlang add hostname to the node (ejabberd uses short name in this case) # ERLANG_NODE=ejabberd@hostname # Erlang uses node name as is (so make sure that hostname is a real # machine hostname or you'll not be able to control ejabberd) # ERLANG_NODE=ejabberd@hostname.domainname # The same as previous, but erlang will use long hostname # (see erl (1) manual for details) # # Default: ejabberd@localhost # #ERLANG_NODE=ejabberd@localhost #. #' EJABBERD_PID_PATH: ejabberd PID file # # Indicate the full path to the ejabberd Process identifier (PID) file. # If this variable is defined, ejabberd writes the PID file when starts, # and deletes it when stops. # Remember to create the directory and grant write permission to ejabberd. # # Default: don't write PID file # #EJABBERD_PID_PATH=/var/run/ejabberd/ejabberd.pid #. #' EJABBERD_CONFIG_PATH: ejabberd configuration file # # Specify the full path to the ejabberd configuration file. If the file name has # yml or yaml extension, it is parsed as a YAML file; otherwise, Erlang syntax is # expected. # # Default: $ETC_DIR/ejabberd.yml # #EJABBERD_CONFIG_PATH=/etc/ejabberd/ejabberd.yml #. #' CONTRIB_MODULES_PATH: contributed ejabberd modules path # # Specify the full path to the contributed ejabberd modules. If the path is not # defined, ejabberd will use ~/.ejabberd-modules in home of user running ejabberd. # # Default: $HOME/.ejabberd-modules # #CONTRIB_MODULES_PATH=/opt/ejabberd-modules #. #' CONTRIB_MODULES_CONF_DIR: configuration directory for contributed modules # # Specify the full path to the configuration directory for contributed ejabberd # modules. In order to configure a module named mod_foo, a mod_foo.yml file can # be created in this directory. This file will then be used instead of the # default configuration file provided with the module. # # Default: $CONTRIB_MODULES_PATH/conf # #CONTRIB_MODULES_CONF_DIR=/etc/ejabberd/modules #. #' # vim: foldmarker=#',#. foldmethod=marker: ejabberd-21.12/COPYING0000644000232200023220000004332414154362354014673 0ustar debalancedebalanceAs a special exception, the authors give permission to link this program with the OpenSSL library and distribute the resulting binary. GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. ejabberd-21.12/rebar.config.script0000644000232200023220000002777014154362354017434 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- Vars = case file:consult(filename:join([filename:dirname(SCRIPT),"vars.config"])) of {ok, Terms} -> Terms; _Err -> [] end ++ [{cflags, "-g -O2 -Wall"}, {cppflags, "-g -O2 -Wall"}, {ldflags, ""}, {system_deps, false}], {cflags, CFlags} = lists:keyfind(cflags, 1, Vars), {cppflags, CPPFlags} = lists:keyfind(cppflags, 1, Vars), {ldflags, LDFlags} = lists:keyfind(ldflags, 1, Vars), {system_deps, SystemDeps} = lists:keyfind(system_deps, 1, Vars), GetCfg = fun GetCfg(Cfg, [Key | Tail], Default) -> Val = case lists:keyfind(Key, 1, Cfg) of {Key, V1} -> V1; false -> Default end, case Tail of [] -> Val; _ -> GetCfg(Val, Tail, Default) end end, ModCfg = fun ModCfg(Cfg, [Key | Tail], Op, Default) -> {OldVal, PartCfg} = case lists:keytake(Key, 1, Cfg) of {value, {_, V1}, V2} -> {V1, V2}; false -> {if Tail == [] -> Default; true -> [] end, Cfg} end, case Tail of [] -> [{Key, Op(OldVal)} | PartCfg]; _ -> [{Key, ModCfg(OldVal, Tail, Op, Default)} | PartCfg] end end, FilterConfig = fun FilterConfig(Cfg, [{Path, true, ModFun, Default} | Tail]) -> FilterConfig(ModCfg(Cfg, Path, ModFun, Default), Tail); FilterConfig(Cfg, [{Path, SourcePath, true, ModFun, Default, SourceDefault} | Tail]) -> SourceVal = GetCfg(Cfg, SourcePath, SourceDefault), ModFun2 = fun(V) -> ModFun(V, SourceVal) end, FilterConfig(ModCfg(Cfg, Path, ModFun2, Default), Tail); FilterConfig(Cfg, [_ | Tail]) -> FilterConfig(Cfg, Tail); FilterConfig(Cfg, []) -> Cfg end, IsRebar3 = case application:get_key(rebar, vsn) of {ok, VSN} -> [VSN1 | _] = string:tokens(VSN, "-"), [Maj|_] = string:tokens(VSN1, "."), (list_to_integer(Maj) >= 3); undefined -> lists:keymember(mix, 1, application:loaded_applications()) end, SysVer = erlang:system_info(otp_release), ProcessSingleVar = fun(F, Var, Tail) -> case F([Var], []) of [] -> Tail; [Val] -> [Val | Tail] end end, ProcessVars = fun F([], Acc) -> lists:reverse(Acc); F([{Type, Ver, Value} | Tail], Acc) when Type == if_version_above orelse Type == if_version_below -> SysVer = erlang:system_info(otp_release), Include = if Type == if_version_above -> SysVer > Ver; true -> SysVer < Ver end, if Include -> F(Tail, ProcessSingleVar(F, Value, Acc)); true -> F(Tail, Acc) end; F([{Type, Ver, Value, ElseValue} | Tail], Acc) when Type == if_version_above orelse Type == if_version_below -> Include = if Type == if_version_above -> SysVer > Ver; true -> SysVer < Ver end, if Include -> F(Tail, ProcessSingleVar(F, Value, Acc)); true -> F(Tail, ProcessSingleVar(F, ElseValue, Acc)) end; F([{Type, Var, Value} | Tail], Acc) when Type == if_var_true orelse Type == if_var_false -> Flag = Type == if_var_true, case proplists:get_bool(Var, Vars) of V when V == Flag -> F(Tail, ProcessSingleVar(F, Value, Acc)); _ -> F(Tail, Acc) end; F([{Type, Value} | Tail], Acc) when Type == if_rebar3 orelse Type == if_not_rebar3 -> Flag = Type == if_rebar3, case IsRebar3 == Flag of true -> F(Tail, ProcessSingleVar(F, Value, Acc)); _ -> F(Tail, Acc) end; F([{Type, Var, Match, Value} | Tail], Acc) when Type == if_var_match orelse Type == if_var_no_match -> case proplists:get_value(Var, Vars) of V when V == Match -> F(Tail, ProcessSingleVar(F, Value, Acc)); _ -> F(Tail, Acc) end; F([{if_have_fun, MFA, Value} | Tail], Acc) -> {Mod, Fun, Arity} = MFA, code:ensure_loaded(Mod), case erlang:function_exported(Mod, Fun, Arity) of true -> F(Tail, ProcessSingleVar(F, Value, Acc)); false -> F(Tail, Acc) end; F([Other1 | Tail1], Acc) -> F(Tail1, [F(Other1, []) | Acc]); F(Val, Acc) when is_tuple(Val) -> list_to_tuple(F(tuple_to_list(Val), Acc)); F(Other2, _Acc) -> Other2 end, MaybeApply = fun(Val) when is_function(Val) -> Val(); (Val) -> Val end, MaybeApply2 = fun(Val, Arg) when is_function(Val) -> Val(Arg); (Val, _) -> Val end, AppendStr = fun(Append) -> fun("") -> lists:flatten(MaybeApply(Append)); (Val) -> lists:flatten([Val, " ", MaybeApply(Append)]) end end, AppendList = fun(Append) -> fun(Val) -> Val ++ MaybeApply(Append) end end, AppendStr2 = fun(Append) -> fun("", Arg) -> lists:flatten(MaybeApply2(Append, Arg)); (Val, Arg) -> lists:flatten([Val, " ", MaybeApply2(Append, Arg)]) end end, AppendList2 = fun(Append) -> fun(Val, Arg) -> Val ++ MaybeApply2(Append, Arg) end end, Rebar3DepsFilter = fun(DepsList, GitOnlyDeps) -> lists:map(fun({DepName, _, {git, _, {tag, Version}}} = Dep) -> case lists:member(DepName, GitOnlyDeps) of true -> Dep; _ -> {DepName, Version} end; (Dep) -> Dep end, DepsList) end, DepAlts = fun("esip") -> ["esip", "p1_sip"]; ("xmpp") -> ["xmpp", "p1_xmpp"]; ("fast_xml") -> ["fast_xml", "p1_xml"]; (Val) -> [Val] end, LibDirInt = fun F([Dep|Rest], Suffix) -> case code:lib_dir(Dep) of {error, _} -> F(Rest, Suffix); V -> V ++ Suffix end; F([], _) -> error end, LibDir = fun(Name, Suffix) -> LibDirInt(DepAlts(Name), Suffix) end, GlobalDepsFilter = fun(Deps) -> DepNames = lists:map(fun({DepName, _, _}) -> DepName; ({DepName, _}) -> DepName end, Deps), lists:filtermap(fun(Dep) -> case LibDir(atom_to_list(Dep), "") of error -> exit("Unable to locate dep '" ++ atom_to_list(Dep) ++ "' in system deps."); _ -> false end end, DepNames) end, {ok, Cwd} = file:get_cwd(), TestConfigFile = filename:join([Cwd, "test", "config.ctc"]), TestConfig = case file:read_file_info(TestConfigFile) of {ok, _} -> [" -userconfig ct_config_plain ", TestConfigFile, " "]; _ -> "" end, ResolveDepPath = case {SystemDeps, IsRebar3} of {true, _} -> fun("deps/" ++ Rest) -> Slash = string:str(Rest, "/"), case LibDir(string:sub_string(Rest, 1, Slash -1), string:sub_string(Rest, Slash)) of error -> Rest; V -> V end; (Path) -> Path end; {_, true} -> fun("deps/" ++ Rest) -> Slash = string:str(Rest, "/"), "_build/default/lib/" ++ string:sub_string(Rest, 1, Slash - 1) ++ string:sub_string(Rest, Slash); (Path) -> Path end; _ -> fun(P) -> P end end, CtParams = fun(CompileOpts) -> ["-ct_hooks cth_surefire ", lists:map(fun({i, IncPath}) -> [" -include ", filename:absname(ResolveDepPath(IncPath), Cwd)] end, CompileOpts), TestConfig] end, GenDepConfigureLine = fun(DepPath, Flags) -> ["sh -c 'if test ! -f config.status -o ", "../../config.status -nt config.status; ", "then (", "CFLAGS=\"", CFlags,"\" ", "CPPFLAGS=\"", CPPFlags, "\" " "LDFLAGS=\"", LDFlags, "\"", " ./configure ", string:join(Flags, " "), "); fi'"] end, GenDepsConfigure = fun(Hooks) -> lists:map(fun({Pkg, Flags}) -> DepPath = ResolveDepPath("deps/" ++ Pkg ++ "/"), Line = lists:flatten(GenDepConfigureLine(DepPath, Flags)), {add, list_to_atom(Pkg), [{pre_hooks, [{{pc, compile}, Line}, {'compile', Line}, {'configure-deps', Line}]}]} end, Hooks) end, ProcessErlOpt = fun(Vals) -> R = lists:map( fun({i, Path}) -> {i, ResolveDepPath(Path)}; (ErlOpt) -> ErlOpt end, Vals), M = lists:filter(fun({d, M}) -> true; (_) -> false end, R), [{d, 'ALL_DEFS', M} | R] end, ProcssXrefExclusions = fun(Items) -> [{lists:flatten(["(XC - UC) || (XU - X - B ", [[" - ", V] || V <- Items], ")"]), []}] end, ProcessFloatingDeps = fun(Deps, FDeps) -> lists:map(fun({DepName, _Ver, {git, Repo, _Commit}} = Dep) -> case lists:member(DepName, FDeps) of true -> {DepName, ".*", {git, Repo}}; _ -> Dep end; (Dep2) -> Dep2 end, Deps) end, VarsApps = case file:consult(filename:join([filename:dirname(SCRIPT),"vars.config"])) of {ok, TermsV} -> case proplists:get_bool(odbc, TermsV) of true -> [odbc]; false -> [] end; _-> [] end, ProcessRelx = fun(Relx, Deps) -> {value, {release, NameVersion, DefaultApps}, RelxTail} = lists:keytake(release, 1, Relx), ProfileApps = case os:getenv("REBAR_PROFILE") of "dev" -> [observer, runtime_tools, wx, debugger]; _ -> [] end, DepApps = lists:map(fun({DepName, _, _}) -> DepName; ({DepName, _}) -> DepName end, Deps), [{release, NameVersion, DefaultApps ++ VarsApps ++ ProfileApps ++ DepApps} | RelxTail] end, GithubConfig = case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of {"true", Token} when is_list(Token) -> CONFIG1 = [{coveralls_repo_token, Token}, {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")}, {coveralls_commit_sha, os:getenv("GITHUB_SHA")}, {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")}], case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request" andalso string:tokens(os:getenv("GITHUB_REF"), "/") of [_, "pull", PRNO, _] -> [{coveralls_service_pull_request, PRNO} | CONFIG1]; _ -> CONFIG1 end; _ -> [] end, Rules = [ {[provider_hooks], IsRebar3, AppendList([{pre, [ {compile, {asn, compile}}, {clean, {asn, clean}} ]}]), []}, {[plugins], IsRebar3 and (os:getenv("GITHUB_ACTIONS") == "true"), AppendList([{coveralls, {git, "https://github.com/processone/coveralls-erl.git", {branch, "addjsonfile"}}} ]), []}, {[overrides], [post_hook_configure], SystemDeps == false, AppendList2(GenDepsConfigure), [], []}, {[ct_extra_params], [eunit_compile_opts], true, AppendStr2(CtParams), "", []}, {[erl_opts], true, ProcessErlOpt, []}, {[xref_queries], [xref_exclusions], true, AppendList2(ProcssXrefExclusions), [], []}, {[relx], [deps], IsRebar3, ProcessRelx, [], []}, {[deps], [floating_deps], true, ProcessFloatingDeps, [], []}, {[deps], [gitonly_deps], IsRebar3, Rebar3DepsFilter, [], []}, {[deps], SystemDeps /= false, GlobalDepsFilter, []} ], Config = [{plugin_dir, filename:join([filename:dirname(SCRIPT),"plugins"])}]++ FilterConfig(ProcessVars(CONFIG, []), Rules)++ GithubConfig, %io:format("ejabberd configuration:~n ~p~n", [Config]), Config. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: ejabberd-21.12/CHANGELOG.md0000644000232200023220000005457414154362354015462 0ustar debalancedebalance# Version 21.12 Commands - `create_room_with_opts`: Fixed when using SQL storage - `change_room_option`: Add missing fields from config inside `mod_muc_admin:change_options` - piefxis: Fixed arguments of all commands Modules - mod_caps: Don't forget caps on XEP-0198 resumption - mod_conversejs: New module to serve a simple page for Converse.js - mod_http_upload_quota: Avoid `max_days` race - mod_muc: Support MUC hats (XEP-0317, conversejs/prosody compatible) - mod_muc: Optimize MucSub processing - mod_muc: Fix exception in mucsub {un}subscription events multicast handler - mod_multicast: Improve and optimize multicast routing code - mod_offline: Allow storing non-composing x:events in offline - mod_ping: Send ping from server, not bare user JID - mod_push: Fix handling of MUC/Sub messages - mod_register: New allow_modules option to restrict registration modules - mod_register_web: Handle unknown host gracefully - mod_register_web: Use mod_register configured restrictions PubSub - Add `delete_expired_pubsub_items` command - Add `delete_old_pubsub_items` command - Optimize publishing on large nodes (SQL) - Support unlimited number of items - Support `max_items=max` node configuration - Bump default value for `max_items` limit from 10 to 1000 - Use configured `max_items` by default - node_flat: Avoid catch-all clauses for RSM - node_flat_sql: Avoid catch-all clauses for RSM SQL - Use `INSERT ... ON CONFLICT` in SQL_UPSERT for PostgreSQL >= 9.5 - mod_mam export: assign MUC entries to the MUC service - MySQL: Fix typo when creating index - PgSQL: Add SASL auth support, PostgreSQL 14 - PgSQL: Add missing SQL migration for table `push_session` - PgSQL: Fix `vcard_search` definition in pgsql new schema Other - `captcha-ng.sh`: "sort -R" command not POSIX, added "shuf" and "cat" as fallback - Make s2s connection table cleanup more robust - Update export/import of scram password to XEP-0227 1.1 - Update Jose to 1.11.1 (the last in hex.pm correctly versioned) # Version 21.07 Compilation - Add rebar3 3.15.2 binary - Add support for mix to: `./configure --enable-rebar=mix` - Improved `make rel` to work with rebar3 and mix - Add `make dev` to build a development release with rebar3 or mix - Hex: Add `sql/` and `vars.config` to Hex package files - Hex: Update mix applications list to fix error `p1_utils is listed as both...` - There are so many targets in Makefile... add `make help` - Fix extauth.py failure in test suite with Python 3 - Added experimental support for GitHub Codespaces - Switch test service from TravisCI to GitHub Actions Commands: - Display extended error message in ejabberdctl - Remove SMP option from ejabberdctl.cfg, `-smp` was removed in OTP 21 - `create_room`: After creating room, store in DB if it's persistent - `help`: Major changes in its usage and output - `srg_create`: Update to use `label` parameter instead of `name` Modules: - ejabberd_listener: New `send_timeout` option - mod_mix: Improvements to update to 0.14.1 - mod_muc_room: Don't leak owner JIDs - mod_multicast: Routing for more MUC packets - mod_multicast: Correctly strip only other bcc addresses - mod_mqtt: Allow shared roster group placeholder in mqtt topic - mod_pubsub: Several fixes when using PubSub with RSM - mod_push: Handle MUC/Sub events correctly - mod_shared_roster: Delete cache after performing change to be sure that in cache will be up to date data - mod_shared_roster: Improve database and caching - mod_shared_roster: Reconfigure cache when options change - mod_vcard: Fix invalid_encoding error when using extended plane characters in vcard - mod_vcard: Update econf:vcard() to generate correct vcard_temp record - WebAdmin: New simple pages to view mnesia tables information and content - WebSocket: Fix typos SQL: - MySQL Backend Patch for scram-sha512 - SQLite: When exporting for SQLite, use its specific escape options - SQLite: Minor fixes for new_sql_schema support - mod_privacy: Cast as boolean when exporting privacy_list_data to PostgreSQL - mod_mqtt: Add mqtt_pub table definition for MSSQL - mod_shared_roster: Add missing indexes to `sr_group` tables in all SQL databases # Version 21.04 API Commands: - `add_rosteritem/...`: Add argument guards to roster commands - `get_user_subscriptions`: New command for MUC/Sub - `remove_mam_for_user_with_peer`: Fix when removing room archive - `send_message`: Fix bug introduced in ejabberd 21.01 - `set_vcard`: Return modules errors Build and setup: - Allow ejabberd to be compatible as a dependency for an Erlang project using rebar3 - CAPTCHA: New question/answer-based CAPTCHA script - `--enable-lua`: new configure option for luerl instead of --enable-tools - Remove support for HiPE, it was experimental and Erlang/OTP 24 removes it - Update `sql_query` record to handle the Erlang/OTP 24 compiler reports - Updated dependencies to fix Dialyzer warnings Miscellaneous: - CAPTCHA: Update `FORM_TYPE` from captcha to register - LDAP: fix eldap certificate verification - MySQL: Fix for "specified key was too long" - Translations: updated the Esperanto, Greek, and Japanese translations - Websocket: Fix PONG responses Modules: - `mod_block_strangers`: If stanza is type error, allow it passing - `mod_caps`: Don't request roster when not needed - `mod_caps`: Skip reading roster in one more case - `mod_mam`: Remove `queryid` from MAM fin element - `mod_mqtt`: When deregistering XMPP account, close its MQTT sessions - `mod_muc`: Take in account subscriber's affiliation when checking access to moderated room - `mod_muc`: Use monitors to track online and hard-killed rooms - `mod_muc`: When occupant is banned, remove his subscriptions too - `mod_privacy`: Make fetching roster lazy - `mod_pubsub`: Don't fail on PEP unsubscribe - `mod_pubsub`: Fix `gen_pubsub_node:get_state` return value - `mod_vcard`: Obtain and provide photo type in vCard LDAP # Version 21.01 Miscellaneous changes: - `log_rotate_size` option: Fix handling of ‘infinity’ value - `mod_time`: Fix invalid timezone - Auth JWT: New `check_decoded_jwt` hook runs the default JWT verifier - MUC: Allow non-occupant non-subscribed service admin send private MUC message - MUC: New `max_password` and `max_captcha_whitelist` options - OAuth: New `oauth_cache_rest_failure_life_time` option - PEP: Skip reading pep nodes that we know won’t be requested due to caps - SQL: Add sql script to migrate mysql from old schema to new - SQL: Don’t use REPLACE for upsert when there are “-” fields. - Shared Rosters LDAP: Add multi-domain support (and flexibility) - Sqlite3: Fix dependency version - Stun: Block loopback addresses by default - Several documentation fixes and clarifications Commands: - `decide_room`: Use better fallback value for room activity time when skipping room - `delete_old_message`: Fix when using sqlite spool table - `module_install`: Make ext_mod compile module with debug_info flags - `room_unused_*`: Don’t fetch subscribers list - `send_message`: Don’t include empty in messages - `set_room_affiliation`: Validate affiliations Running: - Docker: New `Dockerfile` and `devcontainer.json` - New `ejabberdctl foreground-quiet` - Systemd: Allow for listening on privileged ports - Systemd: Integrate nicely with systemd Translations: - Moved gettext PO files to a new `ejabberd-po` repository - Improved several translations: Catalan, Chinese, German, Greek, Indonesian, Norwegian, Portuguese (Brazil), Spanish. # Version 20.12 - Add support for `SCRAM-SHA-{256,512}-{PLUS}` authentication - Don't use same value in cache for user don't exist and wrong password - `outgoing_s2s_ipv*_address`: New options to set ipv4/ipv6 outbound s2s out interface - s2s_send_packet: this hook now filters outgoing s2s stanzas - start_room: new hook runs when a room process is started - check_decoded_jwt: new hook to check decoded JWT after success authentication * Admin - Docker: Fix DB initialization - New sql_odbc_driver option: choose the mssql ODBC driver - Rebar3: Fully supported. Enable with `./configure --with-rebar=/path/to/rebar3` - systemd: start ejabberd in foreground * Modules: - MAM: Make sure that jid used as base in mam xml_compress is bare - MAM: Support for MAM Flipped Pages - MUC: Always show MucSub subscribers nicks - MUC: Don't forget not-persistent rooms in load_permanent_rooms - MUC Admin: Better error reporting - MUC Admin: Fix commands with hibernated rooms - MUC Admin: Many improvements in rooms_unused_list/destroy - MUC Admin: create_room_with_opts Store options only if room starts - Pubsub: Remove 'dag' node plugin documentation - Push: Fix API call return type on error - Push: Support cache config changes on reload - Register: Allow for account-removal-only setup again - Roster: Make roster subscriptions work better with invalid roster state in db - Vcard: Fix vCard search by User when using Mnesia - WebAdmin: Allow vhost admins to view WebAdmin menus - WebAdmin: Don't do double utf-8 conversion on translated strings - WebAdmin: Mark dangerous buttons with CSS - WebSocket: Make websocket send put back pressure on c2s process # Version 20.07 * Changes in this version - Add support for using unix sockets in listeners. - Make this version compatible with erlang R23 - Make room permissions checks more strict for subscribers - Fix problem with muc rooms crashing when using muc logger with some locales - Limit stat calls that logger module issues - Don't throw errors when using user_regexp acl rule and having non-matching host - Fix problem with leaving old data when updating shared rosters - Fix edge case that caused failure of resuming old sessions with stream management. - Fix crash when room that was started with loging enabled was later changed to logging disabled - Increase default shaper limits (this should help with delays for clients that are using jingle) - Fix couple compatibility problems which prevented working on erlang R19 - Fix sending presence unavailable when session terminates for clients that only send directed presences (helps with sometimes not leaving muc rooms on disconnect). - Prevent supervisor errors for sockets that were closed before they were passed to handler modules - Make stun module work better with ipv6 addresses # Version 20.03 * Changes in this version - Add support of ssl connection when connection to mysql database (configured with `sql_ssl: true` option) - Experimental support for cockroachdb when configured with postgres connector - Add cache and optimize queries issued by `mod_shared_roster`, this should greatly improve performance of this module when used with `sql` backend - Fix problem with accessing webadmin - Make webadmin work even when url is missing trailing slash - When compiling external modules with ext_mod, use flags that were detected during compilation of ejabberd - Make config changed to ldap options be updated when issued `reload_config` command - Fix `room_empty_destory` command - Fix reporting errors in `send_stanza` command when xml passed to it couldn't be passed correctly # Version 20.02 * Changes in this version - Fix problems when trying to use string format with unicode values directly in xmpp nodes - Add missing oauth_client table declaration in lite.new.sql - Improve compatibility with CocroachDB - Fix importing of piefxis files that did use scram passwords - Fix importing of piefxis files that had multiple includes in them - Update jiffy dependency - Allow storage of emojis when using mssql database (Thanks to Christoph Scholz) - Make ejabberd_auth_http be able to use auth_opts - Make custom_headers options in http modules correctly override built-in values - Fix return value of reload_config and dump_config commands # Version 20.01 * New features - Implement OAUTH authentication in mqtt - Make logging infrastructure use new logger introduced in Erlang (requires OTP22) - New configuration parser/validator - Initial work on being able to use CockroachDB as database backend - Add gc command - Add option to disable using prepared statements on Postgresql - Implement routine for converting password to SCRAM format for all backends not only SQL - Add infrastructure for having module documentation directly in individual module source code - Generate man page automaticaly - Implement copy feature in mod_carboncopy * Fixes - Make webadmin work with configurable paths - Fix handling of result in xmlrpc module - Make webadmin work even when accessed through not declared domain - Better error reporting in xmlrpc - Limit ammount of results returned by disco queries to pubsub nodes - Improve validation of configured JWT keys - Fix race condition in Redis/SQL startup - Fix loading order of third party modules - Fix reloading of ACL rules - Make account removal requests properly route response - Improve handling of malformed inputs in send_message command - Omit push notification if storing message in offline storage failed - Fix crash in stream management when timeout was not set # Version 19.09 * Admin - The minimum required Erlang/OTP version is now 19.3 - Fix API call using OAuth (#2982) - Rename MUC command arguments from Host to Service (#2976) * Webadmin - Don't treat 'Host' header as a virtual XMPP host (#2989) - Fix some links to Guide in WebAdmin and add new ones (#3003) - Use select fields to input host in WebAdmin Backup (#3000) - Check account auth provided in WebAdmin is a local host (#3000) * ACME - Improve ACME implementation - Fix IDA support in ACME requests - Fix unicode formatting in ACME module - Log an error message on IDNA failure - Support IDN hostnames in ACME requests - Don't attempt to create ACME directory on ejabberd startup - Don't allow requesting certificates for localhost or IP-like domains - Don't auto request certificate for localhost and IP-like domains - Add listener for ACME challenge in example config * Authentication - JWT-only authentication for some users (#3012) * MUC - Apply default role after revoking admin affiliation (#3023) - Custom exit message is not broadcast (#3004) - Revert "Affiliations other than admin and owner cannot invite to members_only rooms" (#2987) - When join new room with password, set pass and password_protected (#2668) - Improve rooms_* commands to accept 'global' as MUC service argument (#2976) - Rename MUC command arguments from Host to Service (#2976) * SQL - Fix transactions for Microsoft SQL Server (#2978) - Spawn SQL connections on demand only * Misc - Add support for XEP-0328: JID Prep - Added gsfonts for captcha - Log Mnesia table type on creation - Replicate Mnesia 'bosh' table when nodes are joined - Fix certificate selection for s2s (#3015) - Provide meaningful error when adding non-local users to shared roster (#3000) - Websocket: don't treat 'Host' header as a virtual XMPP host (#2989) - Fix sm ack related c2s error (#2984) - Don't hide the reason why c2s connection has failed - Unicode support - Correctly handle unicode in log messages - Fix unicode processing in ejabberd.yml # Version 19.08 * Administration - Improve ejabberd halting procedure - Process unexpected erlang messages uniformly: logging a warning - mod_configure: Remove modules management * Configuration - Use new configuration validator - ejabberd_http: Use correct virtual host when consulting trusted_proxies - Fix Elixir modules detection in the configuration file - Make option 'validate_stream' global - Allow multiple definitions of host_config and append_host_config - Introduce option 'captcha_url' - mod_stream_mgmt: Allow flexible timeout format - mod_mqtt: Allow flexible timeout format in session_expiry option * Misc - Fix SQL connections leakage - New authentication method using JWT tokens - extauth: Add 'certauth' command - Improve SQL pool logic - Add and improve type specs - Improve extraction of translated strings - Improve error handling/reporting when loading language translations - Improve hooks validator and fix bugs related to hooks registration - Gracefully close inbound s2s connections - mod_mqtt: Fix usage of TLS - mod_offline: Make count_offline_messages cache work when using mam for storage - mod_privacy: Don't attempt to query 'undefined' active list - mod_privacy: Fix race condition * MUC - Add code for hibernating inactive muc_room processes - Improve handling of unexpected iq in mod_muc_room - Attach mod_muc_room processes to a supervisor - Restore room when receiving message or generic iq for not started room - Distribute routing of MUC messages accross all CPU cores * PubSub - Fix pending nodes retrieval for SQL backend - Check access_model when publishing PEP - Remove deprecated pubsub plugins - Expose access_model and publish_model in pubsub#metadata # Version 19.05 * Admin - The minimum required Erlang/OTP version is now 19.1 - Provide a suggestion when unknown command, module, option or request handler is detected - Deprecate some listening options: captcha, register, web_admin, http_bind and xmlrpc - Add commands to get Mnesia info: mnesia_info and mnesia_table_info - Fix Register command to respect mod_register's Access option - Fixes in Prosody import: privacy and rooms - Remove TLS options from the example config - Improve request_handlers validator - Fix syntax in example Elixir config file * Auth - Correctly support cache tags in ejabberd_auth - Don't process failed EXTERNAL authentication by mod_fail2ban - Don't call to mod_register when it's not loaded - Make anonymous auth don't {de}register user when there are other resources * Developer - Rename listening callback from start/2 to start/3 - New hook called when room gets destroyed: room_destroyed - New hooks for tracking mucsub subscriptions changes: muc_subscribed, muc_unsubscribed - Make static hooks analyzer working again * MUC - Service admins are allowed to recreate room even if archiv is nonempty - New option user_mucsub_from_muc_archive - Avoid late arrival of get_disco_item response - Handle get_subscribed_rooms call from mod_muc_room pid - Fix room state cleanup from db on change of persistent option change - Make get_subscribed_rooms work even for non-persistant rooms - Allow non-moderator subscribers to get list of room subscribers * Offline - New option bounce_groupchat: make it not bounce mucsub/groupchat messages - New option use_mam_for_storage: fetch data from mam instead of spool table - When applying limit of max msgs in spool check only spool size - Do not store mucsub wrapped messages with no-store hint in offline storage - Always store ActivityMarker messages - Don't issue count/message fetch queries for offline from mam when not needed - Properly handle infinity as max number of message in mam offline storage - Sort messages by stanza_id when using mam storage in mod_offline - Return correct value from count_offline_messages with mam storage option - Make mod_offline put msg ignored by mam in spool when mam storage is on * SQL: - Add SQL schemas for MQTT tables - Report better errors on SQL terms decode failure - Fix PostgreSQL compatibility in mod_offline_sql:remove_old_messages - Fix handling of list arguments on pgsql - Preliminary support for SQL in process_rosteritems command * Tests - Add tests for user mucsub mam from muc mam - Add tests for offline with mam storage - Add tests for offline use_mam_for_storage - Initial Docker environment to run ejabberd test suite - Test offline:use_mam_for_storage, mam:user_mucsub_from_muc_archive used together * Websocket - Add WebSockets support to mod_mqtt - Return "Bad request" error when origin in websocket connection doesn't match - Fix RFC6454 violation on websocket connection when validating Origin header - Origin header validation on websocket connection * Other modules - mod_adhoc: Use xml:lang from stanza when it's missing in element - mod_announce: Add 'sessionid' attribute when required - mod_bosh: Don't put duplicate polling attribute in bosh payload - mod_http_api: Improve argument error messages and log messages - mod_http_upload: Feed whole image to eimp:identify/1 - mod_http_upload: Log nicer warning on unknown host - mod_http_upload: Case-insensitive host comparison - mod_mqtt: Support other socket modules - mod_push: Check for payload in encrypted messages # Version 19.02 * Admin - Fix in configure.ac the Erlang/OTP version: from 17.5 to 19.0 - reload_config command: Fix crash when sql_pool_size option is used - reload_config command: Fix crash when SQL is not configured - rooms_empty_destroy command: Several fixes to behave more conservative - Fix serverhost->host parameter name for muc_(un)register_nick API * Configuration - Allow specifying tag for listener for api_permission purposes - Change default ciphers to intermediate - Define default ciphers/protocol_option in example config - Don't crash on malformed 'modules' section - mod_mam: New option clear_archive_on_room_destroy to prevent archive removal on room destroy - mod_mam: New option access_preferences to restrict who can modify the MAM preferences - mod_muc: New option access_mam to restrict who can modify that room option - mod_offline: New option store_groupchat to allow storing group chat messages * Core - Add MQTT protocol support - Fix (un)setting of priority - Use OTP application startup infrastructure for starting dependencies - Improve starting order of several dependencies * MAM - mod_mam_mnesia/sql: Improve check for empty archive - disallow room creation if archive not empty and clear_archive_on_room_destroy is false - allow check if archive is empty for or user or room - Additional checks for database failures * MUC - Make sure that room_destroyed is called even when some code throws in terminate - Update muc room state after adding extra access field to it - MUC/Sub: Send mucsub subscriber notification events with from set to room jid * Shared Roster - Don't perform roster push for non-local contacts - Handle versioning result when shared roster group has remote account - Fix SQL queries * Miscelanea - CAPTCHA: Add no-store hint to CAPTCHA challenge stanzas - HTTP: Reject http_api request with malformed Authentication header - mod_carboncopy: Don't lose carbons on presence change or session resumption - mod_mix: Fix submission-id and channel resource - mod_ping: Fix ping IQ reply/timeout processing (17.x regression) - mod_private: Hardcode item ID for PEP bookmarks - mod_push: Improve notification error handling - PIEFXIS: Fix user export when password is scrammed - Prosody: Improve import of roster items, rooms and attributes - Translations: fixed "make translations" - WebAdmin: Fix support to restart module with new options # Version 18.12 * MAM data store compression * Proxy protocol support (http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) * MUC Self-Ping optimization (XEP-0410) * Bookmarks conversion (XEP-0411) ejabberd-21.12/CONTRIBUTORS.md0000644000232200023220000000137614154362354016120 0ustar debalancedebalance# Contributors We would like to thanks official ejabberd source code contributors: - Sergey Abramyan - Badlop - Ludovic Bocquet - Emilio Bustos - Thiago Camargo - Juan Pablo Carlino - Paweł Chmielowski - Gabriel Gatu - Tsukasa Hamano - Konstantinos Kallas - Evgeny Khramtsov - Ben Langfeld - Peter Lemenkov - Anna Mukharram - Johan Oudinet - Pablo Polvorin - Mickaël Rémond - Matthias Rieber - Rafael Roemhild - Christophe Romain - Jérôme Sautret - Sonny Scroggin - Alexey Shchepin - Shelley Shyan - Radoslaw Szymczyszyn - Stu Tomlinson - Christian Ulrich - Holger Weiß Please, if you think we are missing your contribution, do not hesitate to contact us at ProcessOne. In case you do not want to appear in this list, please, let us know as well. Thanks ! ejabberd-21.12/tools/0000755000232200023220000000000014154362354014772 5ustar debalancedebalanceejabberd-21.12/tools/xml_compress_gen.erl0000644000232200023220000003756714154362354021064 0ustar debalancedebalance%% File : xml_compress_gen.erl %% Author : Pawel Chmielowski %% Purpose : %% Created : 14 Sep 2018 Pawel Chmielowski %% %% %% ejabberd, Copyright (C) 2002-2021 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(xml_compress_gen). -author("pawel@process-one.net"). -include_lib("xmpp/include/xmpp.hrl"). %% API -export([archive_analyze/3, process_stats/1, gen_code/3]). -record(el_stats, {count = 0, empty_count = 0, only_text_count = 0, attrs = #{}, text_stats = #{}}). -record(attr_stats, {count = 0, vals = #{}}). archive_analyze(Host, Table, EHost) -> case ejabberd_sql:sql_query(Host, [<<"select username, peer, kind, xml from ", Table/binary>>]) of {selected, _, Res} -> lists:foldl( fun([U, P, K, X], Stats) -> M = case K of <<"groupchat">> -> U; _ -> <> end, El = fxml_stream:parse_element(X), analyze_element({El, <<"stream">>, <<"jabber:client">>, M, P}, Stats) end, {0, #{}}, Res); _ -> none end. encode_id(Num) when Num < 64 -> iolist_to_binary(io_lib:format("~p:8", [Num])). gen_code(_File, _Rules, $<) -> {error, <<"Invalid version">>}; gen_code(File, Rules, Ver) when Ver < 64 -> {Data, _} = lists:foldl( fun({Ns, El, Attrs, Text}, {Acc, Id}) -> NsC = case lists:keyfind(Ns, 1, Acc) of false -> []; {_, L} -> L end, {AttrsE, _} = lists:mapfoldl( fun({AName, AVals}, Id2) -> {AD, Id3} = lists:mapfoldl( fun(AVal, Id3) -> {{AVal, encode_id(Id3)}, Id3 + 1} end, Id2, AVals), {{AName, AD ++ [encode_id(Id3)]}, Id3 + 1} end, 3, Attrs), {TextE, Id5} = lists:mapfoldl( fun(TextV, Id4) -> {{TextV, encode_id(Id4)}, Id4 + 1} end, Id + 1, Text), {lists:keystore(Ns, 1, Acc, {Ns, NsC ++ [{El, encode_id(Id), AttrsE, TextE}]}), Id5} end, {[], 5}, Rules), {ok, Dev} = file:open(File, [write]), Mod = filename:basename(File, ".erl"), io:format(Dev, "-module(~s).~n-export([encode/3, decode/3]).~n~n", [Mod]), RulesS = iolist_to_binary(io_lib:format("~p", [Rules])), RulesS2 = binary:replace(RulesS, <<"\n">>, <<"\n% ">>, [global]), io:format(Dev, "% This file was generated by xml_compress_gen~n%~n" "% Rules used:~n%~n% ~s~n~n", [RulesS2]), VerId = iolist_to_binary(io_lib:format("~p:8", [Ver])), gen_encode(Dev, Data, VerId), gen_decode(Dev, Data, VerId), file:close(Dev), Data. gen_decode(Dev, Data, VerId) -> io:format(Dev, "decode(<<$<, _/binary>> = Data, _J1, _J2) ->~n" " fxml_stream:parse_element(Data);~n" "decode(<<~s, Rest/binary>>, J1, J2) ->~n" " {El, _} = decode(Rest, <<\"jabber:client\">>, J1, J2),~n" " El.~n~n", [VerId]), io:format(Dev, "decode_string(Data) ->~n" " case Data of~n" " <<0:2, L:6, Str:L/binary, Rest/binary>> ->~n" " {Str, Rest};~n" " <<1:2, L1:6, 0:2, L2:6, Rest/binary>> ->~n" " L = L2*64 + L1,~n" " <> = Rest,~n" " {Str, Rest2};~n" " <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> ->~n" " L = (L3*64 + L2)*64 + L1,~n" " <> = Rest,~n" " {Str, Rest2}~n" " end.~n~n", []), io:format(Dev, "decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2) ->~n" " {Text, Rest2} = decode_string(Rest),~n" " {{xmlcdata, Text}, Rest2};~n", []), io:format(Dev, "decode_child(<<2:8, Rest/binary>>, PNs, J1, J2) ->~n" " {Name, Rest2} = decode_string(Rest),~n" " {Attrs, Rest3} = decode_attrs(Rest2),~n" " {Children, Rest4} = decode_children(Rest3, PNs, J1, J2),~n" " {{xmlel, Name, Attrs, Children}, Rest4};~n", []), io:format(Dev, "decode_child(<<3:8, Rest/binary>>, PNs, J1, J2) ->~n" " {Ns, Rest2} = decode_string(Rest),~n" " {Name, Rest3} = decode_string(Rest2),~n" " {Attrs, Rest4} = decode_attrs(Rest3),~n" " {Children, Rest5} = decode_children(Rest4, Ns, J1, J2),~n" " {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5};~n", []), io:format(Dev, "decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2) ->~n" " {stop, Rest};~n", []), io:format(Dev, "decode_child(Other, PNs, J1, J2) ->~n" " decode(Other, PNs, J1, J2).~n~n", []), io:format(Dev, "decode_children(Data, PNs, J1, J2) ->~n" " prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2) end, Data).~n~n", []), io:format(Dev, "decode_attr(<<1:8, Rest/binary>>) ->~n" " {Name, Rest2} = decode_string(Rest),~n" " {Val, Rest3} = decode_string(Rest2),~n" " {{Name, Val}, Rest3};~n", []), io:format(Dev, "decode_attr(<<2:8, Rest/binary>>) ->~n" " {stop, Rest}.~n~n", []), io:format(Dev, "decode_attrs(Data) ->~n" " prefix_map(fun decode_attr/1, Data).~n~n", []), io:format(Dev, "prefix_map(F, Data) ->~n" " prefix_map(F, Data, []).~n~n", []), io:format(Dev, "prefix_map(F, Data, Acc) ->~n" " case F(Data) of~n" " {stop, Rest} ->~n" " {lists:reverse(Acc), Rest};~n" " {Val, Rest} ->~n" " prefix_map(F, Rest, [Val | Acc])~n" " end.~n~n", []), io:format(Dev, "add_ns(Ns, Ns, Attrs) ->~n" " Attrs;~n" "add_ns(_, Ns, Attrs) ->~n" " [{<<\"xmlns\">>, Ns} | Attrs].~n~n", []), lists:foreach( fun({Ns, Els}) -> lists:foreach( fun({Name, Id, Attrs, Text}) -> io:format(Dev, "decode(<<~s, Rest/binary>>, PNs, J1, J2) ->~n" " Ns = ~p,~n", [Id, Ns]), case Attrs of [] -> io:format(Dev, " {Attrs, Rest2} = decode_attrs(Rest),~n", []); _ -> io:format(Dev, " {Attrs, Rest2} = prefix_map(fun~n", []), lists:foreach( fun({AName, AVals}) -> lists:foreach( fun({j1, AId}) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {{~p, J1}, Rest3};~n", [AId, AName]); ({j2, AId}) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {{~p, J2}, Rest3};~n", [AId, AName]); ({{j1}, AId}) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {AVal, Rest4} = decode_string(Rest3),~n" " {{~p, <>}, Rest4};~n", [AId, AName]); ({{j2}, AId}) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {AVal, Rest4} = decode_string(Rest3),~n" " {{~p, <>}, Rest4};~n", [AId, AName]); ({AVal, AId}) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {{~p, ~p}, Rest3};~n", [AId, AName, AVal]); (AId) -> io:format(Dev, " (<<~s, Rest3/binary>>) ->~n" " {AVal, Rest4} = decode_string(Rest3),~n" " {{~p, AVal}, Rest4};~n", [AId, AName]) end, AVals) end, Attrs), io:format(Dev, " (<<2:8, Rest3/binary>>) ->~n" " {stop, Rest3};~n" " (Data) ->~n" " decode_attr(Data)~n" " end, Rest),~n", []) end, case Text of [] -> io:format(Dev, " {Children, Rest6} = decode_children(Rest2, Ns, J1, J2),~n", []); _ -> io:format(Dev, " {Children, Rest6} = prefix_map(fun", []), lists:foreach( fun({TextS, TId}) -> io:format(Dev, " (<<~s, Rest5/binary>>) ->~n" " {{xmlcdata, ~p}, Rest5};~n", [TId, TextS]) end, Text), io:format(Dev, " (Other) ->~n" " decode_child(Other, Ns, J1, J2)~n" " end, Rest2),~n", []) end, io:format(Dev, " {{xmlel, ~p, add_ns(PNs, Ns, Attrs), Children}, Rest6};~n", [Name]) end, Els) end, Data), io:format(Dev, "decode(Other, PNs, J1, J2) ->~n" " decode_child(Other, PNs, J1, J2).~n~n", []). gen_encode(Dev, Data, VerId) -> io:format(Dev, "encode(El, J1, J2) ->~n" " encode_child(El, <<\"jabber:client\">>,~n" " J1, J2, byte_size(J1), byte_size(J2), <<~s>>).~n~n", [VerId]), io:format(Dev, "encode_attr({<<\"xmlns\">>, _}, Acc) ->~n" " Acc;~n" "encode_attr({N, V}, Acc) ->~n" " <>.~n~n", []), io:format(Dev, "encode_attrs(Attrs, Acc) ->~n" " lists:foldl(fun encode_attr/2, Acc, Attrs).~n~n", []), io:format(Dev, "encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" " E1 = if~n" " PNs == Ns -> encode_attrs(Attrs, <>);~n" " true -> encode_attrs(Attrs, <>)~n" " end,~n" " E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>),~n" " <>.~n~n", []), io:format(Dev, "encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) ->~n" " case lists:keyfind(<<\"xmlns\">>, 1, Attrs) of~n" " false ->~n" " encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx);~n" " {_, Ns} ->~n" " encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx)~n" " end;~n" "encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) ->~n" " <>.~n~n", []), io:format(Dev, "encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) ->~n" " lists:foldl(~n" " fun(Child, Acc) ->~n" " encode_child(Child, PNs, J1, J2, J1L, J2L, Acc)~n" " end, Pfx, Children).~n~n", []), io:format(Dev, "encode_string(Data) ->~n" " <> = <<(byte_size(Data)):16/unsigned-big-integer>>,~n" " case {V1, V2, V3} of~n" " {0, 0, V3} ->~n" " <>;~n" " {0, V2, V3} ->~n" " <<(V3 bor 64):8, V2:8, Data/binary>>;~n" " _ ->~n" " <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>>~n" " end.~n~n", []), lists:foreach( fun({Ns, Els}) -> io:format(Dev, "encode(PNs, ~p = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" " case Name of~n", [Ns]), lists:foreach( fun({ElN, Id, Attrs, Text}) -> io:format(Dev, " ~p ->~n", [ElN]), case Attrs of [] -> io:format(Dev, " E = encode_attrs(Attrs, <>),~n", [Id]); _ -> io:format(Dev, " E = lists:foldl(fun~n", []), lists:foreach( fun({AName, AVals}) -> case AVals of [AIdS] when is_binary(AIdS) -> io:format(Dev, " ({~p, AVal}, Acc) ->~n" " <>;~n", [AName, AIdS]); _ -> io:format(Dev, " ({~p, AVal}, Acc) ->~n" " case AVal of~n", [AName]), lists:foreach( fun({j1, AId}) -> io:format(Dev, " J1 -> <>;~n", [AId]); ({j2, AId}) -> io:format(Dev, " J2 -> <>;~n", [AId]); ({{j1}, AId}) -> io:format(Dev, " <> -> " "<>;~n", [AId]); ({{j2}, AId}) -> io:format(Dev, " <> -> " "<>;~n", [AId]); ({AVal, AId}) -> io:format(Dev, " ~p -> <>;~n", [AVal, AId]); (AId) -> io:format(Dev, " _ -> <>~n", [AId]) end, AVals), io:format(Dev, " end;~n", []) end end, Attrs), io:format(Dev, " (Attr, Acc) -> encode_attr(Attr, Acc)~n", []), io:format(Dev, " end, <>, Attrs),~n", [Id]) end, case Text of [] -> io:format(Dev, " E2 = encode_children(Children, Ns, " "J1, J2, J1L, J2L, <>),~n", []); _ -> io:format(Dev, " E2 = lists:foldl(fun~n", []), lists:foreach( fun({TextV, TId}) -> io:format(Dev, " ({xmlcdata, ~p}, Acc) -> <>;~n", [TextV, TId]) end, Text), io:format(Dev, " (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc)~n", []), io:format(Dev, " end, <>, Children),~n", []) end, io:format(Dev, " <>;~n", []) end, Els), io:format(Dev, " _ -> encode_el(PNs, Ns, Name, Attrs, Children, " "J1, J2, J1L, J2L, Pfx)~nend;~n", []) end, Data), io:format(Dev, "encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) ->~n" " encode_el(PNs, Ns, Name, Attrs, Children, " "J1, J2, J1L, J2L, Pfx).~n~n", []). process_stats({_Counts, Stats}) -> SStats = lists:sort( fun({_, #el_stats{count = C1}}, {_, #el_stats{count = C2}}) -> C1 >= C2 end, maps:to_list(Stats)), lists:map( fun({Name, #el_stats{count = C, attrs = A, text_stats = T}}) -> [Ns, El] = binary:split(Name, <<"<">>), Attrs = lists:filtermap( fun({AN, #attr_stats{count = AC, vals = AV}}) -> if AC*5 < C -> false; true -> AVC = AC div min(maps:size(AV)*2, 10), AVA = [N || {N, C2} <- maps:to_list(AV), C2 > AVC], {true, {AN, AVA}} end end, maps:to_list(A)), Text = [TE || {TE, TC} <- maps:to_list(T), TC > C/2], {Ns, El, Attrs, Text} end, SStats). analyze_elements(Elements, Stats, PName, PNS, J1, J2) -> lists:foldl(fun analyze_element/2, Stats, lists:map(fun(V) -> {V, PName, PNS, J1, J2} end, Elements)). maps_update(Key, F, InitVal, Map) -> case maps:is_key(Key, Map) of true -> maps:update_with(Key, F, Map); _ -> maps:put(Key, F(InitVal), Map) end. analyze_element({{xmlcdata, Data}, PName, PNS, _J1, _J2}, {ElCount, Stats}) -> Stats2 = maps_update(<>, fun(#el_stats{text_stats = TS} = E) -> TS2 = maps_update(Data, fun(C) -> C + 1 end, 0, TS), E#el_stats{text_stats = TS2} end, #el_stats{}, Stats), {ElCount, Stats2}; analyze_element({#xmlel{name = Name, attrs = Attrs, children = Children}, _PName, PNS, J1, J2}, {ElCount, Stats}) -> XMLNS = case lists:keyfind(<<"xmlns">>, 1, Attrs) of {_, NS} -> NS; false -> PNS end, NStats = maps_update(<>, fun(#el_stats{count = C, empty_count = EC, only_text_count = TC, attrs = A} = ES) -> A2 = lists:foldl( fun({<<"xmlns">>, _}, AMap) -> AMap; ({AName, AVal}, AMap) -> J1S = size(J1), J2S = size(J2), AVal2 = case AVal of J1 -> j1; J2 -> j2; <> -> {j1}; <> -> {j2}; Other -> Other end, maps_update(AName, fun(#attr_stats{count = AC, vals = AV}) -> AV2 = maps_update(AVal2, fun(C2) -> C2 + 1 end, 0, AV), #attr_stats{count = AC + 1, vals = AV2} end, #attr_stats{}, AMap) end, A, Attrs), ES#el_stats{count = C + 1, empty_count = if Children == [] -> EC + 1; true -> EC end, only_text_count = case Children of [{xmlcdata, _}] -> TC + 1; _ -> TC end, attrs = A2} end, #el_stats{}, Stats), analyze_elements(Children, {ElCount + 1, NStats}, Name, XMLNS, J1, J2). ejabberd-21.12/tools/ejabberdctl.bc0000644000232200023220000000556214154362354017551 0ustar debalancedebalance# # bash completion for ejabberdctl # get_help() { local COMMANDCACHE=/var/log/ejabberd/bash_completion_$RANDOM ejabberdctl $CTLARGS help tags >$COMMANDCACHE.tags ejabberdctl $CTLARGS >$COMMANDCACHE if [[ $? == 2 ]] ; then ISRUNNING=1 runningcommands=`cat $COMMANDCACHE | grep "^ [a-z]" | awk '{print $1}' | xargs` runningtags=`cat $COMMANDCACHE.tags | grep "^ [a-z]" | awk '{print $1}' | xargs` fi rm $COMMANDCACHE rm $COMMANDCACHE.tags } _ejabberdctl() { local cur prev local ISRUNNING=0 local runningcommands COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" local startcoms="start foreground foreground-quiet live debug etop iexdebug iexlive ping started stopped" local startpars="--config-dir --config --ctl-config --logs --node --spool" local i=1 local CTLARGS="" while [ $i -lt $COMP_CWORD ] ; do local PARAM="${COMP_WORDS[i]}" i=$((i+1)) case $PARAM in --*) CTLARGS="--node ${COMP_WORDS[i]}" i=$((i+1)) ;; *) break ;; esac done case "${prev##*/}" in ejabberdctl) # This clause matches even when calling `/sbin/ejabberdctl` thanks to the ##*/ in the case get_help COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur)) return 0 ;; start|live) COMPREPLY=($(compgen -W "--node ${startpars}" -- $cur)) return 0 ;; debug) COMPREPLY=($(compgen -W "--node" -- $cur)) return 0 ;; help) get_help COMPREPLY=($(compgen -W "${runningcommands} ${runningtags}" -- $cur)) return 0 ;; --node) RUNNINGNODES=`epmd -names | grep name | awk '{print $2"@localhost"}' | xargs` COMPREPLY=($(compgen -W "$RUNNINGNODES" -- $cur)) return 0 ;; --config|--ctl-config) _filedir '?(u)cfg' return 0 ;; --config-dir|--logs|--spool) _filedir return 0 ;; *) prev2="${COMP_WORDS[COMP_CWORD-2]}" get_help if [[ "$prev2" == --* ]]; then COMPREPLY=($(compgen -W "--node ${startpars} ${startcoms} ${runningcommands}" -- $cur)) else if [[ $ISRUNNING == 1 ]]; then echo "" ejabberdctl $CTLARGS help ${PARAM} echo -n "${COMP_LINE}" fi fi return 0 ;; esac } complete -F _ejabberdctl ejabberdctl # Local variables: # mode: shell-script # sh-basic-offset: 4 # sh-indent-comment: t # indent-tabs-mode: nil # End: # ex: ts=4 sw=4 et filetype=sh ejabberd-21.12/tools/prepare-tr.sh0000755000232200023220000000604414154362354017416 0ustar debalancedebalance#!/bin/bash # Frontend for ejabberd's extract-tr.sh # How to create template files for a new language: # NEWLANG=zh # cp priv/msgs/ejabberd.pot priv/msgs/$NEWLANG.po # echo \{\"\",\"\"\}. > priv/msgs/$NEWLANG.msg # make translations extract_lang_src2pot () { ./tools/extract-tr.sh src $DEPS_DIR/xmpp/src > $PO_DIR/ejabberd.pot } extract_lang_popot2po () { LANG_CODE=$1 PO_PATH=$PO_DIR/$LANG_CODE.po POT_PATH=$PO_DIR/$PROJECT.pot msgmerge $PO_PATH $POT_PATH >$PO_PATH.translate 2>>$LOG mv $PO_PATH.translate $PO_PATH } extract_lang_po2msg () { LANG_CODE=$1 PO_PATH=$LANG_CODE.po MS_PATH=$PO_PATH.ms MSGID_PATH=$PO_PATH.msgid MSGSTR_PATH=$PO_PATH.msgstr MSGS_PATH=$LANG_CODE.msg cd $PO_DIR # Check PO has correct ~ # Let's convert to C format so we can use msgfmt PO_TEMP=$LANG_CODE.po.temp cat $PO_PATH | sed 's/%/perc/g' | sed 's/~/%/g' | sed 's/#:.*/#, c-format/g' >$PO_TEMP msgfmt $PO_TEMP --check-format result=$? rm $PO_TEMP if [ $result -ne 0 ] ; then exit 1 fi msgattrib $PO_PATH --translated --no-fuzzy --no-obsolete --no-location --no-wrap | grep "^msg" | tail --lines=+3 >$MS_PATH grep "^msgid" $PO_PATH.ms | sed 's/^msgid //g' >$MSGID_PATH grep "^msgstr" $PO_PATH.ms | sed 's/^msgstr //g' >$MSGSTR_PATH echo "%% Generated automatically" >$MSGS_PATH echo "%% DO NOT EDIT: run \`make translations\` instead" >>$MSGS_PATH echo "%% To improve translations please read:" >>$MSGS_PATH echo "%% https://docs.ejabberd.im/developer/extending-ejabberd/localization/" >>$MSGS_PATH echo "" >>$MSGS_PATH paste $MSGID_PATH $MSGSTR_PATH --delimiter=, | awk '{print "{" $0 "}."}' | sort -g >>$MSGS_PATH rm $MS_PATH rm $MSGID_PATH rm $MSGSTR_PATH mv $MSGS_PATH $MSGS_DIR } extract_lang_updateall () { echo "" echo "Generating POT..." extract_lang_src2pot cd $MSGS_DIR echo "" echo -e "File Missing (fuzzy) Language Last translator" echo -e "---- ------- ------- -------- ---------------" for i in $( ls *.msg ) ; do LANG_CODE=${i%.msg} echo -n $LANG_CODE | awk '{printf "%-6s", $1 }' PO=$PO_DIR/$LANG_CODE.po extract_lang_popot2po $LANG_CODE extract_lang_po2msg $LANG_CODE MISSING=`msgfmt --statistics $PO 2>&1 | awk '{printf "%5s", $4+$7 }'` echo -n " $MISSING" FUZZY=`msgfmt --statistics $PO 2>&1 | awk '{printf "%7s", $4 }'` echo -n " $FUZZY" LANGUAGE=`grep "X-Language:" $PO | sed 's/\"X-Language: //g' | sed 's/\\\\n\"//g' | awk '{printf "%-12s", $1}'` echo -n " $LANGUAGE" LASTAUTH=`grep "Last-Translator" $PO | sed 's/\"Last-Translator: //g' | sed 's/\\\\n\"//g'` echo " $LASTAUTH" done echo "" rm messages.mo grep -v " done" $LOG rm $LOG cd .. } EJA_DIR=`pwd` PROJECT=ejabberd DEPS_DIR=$1 MSGS_DIR=$EJA_DIR/priv/msgs LOG=/tmp/ejabberd-translate-errors.log PO_DIR=$EJA_DIR/$DEPS_DIR/ejabberd_po/src/ if [ ! -f $EJA_DIR/$DEPS_DIR/ejabberd_po/src/ejabberd.pot ]; then echo "Couldn't find the required ejabberd_po repository in" echo " $PO_DIR" echo "Run: ./configure --enable-tools; ./rebar get-deps" exit 1 fi echo "Using PO files from $PO_DIR." extract_lang_updateall ejabberd-21.12/tools/jhbtest.pl0000755000232200023220000002755214154362354017010 0ustar debalancedebalance#!/usr/bin/perl -w use strict; # har har use constant ERR => 0; use constant WARN => 1; use constant INFO => 2; use constant DEBUG => 3; use constant RID => 31974; ### ### ### conf ### ### ### my $BASE_ADDRESS = "http://localhost:5280/http-bind/"; my $JABBER_SERVER = "localhost"; my $RID = RID; my $WAIT = 60; my $USER = "tester"; my $UPW = "mysecret"; my $DEBUG = INFO; ### ### ### END conf ### ### ### # create an agent we can use for our requests use LWP::UserAgent; my $ua = new LWP::UserAgent(); # create a tree parser to parse response content use XML::Parser; my $p = new XML::Parser(Style => 'Tree'); ### ### ### subs ### ### ### sub doSend() { my $content = shift; # create a request my $req = new HTTP::Request(POST => $BASE_ADDRESS); $req->content_type('text/xml; charset=utf-8'); $req->content($content); debug(DEBUG,"<< Request\n".$req->as_string."<< END Request"); # send request my $res = $ua->request($req); debug(DEBUG,">> Response\n" . $res->as_string .">> END Response"); return $res; } # getChildEls # used to strip enclosing body element # PARAMS: @tree - tree style array from XML::Parser # RETURN: @children - child elements of top level element sub getChildEls { my $t = $_[0]; shift @{$t->[1]}; return @{$t->[1]}; } sub debug { my $lvl = shift; my $msg = shift; return if ($DEBUG < $lvl); my $prefix = "["; $prefix .= "ERROR" if ($lvl == ERR); $prefix .= "WARNING" if ($lvl == WARN); $prefix .= "INFO" if ($lvl == INFO); $prefix .= "DEBUG" if ($lvl == DEBUG); $prefix .= "] "; $msg =~ s/\n/\n$prefix/g; print STDERR $prefix . $msg . "\n"; } ### ### ### main ### ### ### $| = 1; # set streaming output # no body print "Sending some 'foo': "; my $res = &doSend("foo"); if ($res->code == 400) { print "OK.\n"; } else { print "Failed!\n"; print $res->as_string, "\n"; } # no body print "Sending some '': "; $res = &doSend(""); if ($res->code == 400) { print "OK.\n"; } else { print "Failed!\n"; print $res->as_string, "\n"; } # empty body print "Sending empty body: "; $res = &doSend(""); if ($res->code == 400) { print "OK.\n"; } else { print "Failed!\n"; print $res->as_string, "\n"; } # fake a sid print "Sending wrong sid: "; $res = &doSend(""); if ($res->code == 404) { print "OK.\n"; } else { print "Failed!\n"; print $res->as_string, "\n"; } # forget to send 'to' print "Missing 'to' attribute at session creation request: "; $res = &doSend(""); if ($res->is_success && $res->content =~ /content =~/as_string, "\n"; } # sending empty 'to' attribute print "Empty 'to' attribute at session creation request: "; $res = &doSend(""); if ($res->is_success && $res->content =~ /content =~/as_string, "\n"; } # forget to send a rid print "Missing 'rid' attribute at session creation request: "; $res = &doSend(""); if ($res->code == 404) { print "OK.\n"; } else { print "Failed!\n"; print $res->as_string, "\n"; } # trying to connect to non-existent domain print "Connecting to non-existent domain: "; $res = &doSend(""); if ($res->is_success && $res->content =~ /content =~/as_string, "\n"; } # trying to connect to non-existent jabber server print "Connecting to non-existent jabber server: "; $res = &doSend(""); if ($res->is_success && $res->content =~ /content =~/content =~/as_string, "\n"; } # connection to foreign server #print "Connecting to foreign jabber server: "; #$res = &doSend(""); #if ($res->is_success && $res->content =~ /content =~/as_string, "\n"; #} my %sess; sub getSess { $sess{rid} = RID; # a rid to start $res = &doSend(""); if ($res->is_success) { my $t = $p->parse($res->content); %sess = %{$t->[1]->[0]}; $sess{rid} = RID; # a rid to start if (defined($sess{sid}) && $sess{sid} ne "" && defined($sess{wait}) && $sess{wait} ne '' && $sess{wait} <= $WAIT) { debug(INFO,"sid: $sess{sid}"); debug(INFO, "authid: $sess{authid}") if (defined($sess{authid})); debug(INFO, "wait: $sess{wait}"); debug(INFO, "inactivity: $sess{inactivity}") if (defined($sess{inactivity})); debug(INFO, "polling: $sess{polling}") if (defined($sess{polling})); debug(INFO, "requests: $sess{requests}") if (defined($sess{requests})); debug(INFO, "accept: $sess{accept}") if (defined($sess{accept})); debug(INFO, "charsets: $sess{charsets}") if (defined($sess{charsets})); debug (WARN, "authid missing") unless (defined($sess{authid}) && $sess{authid} ne ''); debug (WARN, "server indicates polling mode") if (defined($sess{requests}) && $sess{requests} == 1); return 1; } } debug(ERR, "sid missing") unless (defined($sess{sid}) && $sess{sid} ne ""); debug(ERR, "wait missing") unless (defined($sess{wait}) && $sess{wait} ne ''); debug(ERR, "wait bigger then requested") unless (defined($sess{wait}) && $sess{wait} ne '' && $sess{wait} <= $WAIT); debug(DEBUG, $res->as_string); return 0; } # try to get a real sid print "Create a new session: "; if (&getSess()) { print "OK.\n"; } else { debug(ERR, "Aborting."); exit(1); } # checking wait attribute print "Creating another session with smaller 'wait' then before: "; $WAIT = $sess{wait} - 1; if (&getSess()) { print "OK.\n"; } else { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } sub doAuth { # query auth $sess{rid}++; $res = &doSend("$USER"); my @els = (&getChildEls($p->parse($res->content))); unless ($els[0] eq 'iq' && $els[1]->[0]->{'type'} eq 'result') { debug(ERR, $res->content); return 0; } # send auth $sess{rid}++; $res = &doSend("$USERtest$UPW"); @els = (&getChildEls($p->parse($res->content))); unless ($els[0] eq 'iq' && $els[1]->[0]->{'type'} eq 'result') { debug(ERR, $res->content); return 0; } return 1; } print "Authenticating: "; if (&doAuth()) { print "OK.\n"; } else { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } sub doPoll { $sess{rid}++; return &doSend(""); } print "Polling with wrong 'rid': "; $sess{rid}--; $res = &doPoll(); if ($res->code != 404) { print "FAILED!\n"; debug(ERR, "Aborting."); # exit(1); } print "OK.\n"; print "Checking if session terminated: "; $res = &doPoll(); if ($res->code != 404) { print "FAILED!\n"; debug(ERR, "Aborting."); # exit(1); } print "OK.\n"; print "Create a new session: "; if (&getSess()) { print "OK.\n"; } else { debug(ERR, "Aborting."); exit(1); } print "Authenticating: "; if (&doAuth()) { print "OK.\n"; } else { print "FAILED!\n"; debug(ERR, "Aborting."); # exit(1); } print "Polling too frequently: "; $res = &doPoll(); if ($res->code != 200) { print "First poll failed unexpectedly!\n"; debug(ERR, "Aborting."); #exit(1); } $res = &doPoll(); if ($res->code != 403) { print "FAILED!\n"; debug(ERR, "Aborting."); #exit(1); } print "OK.\n"; print "Checking if session terminated: "; $res = &doPoll(); if ($res->code != 404) { print "FAILED!\n"; debug(ERR, "Aborting."); #exit(1); } print "OK.\n"; print "Create a new session: "; if (&getSess()) { print "OK.\n"; } else { debug(ERR, "Aborting."); exit(1); } print "Authenticating: "; if (&doAuth()) { print "OK.\n"; } else { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } print "Test if proper polling is allowed: "; $res = &doPoll(); if ($res->code != 200) { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } sleep $sess{polling}; $res = &doPoll(); if ($res->code != 200) { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } print "OK.\n"; print "Waiting for session to timeout: "; my $STEP=10; for (my $i=0; $i<$sess{inactivity}; $i+=$STEP) { print "."; sleep $STEP; } sleep 1; # take another nap $res = &doPoll(); if ($res->code != 404) { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } print "OK.\n"; print "Create a new session: "; if (&getSess()) { print "OK.\n"; } else { debug(ERR, "Aborting."); exit(1); } print "Authenticating: "; if (&doAuth()) { print "OK.\n"; } else { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } # [TODO] # Check for # * KEY Sequence Algorithm Compliance # * Too Many Simultaneous Connections (probably hard to achieve for polling mode) # * request custom content-type/-encoding # get roster print "getting roster\n"; $sess{rid}++; $res = &doSend(""); debug(INFO, $res->content); # send presence print "sending presence\n"; $sess{rid}++; $res = &doSend(""); debug(INFO, $res->content); # sending bullshit print "sending bullshit\n"; $sess{rid}++; $res = &doSend("sending bullshit"); debug(INFO, $res->content); # send presence print "sending xa presence\n"; $sess{rid}++; $res = &doSend("xa"); debug(INFO, $res->content); # disconnect sleep 3; print "logout\n"; $sess{rid}++; $res = &doSend(""); debug(INFO, $res->content); print "Checking if session terminated: "; $res = &doPoll(); if ($res->code != 404) { print "FAILED!\n"; debug(ERR, "Aborting."); exit(1); } print "OK.\n"; ejabberd-21.12/tools/check_xep_versions.sh0000755000232200023220000000130314154362354021207 0ustar debalancedebalance#!/bin/bash check_xep() { xep=xep-$1 int=$(echo $1 | sed 's/^0*//') [ -f $BASE/doc/$xep ] || curl -s -o $BASE/doc/$xep https://xmpp.org/extensions/$xep.html title=$(sed '//!d;s/.*<title>\(.*\)<\/title>.*/\1/' $BASE/doc/$xep) vsn=$(grep -A1 Version $BASE/doc/$xep | sed '/<dd>/!d;q' | sed 's/.*>\(.*\)<.*/\1/') imp=$(grep "{xep, $int," $BASE/src/* | sed "s/.*src\/\(.*\).erl.*'\([0-9.-]*\)'.*/\1 \2/") [ "$imp" == "" ] && imp="NA 0.0" echo "$title;$vsn;${imp/ /;}" } [ $# -eq 1 ] && BASE="$1" || BASE="$PWD" [ -d $BASE/doc ] || mkdir $BASE/doc for x_num in $(grep "{xep" $BASE/src/* | sed "s/,//" | awk '{printf("%04d\n", $2)}' | sort -u) do check_xep $x_num done �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/tools/opt_types.sh�������������������������������������������������������������������0000755�0002322�0002322�00000041410�14154362354�017357� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env escript %% -*- erlang -*- -compile([nowarn_unused_function]). -record(state, {g_opts = #{} :: map(), m_opts = #{} :: map(), globals = [] :: [atom()], defaults = #{} :: map(), mod_defaults = #{} :: map(), specs = #{} :: map(), mod_specs = #{} :: map()}). main([Mod|Paths]) -> State = fold_beams( fun(File, Form, StateAcc) -> append(Form, File, StateAcc) end, #state{}, Paths), emit_modules(map_to_specs(State#state.m_opts, State#state.mod_defaults, State#state.mod_specs)), emit_config(Mod, map_to_specs(State#state.g_opts, State#state.defaults, State#state.specs), State#state.globals). emit_config(Mod, Specs, Globals) -> File = filename:join("src", Mod ++ ".erl"), case file:open(File, [write]) of {ok, Fd} -> emit_header(Fd, Mod, Specs, Globals), emit_funs(Fd, Mod, Specs, Globals); {error, Reason} -> err("Failed to open file ~s for writing: ~s", [File, file:format_error(Reason)]) end. emit_modules(Specs) -> M = lists:foldl( fun({{Mod, Opt}, Spec}, Acc) -> Opts = maps:get(Mod, Acc, []), Opts1 = [{Opt, Spec}|Opts], maps:put(Mod, Opts1, Acc) end, #{}, Specs), maps:fold( fun(Mod, OptSpecs, _) -> ModS = atom_to_list(Mod) ++ "_opt", File = filename:join("src", ModS ++ ".erl"), case file:open(File, [write]) of {ok, Fd} -> OptSpecs1 = lists:reverse(OptSpecs), emit_header(Fd, ModS, OptSpecs1), emit_funs(Fd, Mod, OptSpecs1); {error, Reason} -> err("Failed to open file ~s for writing: ~s", [File, file:format_error(Reason)]) end end, ok, M). emit_header(Fd, Mod, Specs, Globals) -> log(Fd, comment(), []), log(Fd, "-module(~s).~n", [Mod]), lists:foreach( fun({{_, Opt}, _}) -> case lists:member(Opt, Globals) of true -> log(Fd, "-export([~s/0]).", [Opt]); false -> log(Fd, "-export([~s/0, ~s/1]).", [Opt, Opt]) end end, Specs), log(Fd, "", []). emit_header(Fd, Mod, Specs) -> log(Fd, comment(), []), log(Fd, "-module(~s).~n", [Mod]), lists:foreach( fun({Opt, _}) -> log(Fd, "-export([~s/1]).", [Opt]) end, Specs), log(Fd, "", []). emit_funs(Fd, _Mod, Specs, Globals) -> lists:foreach( fun({{_, Opt}, Type}) -> SType = t_to_string(Type), case lists:member(Opt, Globals) of true -> log(Fd, "-spec ~s() -> ~s.~n" "~s() ->~n" " ejabberd_config:get_option({~s, global}).~n", [Opt, SType, Opt, Opt]); false -> log(Fd, "-spec ~s() -> ~s.~n" "~s() ->~n" " ~s(global).~n" "-spec ~s(global | binary()) -> ~s.~n" "~s(Host) ->~n" " ejabberd_config:get_option({~s, Host}).~n", [Opt, SType, Opt, Opt, Opt, SType, Opt, Opt]) end end, Specs). emit_funs(Fd, Mod, Specs) -> lists:foreach( fun({Opt, Type}) -> Mod2 = strip_db_type(Mod), log(Fd, "-spec ~s(gen_mod:opts() | global | binary()) -> ~s.~n" "~s(Opts) when is_map(Opts) ->~n" " gen_mod:get_opt(~s, Opts);~n" "~s(Host) ->~n" " gen_mod:get_module_opt(Host, ~s, ~s).~n", [Opt, t_to_string(Type), Opt, Opt, Opt, Mod2, Opt]) end, Specs). strip_db_type(mod_vcard_ldap) -> mod_vcard; strip_db_type(mod_vcard_mnesia) -> mod_vcard; strip_db_type(Mod) -> Mod. append({globals, Form}, _File, State) -> [Clause] = erl_syntax:function_clauses(Form), Body = lists:last(erl_syntax:clause_body(Clause)), Gs = lists:map(fun erl_syntax:atom_value/1, erl_syntax:list_elements(Body)), Globals = State#state.globals ++ Gs, State#state{globals = Globals}; append({Index, Form}, File, State) when Index == #state.defaults; Index == #state.mod_defaults -> Mod = module(File), [Clause] = erl_syntax:function_clauses(Form), Body = lists:last(erl_syntax:clause_body(Clause)), case erl_syntax:is_proper_list(Body) of true -> Opts = lists:foldl( fun(E, M) -> try [E1, E2|_] = erl_syntax:tuple_elements(E), Name = erl_syntax:atom_value(E1), Val = erl_syntax:concrete(E2), maps:put({Mod, Name}, Val, M) catch _:_ -> M end end, element(Index, State), erl_syntax:list_elements(Body)), setelement(Index, State, Opts); false -> warn("~s: improper list", [format_file(File, Body)]), State end; append({Index, Form}, File, State) when Index == #state.specs; Index == #state.mod_specs -> Specs = element(Index, State), Mod = module(File), try {type, _, 'fun', Form1} = Form, {type, _, list, Form2} = lists:last(Form1), Tuples = case Form2 of [{type, _, union, Form3}] -> Form3; _ -> Form2 end, Specs1 = lists:foldl( fun({type, _, tuple, [{atom, _, Atom}, Form5]}, Acc) -> maps:put({Mod, Atom}, Form5, Acc); (_, Acc) -> Acc end, Specs, Tuples), setelement(Index, State, Specs1) catch _:_ -> warn("~s: unsupported type spec", [format_file(File, Form)]), State end; append({Type, Form}, File, State) when Type == opt_type; Type == mod_opt_type -> Clauses = erl_syntax:function_clauses(Form), Mod = module(File), lists:foldl( fun(Clause, StateAcc) -> [Arg] = erl_syntax:clause_patterns(Clause), Body = lists:last(erl_syntax:clause_body(Clause)), case erl_syntax:type(Arg) of atom -> Name = erl_syntax:atom_value(Arg), case Type of opt_type -> GOpts = StateAcc#state.g_opts, State#state{ g_opts = append_body({Mod, Name}, Body, GOpts)}; mod_opt_type -> MOpts = StateAcc#state.m_opts, State#state{ m_opts = append_body({Mod, Name}, Body, MOpts)} end; T -> warn("~s: unexpected option name: ~s", [format_file(File, Arg), T]), StateAcc end end, State, Clauses). append_body(Name, Body, Map) -> maps:put(Name, Body, Map). map_to_specs(Map, Defaults, Specs) -> lists:keysort( 1, maps:fold( fun({Mod, Opt} = Key, Val, Acc) -> S1 = type_with_default(Key, Val, Defaults), S2 = case t_is_any(S1) of true -> try maps:get(Key, Specs) catch _:{badkey, _} -> warn("Cannot derive type for ~s->~s", [Mod, Opt]), S1 end; false -> S1 end, [{Key, S2}|Acc] end, [], Map)). type_with_default({Mod, _} = Key, Val, Defaults) -> S = try spec(Mod, Val) catch throw:unknown -> erl_types:t_any() end, case t_is_any(S) of true -> S; false -> try maps:get(Key, Defaults) of T -> erl_types:t_sup( [S, erl_types:t_from_term(T)]) catch _:{badkey, _} -> S end end. spec(Mod, Form) -> case erl_syntax:type(Form) of application -> case erl_syntax_lib:analyze_application(Form) of {M, {Fun, Arity}} when M == econf; M == yconf -> Args = erl_syntax:application_arguments(Form), spec(Fun, Arity, Args, Mod); _ -> t_unknown(Mod) end; _ -> t_unknown(Mod) end. spec(pos_int, 0, _, _) -> erl_types:t_pos_integer(); spec(pos_int, 1, [Inf], _) -> erl_types:t_sup( erl_types:t_pos_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(non_neg_int, 0, _, _) -> erl_types:t_non_neg_integer(); spec(non_neg_int, 1, [Inf], _) -> erl_types:t_sup( erl_types:t_non_neg_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(int, 0, _, _) -> erl_types:t_integer(); spec(int, 2, [Min, Max], _) -> erl_types:t_from_range( erl_syntax:integer_value(Min), erl_syntax:integer_value(Max)); spec(number, 1, _, _) -> erl_types:t_number(); spec(octal, 0, _, _) -> erl_types:t_non_neg_integer(); spec(binary, A, _, _) when A == 0; A == 1; A == 2 -> erl_types:t_binary(); spec(enum, 1, [L], _) -> try Els = erl_syntax:list_elements(L), Atoms = lists:map( fun(A) -> erl_types:t_atom( erl_syntax:atom_value(A)) end, Els), erl_types:t_sup(Atoms) catch _:_ -> erl_types:t_binary() end; spec(bool, 0, _, _) -> erl_types:t_boolean(); spec(atom, 0, _, _) -> erl_types:t_atom(); spec(string, A, _, _) when A == 0; A == 1; A == 2 -> erl_types:t_string(); spec(any, 0, _, Mod) -> t_unknown(Mod); spec(url, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(file, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(directory, A, _, _) when A == 0; A == 1 -> erl_types:t_binary(); spec(ip, 0, _, _) -> t_remote(inet, ip_address); spec(ipv4, 0, _, _) -> t_remote(inet, ip4_address); spec(ipv6, 0, _, _) -> t_remote(inet, ip6_address); spec(ip_mask, 0, _, _) -> erl_types:t_sup( erl_types:t_tuple( [t_remote(inet, ip4_address), erl_types:t_from_range(0, 32)]), erl_types:t_tuple( [t_remote(inet, ip6_address), erl_types:t_from_range(0, 128)])); spec(port, 0, _, _) -> erl_types:t_from_range(1, 65535); spec(re, A, _, _) when A == 0; A == 1 -> t_remote(re, mp); spec(glob, A, _, _) when A == 0; A == 1 -> t_remote(re, mp); spec(path, 0, _, _) -> erl_types:t_binary(); spec(binary_sep, 1, _, _) -> erl_types:t_list(erl_types:t_binary()); spec(beam, A, _, _) when A == 0; A == 1 -> erl_types:t_module(); spec(timeout, 1, _, _) -> erl_types:t_pos_integer(); spec(timeout, 2, [_, Inf], _) -> erl_types:t_sup( erl_types:t_pos_integer(), erl_types:t_atom(erl_syntax:atom_value(Inf))); spec(non_empty, 1, [Form], Mod) -> S = spec(Mod, Form), case erl_types:t_is_list(S) of true -> erl_types:t_nonempty_list( erl_types:t_list_elements(S)); false -> S end; spec(unique, 1, [Form], Mod) -> spec(Mod, Form); spec(acl, 0, _, _) -> t_remote(acl, acl); spec(shaper, 0, _, _) -> erl_types:t_sup( [erl_types:t_atom(), erl_types:t_list(t_remote(ejabberd_shaper, shaper_rule))]); spec(url_or_file, 0, _, _) -> erl_types:t_tuple( [erl_types:t_sup([erl_types:t_atom(file), erl_types:t_atom(url)]), erl_types:t_binary()]); spec(lang, 0, _, _) -> erl_types:t_binary(); spec(pem, 0, _, _) -> erl_types:t_binary(); spec(jid, 0, _, _) -> t_remote(jid, jid); spec(domain, 0, _, _) -> erl_types:t_binary(); spec(db_type, 1, _, _) -> erl_types:t_atom(); spec(queue_type, 0, _, _) -> erl_types:t_sup([erl_types:t_atom(ram), erl_types:t_atom(file)]); spec(ldap_filter, 0, _, _) -> erl_types:t_binary(); spec(sip_uri, 0, _, _) -> t_remote(esip, uri); spec(Fun, A, [Form|_], Mod) when (A == 1 orelse A == 2) andalso (Fun == list orelse Fun == list_or_single) -> erl_types:t_list(spec(Mod, Form)); spec(map, A, [F1, F2|OForm], Mod) when A == 2; A == 3 -> T1 = spec(Mod, F1), T2 = spec(Mod, F2), case options_return_type(OForm) of map -> erl_types:t_map([], T1, T2); dict -> t_remote(dict, dict); _ -> erl_types:t_list(erl_types:t_tuple([T1, T2])) end; spec(either, 2, [F1, F2], Mod) -> Spec1 = case erl_syntax:type(F1) of atom -> erl_types:t_atom(erl_syntax:atom_value(F1)); _ -> spec(Mod, F1) end, Spec2 = spec(Mod, F2), erl_types:t_sup([Spec1, Spec2]); spec(and_then, 2, [_, F], Mod) -> spec(Mod, F); spec(host, 0, _, _) -> erl_types:t_binary(); spec(hosts, 0, _, _) -> erl_types:t_list(erl_types:t_binary()); spec(vcard_temp, 0, _, _) -> erl_types:t_sup([erl_types:t_atom(undefined), erl_types:t_tuple()]); spec(options, A, [Form|OForm], Mod) when A == 1; A == 2 -> case erl_syntax:type(Form) of map_expr -> Fs = erl_syntax:map_expr_fields(Form), Required = options_required(OForm), {Els, {DefK, DefV}} = lists:mapfoldl( fun(F, Acc) -> Name = erl_syntax:map_field_assoc_name(F), Val = erl_syntax:map_field_assoc_value(F), OptType = spec(Mod, Val), case erl_syntax:atom_value(Name) of '_' -> {[], {erl_types:t_atom(), OptType}}; Atom -> Mand = case lists:member(Atom, Required) of true -> mandatory; false -> optional end, {[{erl_types:t_atom(Atom), Mand, OptType}], Acc} end end, {erl_types:t_none(), erl_types:t_none()}, Fs), case options_return_type(OForm) of map -> erl_types:t_map(lists:keysort(1, lists:flatten(Els)), DefK, DefV); dict -> t_remote(dict, dict); _ -> erl_types:t_list( erl_types:t_sup( [erl_types:t_tuple([DefK, DefV])| lists:map( fun({K, _, V}) -> erl_types:t_tuple([K, V]) end, lists:flatten(Els))])) end; _ -> t_unknown(Mod) end; spec(_, _, _, Mod) -> t_unknown(Mod). t_from_form(Spec) -> {T, _} = erl_types:t_from_form( Spec, sets:new(), {type, {mod, foo, 1}}, dict:new(), erl_types:var_table__new(), erl_types:cache__new()), T. t_remote(Mod, Type) -> D = maps:from_list([{{opaque, Type, []}, {{Mod, 1, 2, []}, type}}]), [T] = erl_types:t_opaque_from_records(D), T. t_unknown(_Mod) -> throw(unknown). t_is_any(T) -> T == erl_types:t_any(). t_to_string(T) -> case erl_types:is_erl_type(T) of true -> erl_types:t_to_string(T); false -> erl_types:t_form_to_string(T) end. options_return_type([]) -> list; options_return_type([Form]) -> Opts = erl_syntax:concrete(Form), proplists:get_value(return, Opts, list). options_required([]) -> []; options_required([Form]) -> Opts = erl_syntax:concrete(Form), proplists:get_value(required, Opts, []). format_file(Path, Form) -> filename:rootname(filename:basename(Path)) ++ ".erl:" ++ integer_to_list(erl_syntax:get_pos(Form)). module(Path) -> list_to_atom(filename:rootname(filename:basename(Path))). fold_beams(Fun, State, Paths) -> Paths1 = fold_paths(Paths), Total = length(Paths1), {_, State1} = lists:foldl( fun(File, {I, Acc}) -> io:format("Progress: ~B% (~B/~B)\r", [round(I*100/Total), I, Total]), case is_elixir_beam(File) of true -> {I+1, Acc}; false -> AbsCode = get_code_from_beam(File), Acc2 = case is_behaviour(AbsCode, ejabberd_config) of true -> fold_opt(File, Fun, Acc, AbsCode); false -> fold_mod_opt(File, Fun, Acc, AbsCode) end, {I+1, Acc2} end end, {0, State}, Paths1), State1. fold_opt(File, Fun, Acc, AbsCode) -> lists:foldl( fun(Form, Acc1) -> case erl_syntax_lib:analyze_form(Form) of {function, {opt_type, 1}} -> Fun(File, {opt_type, Form}, Acc1); {function, {globals, 0}} -> Fun(File, {globals, Form}, Acc1); {function, {options, 0}} -> Fun(File, {#state.defaults, Form}, Acc1); {attribute, {spec, {spec, {{options, 0}, Spec}}}} -> Fun(File, {#state.specs, hd(Spec)}, Acc1); _ -> Acc1 end end, Acc, AbsCode). fold_mod_opt(File, Fun, Acc, AbsCode) -> lists:foldl( fun(Form, Acc1) -> case erl_syntax_lib:analyze_form(Form) of {function, {mod_opt_type, 1}} -> Fun(File, {mod_opt_type, Form}, Acc1); {function, {mod_options, 1}} -> Fun(File, {#state.mod_defaults, Form}, Acc1); {attribute, {spec, {spec, {{mod_options, 1}, Spec}}}} -> Fun(File, {#state.mod_specs, hd(Spec)}, Acc1); _ -> Acc1 end end, Acc, AbsCode). fold_paths(Paths) -> lists:flatmap( fun(Path) -> case filelib:is_dir(Path) of true -> Beams = lists:reverse( filelib:fold_files( Path, ".+\.beam\$", false, fun(File, Acc) -> [File|Acc] end, [])), case Beams of [] -> ok; _ -> code:add_path(Path) end, Beams; false -> [Path] end end, Paths). is_behaviour(AbsCode, Mod) -> lists:any( fun(Form) -> case erl_syntax_lib:analyze_form(Form) of {attribute, {Attr, {_, Mod}}} when Attr == behaviour orelse Attr == behavior -> true; _ -> false end end, AbsCode). is_elixir_beam(File) -> case filename:basename(File) of "Elixir" ++ _ -> true; _ -> false end. get_code_from_beam(File) -> try {ok, {_, List}} = beam_lib:chunks(File, [abstract_code]), {_, {raw_abstract_v1, Forms}} = lists:keyfind(abstract_code, 1, List), Forms catch _:{badmatch, _} -> err("no abstract code found in ~s", [File]) end. comment() -> "%% Generated automatically~n" "%% DO NOT EDIT: run `make options` instead~n". log(Format, Args) -> log(standard_io, Format, Args). log(Fd, Format, Args) -> case io:format(Fd, Format ++ "~n", Args) of ok -> ok; {error, Reason} -> err("Failed to write to file: ~s", [file:format_error(Reason)]) end. warn(Format, Args) -> io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). err(Format, Args) -> io:format(standard_error, "Error: " ++ Format ++ "~n", Args), halt(1). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/tools/captcha-ng.sh������������������������������������������������������������������0000755�0002322�0002322�00000006140�14154362354�017337� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash # Copyright © 2021 Adrien Bourmault (neox@os-k.eu) # 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 script is an example captcha script. # It takes the text to recognize in the captcha image as a parameter. # It return the image binary as a result. ejabberd support PNG, JPEG and GIF. # The whole idea of the captcha script is to let server admins adapt it to # their own needs. The goal is to be able to make the captcha generation as # unique as possible, to make the captcha challenge difficult to bypass by # a bot. # Server admins are thus supposed to write and use their own captcha generators. # This script relies on ImageMagick. # It is NOT compliant with ImageMagick forks like GraphicsMagick. INPUT=$1 TRANSFORMATIONS=(INTRUDER SUM) DIGIT=(zero one two three four five six seven eight nine ten) if test -n "${BASH_VERSION:-''}" ; then get_random () { R=$RANDOM } else for n in $(od -A n -t u2 -N 64 /dev/urandom); do RL="$RL$n "; done get_random () { R=${RL%% *} RL=${RL#* } } fi INTRUDER() { NUMBERS=$(echo "$INPUT" | grep -o . | tr '\n' ' ') SORTED_UNIQ_NUM=$(echo "${NUMBERS[@]}" | sort -u | tr '\n' ' ') SORT_RANDOM_CMD="$( ( echo x|sort -R >&/dev/null && echo "sort -R" ) || ( echo x|shuf >&/dev/null && echo shuf ) || echo cat)" RANDOM_DIGITS=$(echo 123456789 | grep -o . | eval "$SORT_RANDOM_CMD" | tr '\n' ' ') INTRUDER=-1 for i in $RANDOM_DIGITS do if [[ ! " ${SORTED_UNIQ_NUM[@]} " =~ ${i} ]]; then INTRUDER=$i break fi done # Worst case if [[ $INTRUDER -eq "-1" ]] then printf "Type %s \n without changes" "$INPUT" return fi for num in ${NUMBERS} do get_random R=$((R % 100)) if [[ $R -lt 60 ]]; then NEWINPUT=${NEWINPUT}${num}${INTRUDER} else NEWINPUT=${NEWINPUT}${num} fi done get_random R=$((R % 100)) if [[ $R -lt 50 ]]; then printf "Type %s by\n deleting the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}" else printf "Enter %s by\n removing the %s" "$NEWINPUT" "${DIGIT[$INTRUDER]}" fi } SUM() { get_random RA=$((R % 100)) if [[ $((INPUT % 2)) -eq 0 ]]; then A=$((INPUT - RA)) B=$RA else B=$((INPUT - RA)) A=$RA fi get_random R=$((R % 100)) if [[ $R -lt 25 ]]; then printf "Type the result\n of %s + %s" "$A" "$B" elif [[ $R -lt 50 ]]; then printf "SUMx\n %s and %s" "$A" "$B" elif [[ $R -lt 75 ]]; then printf "Add\n %s and %s" "$A" "$B" else printf "Enter the result\n of %s + %s" "$A" "$B" fi } get_random RAND_ITALIC=$((R % 25)) get_random RAND_ANGLE=$((R % 3)) get_random RAND_INDEX=$((R % ${#TRANSFORMATIONS[@]})) convert -size 300x60 xc:none -pointsize 20 \ \( -clone 0 -fill black \ -stroke black -strokewidth 1 \ -annotate "${RAND_ANGLE}x${RAND_ITALIC}+0+0" "\n $(${TRANSFORMATIONS[$RAND_INDEX]})" \ -roll +"$ROLL_X"+0 \ -wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \ -roll -"$ROLL_X"+0 \) \ -flatten -crop 300x60 +repage -quality 500 -depth 11 png:- ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/tools/hook_deps.sh�������������������������������������������������������������������0000755�0002322�0002322�00000032075�14154362354�017313� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env escript %% -*- erlang -*- -record(state, {run_hooks = #{}, run_fold_hooks = #{}, hooked_funs = {#{}, #{}}, iq_handlers = {#{}, #{}}, exports = #{}, module :: module(), file :: filename:filename()}). main(Paths) -> State = fold_beams( fun(File0, Tree, X, Acc0) -> BareName = filename:rootname(filename:basename(File0)), Mod = list_to_atom(BareName), File = BareName ++ ".erl", Exports = maps:put(Mod, X, Acc0#state.exports), Acc1 = Acc0#state{file = File, module = Mod, exports = Exports}, erl_syntax_lib:fold( fun(Form, Acc) -> case erl_syntax:type(Form) of application -> case erl_syntax_lib:analyze_application(Form) of {ejabberd_hooks, {run, N}} when N == 2; N == 3 -> collect_run_hook(Form, Acc); {ejabberd_hooks, {run_fold, N}} when N == 3; N == 4 -> collect_run_fold_hook(Form, Acc); {ejabberd_hooks, {add, N}} when N == 4; N == 5 -> collect_run_fun(Form, add, Acc); {ejabberd_hooks, {delete, N}} when N == 4; N == 5 -> collect_run_fun(Form, delete, Acc); {gen_iq_handler, {add_iq_handler, 5}} -> collect_iq_handler(Form, add, Acc); {gen_iq_handler, {remove_iq_handler, 3}} -> collect_iq_handler(Form, delete, Acc); _ -> Acc end; _ -> Acc end end, Acc1, Tree) end, #state{}, Paths), check_hooks_arity(State#state.run_hooks), check_hooks_arity(State#state.run_fold_hooks), check_iq_handlers_export(State#state.iq_handlers, State#state.exports), analyze_iq_handlers(State#state.iq_handlers), analyze_hooks(State#state.hooked_funs), RunDeps = build_deps(State#state.run_hooks, State#state.hooked_funs), RunFoldDeps = build_deps(State#state.run_fold_hooks, State#state.hooked_funs), emit_module(RunDeps, RunFoldDeps, hooks_type_test). collect_run_hook(Form, State) -> [Hook|Tail] = erl_syntax:application_arguments(Form), case atom_value(Hook, State) of undefined -> State; HookName -> Args = case Tail of [_Host, Args0] -> Args0; [Args0] -> Args0 end, Arity = erl_syntax:list_length(Args), Hooks = maps:put({HookName, Arity}, {State#state.file, erl_syntax:get_pos(Hook)}, State#state.run_hooks), State#state{run_hooks = Hooks} end. collect_run_fold_hook(Form, State) -> [Hook|Tail] = erl_syntax:application_arguments(Form), case atom_value(Hook, State) of undefined -> State; HookName -> Args = case Tail of [_Host, _Val, Args0] -> Args0; [_Val, Args0] -> Args0 end, Arity = erl_syntax:list_length(Args) + 1, Hooks = maps:put({HookName, Arity}, {State#state.file, erl_syntax:get_pos(Form)}, State#state.run_fold_hooks), State#state{run_fold_hooks = Hooks} end. collect_run_fun(Form, Action, State) -> [Hook|Tail] = erl_syntax:application_arguments(Form), case atom_value(Hook, State) of undefined -> State; HookName -> {Module, Fun, Seq} = case Tail of [_Host, M, F, S] -> {M, F, S}; [M, F, S] -> {M, F, S} end, ModName = module_name(Module, State), FunName = atom_value(Fun, State), SeqInt = integer_value(Seq, State), if ModName /= undefined, FunName /= undefined, SeqInt /= undefined -> Pos = case Action of add -> 1; delete -> 2 end, Funs = maps_append( HookName, {ModName, FunName, SeqInt, {State#state.file, erl_syntax:get_pos(Form)}}, element(Pos, State#state.hooked_funs)), Hooked = setelement(Pos, State#state.hooked_funs, Funs), State#state{hooked_funs = Hooked}; true -> State end end. collect_iq_handler(Form, add, #state{iq_handlers = {Add, Del}} = State) -> [Component, _Host, Namespace, Module, Function] = erl_syntax:application_arguments(Form), Mod = module_name(Module, State), Fun = atom_value(Function, State), Comp = atom_value(Component, State), NS = binary_value(Namespace, State), if Mod /= undefined, Fun /= undefined, Comp /= undefined, NS /= undefined -> Handlers = maps_append( {Comp, NS}, {Mod, Fun, {State#state.file, erl_syntax:get_pos(Form)}}, Add), State#state{iq_handlers = {Handlers, Del}}; true -> State end; collect_iq_handler(Form, delete, #state{iq_handlers = {Add, Del}} = State) -> [Component, _Host, Namespace] = erl_syntax:application_arguments(Form), Comp = atom_value(Component, State), NS = binary_value(Namespace, State), if Comp /= undefined, NS /= undefined -> Handlers = maps_append( {Comp, NS}, {State#state.file, erl_syntax:get_pos(Form)}, Del), State#state{iq_handlers = {Add, Handlers}}; true -> State end. check_hooks_arity(Hooks) -> maps:fold( fun({Hook, Arity}, _, M) -> case maps:is_key(Hook, M) of true -> err("Error: hook ~s is called with different " "number of arguments~n", [Hook]); false -> maps:put(Hook, Arity, M) end end, #{}, Hooks). check_iq_handlers_export({HookedFuns, _}, Exports) -> maps:map( fun(_, Funs) -> lists:foreach( fun({Mod, Fun, {File, FileNo}}) -> case is_exported(Mod, Fun, 1, Exports) of true -> ok; false -> err("~s:~B: Error: " "iq handler is registered on unexported function: " "~s:~s/1~n", [File, FileNo, Mod, Fun]) end end, Funs) end, HookedFuns). analyze_iq_handlers({Add, Del}) -> maps:map( fun(Handler, Funs) -> lists:foreach( fun({_, _, {File, FileNo}}) -> case maps:is_key(Handler, Del) of true -> ok; false -> err("~s:~B: Error: " "iq handler is added but not removed~n", [File, FileNo]) end end, Funs) end, Add), maps:map( fun(Handler, Meta) -> lists:foreach( fun({File, FileNo}) -> case maps:is_key(Handler, Add) of true -> ok; false -> err("~s:~B: Error: " "iq handler is removed but not added~n", [File, FileNo]) end end, Meta) end, Del). analyze_hooks({Add, Del}) -> Del1 = maps:fold( fun(Hook, Funs, D) -> lists:foldl( fun({Mod, Fun, Seq, {File, FileNo}}, D1) -> maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1) end, D, Funs) end, #{}, Del), Add1 = maps:fold( fun(Hook, Funs, D) -> lists:foldl( fun({Mod, Fun, Seq, {File, FileNo}}, D1) -> maps:put({Hook, Mod, Fun, Seq}, {File, FileNo}, D1) end, D, Funs) end, #{}, Add), lists:foreach( fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) -> case maps:is_key(Key, Del1) of true -> ok; false -> err("~s:~B: Error: " "hook ~s->~s->~s is added but was never removed~n", [File, FileNo, Hook, Mod, Fun]) end end, maps:to_list(Add1)), lists:foreach( fun({{Hook, Mod, Fun, _} = Key, {File, FileNo}}) -> case maps:is_key(Key, Add1) of true -> ok; false -> err("~s:~B: Error: " "hook ~s->~s->~s is removed but was never added~n", [File, FileNo, Hook, Mod, Fun]) end end, maps:to_list(Del1)). build_deps(Hooks, {HookedFuns, _}) -> maps:fold( fun({Hook, Arity}, Meta, Deps) -> case maps:find(Hook, HookedFuns) of {ok, Funs} -> ExportedFuns = lists:map( fun({M, F, Seq, FunMeta}) -> {{M, F, Arity}, Seq, FunMeta} end, Funs), maps_append_list({Hook, Arity, Meta}, ExportedFuns, Deps); error -> maps_append_list({Hook, Arity, Meta}, [], Deps) end end, #{}, Hooks). module_name(Form, State) -> try Name = erl_syntax:macro_name(Form), 'MODULE' = erl_syntax:variable_name(Name), State#state.module catch _:_ -> atom_value(Form, State) end. atom_value(Form, State) -> case erl_syntax:type(Form) of atom -> erl_syntax:atom_value(Form); _ -> warn_type(Form, State, "not an atom"), undefined end. integer_value(Form, State) -> case erl_syntax:type(Form) of integer -> erl_syntax:integer_value(Form); _ -> warn_type(Form, State, "not an integer"), undefined end. binary_value(Form, State) -> try erl_syntax:concrete(Form) of Binary when is_binary(Binary) -> Binary; _ -> warn_type(Form, State, "not a binary"), undefined catch _:_ -> warn_type(Form, State, "not a binary"), undefined end. is_exported(Mod, Fun, Arity, Exports) -> try maps:get(Mod, Exports) of L -> lists:member({Fun, Arity}, L) catch _:{badkey, _} -> false end. warn_type({var, _, 'Type'}, #state{module = mod_delegation}, "not an atom") -> ok; warn_type({var, _, 'NS'}, #state{module = mod_delegation}, "not a binary") -> ok; warn_type(Form, State, Warning) -> log("~s:~p: Warning: " ++ Warning ++ ": ~s~n", [State#state.file, erl_syntax:get_pos(Form), erl_prettypr:format(Form)]). emit_module(RunDeps, RunFoldDeps, Module) -> File = filename:join(["src", Module]) ++ ".erl", try {ok, Fd} = file:open(File, [write]), write(Fd, "%% Generated automatically~n" "%% DO NOT EDIT: run `make hooks` instead~n~n", []), write(Fd, "-module(~s).~n", [Module]), write(Fd, "-compile(nowarn_unused_vars).~n", []), write(Fd, "-dialyzer(no_return).~n~n", []), emit_export(Fd, RunDeps, "run hooks"), emit_export(Fd, RunFoldDeps, "run_fold hooks"), emit_run_hooks(Fd, RunDeps), emit_run_fold_hooks(Fd, RunFoldDeps), file:close(Fd), log("Module written to ~s~n", [File]) catch _:{badmatch, {error, Reason}} -> err("Error: writing to ~s failed: ~s", [File, file:format_error(Reason)]) end. emit_run_hooks(Fd, Deps) -> DepsList = lists:sort(maps:to_list(Deps)), lists:foreach( fun({{Hook, Arity, {File, LineNo}}, Funs}) -> write(Fd, "%% called at ~s:~p~n", [File, LineNo]), Args = string:join( [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity)], ", "), write(Fd, "~s(~s) ->~n ", [Hook, Args]), Calls = [io_lib:format("_ = ~s:~s(~s)", [Mod, Fun, Args]) || {{Mod, Fun, _}, _Seq, _} <- lists:keysort(2, Funs)], write(Fd, "~s.~n~n", [string:join(Calls ++ ["ok"], ",\n ")]) end, DepsList). emit_run_fold_hooks(Fd, Deps) -> DepsList = lists:sort(maps:to_list(Deps)), lists:foreach( fun({{Hook, Arity, {File, LineNo}}, []}) -> write(Fd, "%% called at ~s:~p~n", [File, LineNo]), Args = ["Acc"|lists:duplicate(Arity - 1, "_")], write(Fd, "~s(~s) -> Acc.~n~n", [Hook, string:join(Args, ", ")]); ({{Hook, Arity, {File, LineNo}}, Funs}) -> write(Fd, "%% called at ~s:~p~n", [File, LineNo]), Args = [[N] || N <- lists:sublist(lists:seq($A, $Z), Arity - 1)], write(Fd, "~s(~s) ->~n ", [Hook, string:join(["Acc0"|Args], ", ")]), {Calls, _} = lists:mapfoldl( fun({{Mod, Fun, _}, _Seq, _}, N) -> Args1 = ["Acc" ++ integer_to_list(N)|Args], {io_lib:format("Acc~p = ~s:~s(~s)", [N+1, Mod, Fun, string:join(Args1, ", ")]), N + 1} end, 0, lists:keysort(2, Funs)), write(Fd, "~s,~n", [string:join(Calls, ",\n ")]), write(Fd, " Acc~p.~n~n", [length(Funs)]) end, DepsList). emit_export(Fd, Deps, Comment) -> DepsList = lists:sort(maps:to_list(Deps)), Exports = lists:map( fun({{Hook, Arity, _}, _}) -> io_lib:format("~s/~p", [Hook, Arity]) end, DepsList), write(Fd, "%% ~s~n-export([~s]).~n~n", [Comment, string:join(Exports, ",\n ")]). fold_beams(Fun, State, Paths) -> Paths1 = fold_paths(Paths), Total = length(Paths1), {_, State1} = lists:foldl( fun(File, {I, Acc}) -> io:format("Progress: ~B% (~B/~B)\r", [round(I*100/Total), I, Total]), case is_elixir_beam(File) of true -> {I+1, Acc}; false -> {AbsCode, Exports} = get_code_from_beam(File), Acc2 = lists:foldl( fun(Form, Acc1) -> Fun(File, Form, Exports, Acc1) end, Acc, AbsCode), {I+1, Acc2} end end, {0, State}, Paths1), State1. fold_paths(Paths) -> lists:flatmap( fun(Path) -> case filelib:is_dir(Path) of true -> lists:reverse( filelib:fold_files( Path, ".+\.beam\$", false, fun(File, Acc) -> [File|Acc] end, [])); false -> [Path] end end, Paths). is_elixir_beam(File) -> case filename:basename(File) of "Elixir" ++ _ -> true; _ -> false end. get_code_from_beam(File) -> case beam_lib:chunks(File, [abstract_code, exports]) of {ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}, {exports, X}]}} -> {Forms, X}; _ -> err("No abstract code found in ~s~n", [File]) end. log(Format, Args) -> io:format(standard_io, Format, Args). err(Format, Args) -> io:format(standard_error, Format, Args), halt(1). write(Fd, Format, Args) -> file:write(Fd, io_lib:format(Format, Args)). maps_append(K, V, M) -> maps_append_list(K, [V], M). maps_append_list(K, L1, M) -> L2 = maps:get(K, M, []), maps:put(K, L2 ++ L1, M). �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/tools/extract-tr.sh������������������������������������������������������������������0000755�0002322�0002322�00000007772�14154362354�017443� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env escript %% -*- erlang -*- main(Paths) -> Dict = fold_erls( fun(File, Tokens, Acc) -> extract_tr(File, Tokens, Acc) end, dict:new(), Paths), generate_pot(Dict). extract_tr(File, [{'?', _}, {var, _, 'T'}, {'(', Line}|Tokens], Acc) -> case extract_string(Tokens, "") of {"", Tokens1} -> err("~s:~B: Warning: invalid string", [File, Line]), extract_tr(File, Tokens1, Acc); {String, Tokens1} -> extract_tr(File, Tokens1, dict:append(String, {File, Line}, Acc)) end; extract_tr(_File, [{atom,_,module}, {'(',_}, {atom,_,ejabberd_doc} | _Tokens], Acc) -> Acc; extract_tr(File, [{atom, _, F}, {'(',_} | Tokens], Acc) when (F == mod_doc); (F == doc) -> Tokens2 = consume_tokens_until_dot(Tokens), extract_tr(File, Tokens2, Acc); extract_tr(File, [_|Tokens], Acc) -> %%err("~p~n", [A]), extract_tr(File, Tokens, Acc); extract_tr(_, [], Acc) -> Acc. consume_tokens_until_dot([{dot, _} | Tokens]) -> Tokens; consume_tokens_until_dot([_ | Tokens]) -> consume_tokens_until_dot(Tokens). extract_string([{string, _, S}|Tokens], Acc) -> extract_string(Tokens, [S|Acc]); extract_string([{')', _}|Tokens], Acc) -> {lists:flatten(lists:reverse(Acc)), Tokens}; extract_string(Tokens, _) -> {"", Tokens}. fold_erls(Fun, State, Paths) -> Paths1 = fold_paths(Paths), Total = length(Paths1), {_, State1} = lists:foldl( fun(File, {I, Acc}) -> io:format(standard_error, "Progress: ~B% (~B/~B)\r", [round(I*100/Total), I, Total]), case tokens(File) of {ok, Tokens} -> {I+1, Fun(File, Tokens, Acc)}; error -> {I+1, Acc} end end, {0, State}, Paths1), State1. fold_paths(Paths) -> lists:flatmap( fun(Path) -> case filelib:is_dir(Path) of true -> lists:reverse( filelib:fold_files( Path, ".+\.erl\$", false, fun(File, Acc) -> [File|Acc] end, [])); false -> [Path] end end, Paths). tokens(File) -> case file:read_file(File) of {ok, Data} -> case erl_scan:string(binary_to_list(Data)) of {ok, Tokens, _} -> {ok, Tokens}; {error, {_, Module, Desc}, Line} -> err("~s:~n: Warning: scan error: ~s", [filename:basename(File), Line, Module:format_error(Desc)]), error end; {error, Why} -> err("Warning: failed to read file ~s: ~s", [File, file:format_error(Why)]), error end. generate_pot(Dict) -> io:format("~s~n~n", [pot_header()]), lists:foreach( fun({Msg, Location}) -> S1 = format_location(Location), S2 = format_msg(Msg), io:format("~smsgstr \"\"~n~n", [S1 ++ S2]) end, lists:keysort(1, dict:to_list(Dict))). format_location([A, B, C|T]) -> format_location_list([A,B,C]) ++ format_location(T); format_location([A, B|T]) -> format_location_list([A,B]) ++ format_location(T); format_location([A|T]) -> format_location_list([A]) ++ format_location(T); format_location([]) -> "". format_location_list(L) -> "#: " ++ string:join( lists:map( fun({File, Pos}) -> io_lib:format("~s:~B", [File, Pos]) end, L), " ") ++ io_lib:nl(). format_msg(Bin) -> io_lib:format("msgid \"~s\"~n", [escape(Bin)]). escape(Bin) -> lists:map( fun($") -> "\\\""; (C) -> C end, binary_to_list(iolist_to_binary(Bin))). pot_header() -> string:join( ["msgid \"\"", "msgstr \"\"", "\"Project-Id-Version: 15.11.127\\n\"", "\"X-Language: Language Name\\n\"", "\"Last-Translator: Translator name and contact method\\n\"", "\"MIME-Version: 1.0\\n\"", "\"Content-Type: text/plain; charset=UTF-8\\n\"", "\"Content-Transfer-Encoding: 8bit\\n\"", "\"X-Poedit-Basepath: ../..\\n\"", "\"X-Poedit-SearchPath-0: .\\n\""], io_lib:nl()). err(Format, Args) -> io:format(standard_error, Format ++ io_lib:nl(), Args). ������ejabberd-21.12/tools/captcha.sh���������������������������������������������������������������������0000755�0002322�0002322�00000005303�14154362354�016735� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # This script is an example captcha script. # It takes the text to recognize in the captcha image as a parameter. # It return the image binary as a result. ejabberd support PNG, JPEG and GIF. # The whole idea of the captcha script is to let server admins adapt it to # their own needs. The goal is to be able to make the captcha generation as # unique as possible, to make the captcha challenge difficult to bypass by # a bot. # Server admins are thus supposed to write and use their own captcha generators. # This script relies on ImageMagick. # It is NOT compliant with ImageMagick forks like GraphicsMagick. INPUT=$1 if test -n ${BASH_VERSION:-''} ; then get_random () { R=$RANDOM } else for n in `od -A n -t u2 -N 48 /dev/urandom`; do RL="$RL$n "; done get_random () { R=${RL%% *} RL=${RL#* } } fi get_random WAVE1_AMPLITUDE=$((2 + $R % 5)) get_random WAVE1_LENGTH=$((50 + $R % 25)) get_random WAVE2_AMPLITUDE=$((2 + $R % 5)) get_random WAVE2_LENGTH=$((50 + $R % 25)) get_random WAVE3_AMPLITUDE=$((2 + $R % 5)) get_random WAVE3_LENGTH=$((50 + $R % 25)) get_random W1_LINE_START_Y=$((10 + $R % 40)) get_random W1_LINE_STOP_Y=$((10 + $R % 40)) get_random W2_LINE_START_Y=$((10 + $R % 40)) get_random W2_LINE_STOP_Y=$((10 + $R % 40)) get_random W3_LINE_START_Y=$((10 + $R % 40)) get_random W3_LINE_STOP_Y=$((10 + $R % 40)) get_random B1_LINE_START_Y=$(($R % 40)) get_random B1_LINE_STOP_Y=$(($R % 40)) get_random B2_LINE_START_Y=$(($R % 40)) get_random B2_LINE_STOP_Y=$(($R % 40)) #B3_LINE_START_Y=$(($R % 40)) #B3_LINE_STOP_Y=$(($R % 40)) get_random B1_LINE_START_X=$(($R % 20)) get_random B1_LINE_STOP_X=$((100 + $R % 40)) get_random B2_LINE_START_X=$(($R % 20)) get_random B2_LINE_STOP_X=$((100 + $R % 40)) #B3_LINE_START_X=$(($R % 20)) #B3_LINE_STOP_X=$((100 + $R % 40)) get_random ROLL_X=$(($R % 40)) convert -size 180x60 xc:none -pointsize 40 \ \( -clone 0 -fill white \ -stroke black -strokewidth 4 -annotate +0+40 "$INPUT" \ -stroke white -strokewidth 2 -annotate +0+40 "$INPUT" \ -roll +$ROLL_X+0 \ -wave "$WAVE1_AMPLITUDE"x"$WAVE1_LENGTH" \ -roll -$ROLL_X+0 \) \ \( -clone 0 -stroke black \ -strokewidth 1 -draw \ "line $B1_LINE_START_X,$B1_LINE_START_Y $B1_LINE_STOP_X,$B1_LINE_STOP_Y" \ -strokewidth 1 -draw \ "line $B2_LINE_START_X,$B2_LINE_START_Y $B2_LINE_STOP_X,$B2_LINE_STOP_Y" \ -wave "$WAVE2_AMPLITUDE"x"$WAVE2_LENGTH" \) \ \( -clone 0 -stroke white \ -strokewidth 2 -draw "line 0,$W1_LINE_START_Y 140,$W1_LINE_STOP_Y" \ -strokewidth 2 -draw "line 0,$W2_LINE_START_Y 140,$W2_LINE_STOP_Y" \ -strokewidth 2 -draw "line 0,$W3_LINE_START_Y 140,$W3_LINE_STOP_Y" \ -wave "$WAVE3_AMPLITUDE"x"$WAVE3_LENGTH" \) \ -flatten -crop 140x60 +repage -quality 90 -depth 8 png:- �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/tools/update-deps-releases.pl��������������������������������������������������������0000755�0002322�0002322�00000043224�14154362354�021353� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/perl use v5.10; use strict; use warnings; use File::Slurp qw(slurp write_file); use File::stat; use File::Touch; use File::chdir; use File::Spec; use Data::Dumper qw(Dumper); use Carp; use Term::ANSIColor; use Term::ReadKey; use List::Util qw(first); use Clone qw(clone); use LWP::UserAgent; sub get_deps { my ($config, %fdeps) = @_; my %deps; return { } unless $config =~ /\{\s*deps\s*,\s*\[(.*?)\]/s; my $sdeps = $1; while ($sdeps =~ /\{\s* (\w+) \s*,\s* ".*?" \s*,\s* \{\s*git \s*,\s* "(.*?)" \s*,\s* (?: (?:{\s*tag \s*,\s* "(.*?)") | "(.*?)" | ( \{ (?: (?-1) | [^{}]+ )+ \} ) )/sgx) { next unless not %fdeps or exists $fdeps{$1}; $deps{$1} = { repo => $2, commit => $3 || $4 }; } return \%deps; } my (%info_updates, %top_deps_updates, %sub_deps_updates, @operations); my $epoch = 1; sub top_deps { state %deps; state $my_epoch = $epoch; if (not %deps or $my_epoch != $epoch) { $my_epoch = $epoch; my $config = slurp "rebar.config"; croak "Unable to extract floating_deps" unless $config =~ /\{floating_deps, \[(.*?)\]/s; my $fdeps = $1; $fdeps =~ s/\s*//g; my %fdeps = map { $_ => 1 } split /,/, $fdeps; %deps = %{get_deps($config, %fdeps)}; } return {%deps, %top_deps_updates}; } sub update_deps_repos { my ($force) = @_; my $deps = top_deps(); $epoch++; mkdir(".deps-update") unless -d ".deps-update"; for my $dep (keys %{$deps}) { my $dd = ".deps-update/$dep"; if (not -d $dd) { say "Downloading $dep..."; my $repo = $deps->{$dep}->{repo}; $repo =~ s!^https?://github.com/!git\@github.com:!; system("git", "-C", ".deps-update", "clone", $repo, $dep); } elsif (time() - stat($dd)->mtime > 24 * 60 * 60 or $force) { say "Updating $dep..."; system("git", "-C", $dd, "pull"); touch($dd) } } } sub sub_deps { state %sub_deps; state $my_epoch = $epoch; if (not %sub_deps or $my_epoch != $epoch) { $my_epoch = $epoch; my $deps = top_deps(); for my $dep (keys %{$deps}) { my $rc = ".deps-update/$dep/rebar.config"; $sub_deps{$dep} = { }; next unless -f $rc; $sub_deps{$dep} = get_deps(scalar(slurp($rc))); } } return {%sub_deps, %sub_deps_updates}; } sub rev_deps_helper { my ($rev_deps, $dep) = @_; if (not exists $rev_deps->{$dep}->{indirect}) { my %deps = %{$rev_deps->{$dep}->{direct} || {}}; for (keys %{$rev_deps->{$dep}->{direct}}) { %deps = (%deps, %{rev_deps_helper($rev_deps, $_)}); } $rev_deps->{$dep}->{indirect} = \%deps; } return $rev_deps->{$dep}->{indirect}; } sub rev_deps { state %rev_deps; state $deps_epoch = $epoch; if (not %rev_deps or $deps_epoch != $epoch) { $deps_epoch = $epoch; my $sub_deps = sub_deps(); for my $dep (keys %$sub_deps) { $rev_deps{$_}->{direct}->{$dep} = 1 for keys %{$sub_deps->{$dep}}; } for my $dep (keys %$sub_deps) { $rev_deps{$dep}->{indirect} = rev_deps_helper(\%rev_deps, $dep); } } return \%rev_deps; } sub update_changelog { my ($dep, $version, @reasons) = @_; my $cl = ".deps-update/$dep/CHANGELOG.md"; return if not -f $cl; my $reason = join "\n", map {"* $_"} @reasons; my $content = slurp($cl); if (not $content =~ /^# Version $version/) { $content = "# Version $version\n\n$reason\n\n$content"; } else { $content =~ s/(# Version $version\n\n)/$1$reason\n/; } write_file($cl, $content); } sub edit_changelog { my ($dep, $version) = @_; my $cl = ".deps-update/$dep/CHANGELOG.md"; return if not -f $cl; my $top_deps = top_deps(); my $git_info = deps_git_info(); say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):"; say " $_" for @{$git_info->{$dep}->{new_commits}}; say ""; my $content = slurp($cl); my $old_content = $content; if (not $content =~ /^# Version $version/) { $content = "# Version $version\n\n* \n\n$content"; } else { $content =~ s/(# Version $version\n\n)/$1* \n/; } write_file($cl, $content); system("$ENV{EDITOR} $cl"); my $new_content = slurp($cl); if ($new_content eq $content) { write_file($cl, $old_content); } else { system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", "Update changelog"); } } sub update_app_src { my ($dep, $version) = @_; my $app = ".deps-update/$dep/src/$dep.app.src"; return if not -f $app; my $content = slurp($app); $content =~ s/(\{\s*vsn\s*,\s*)".*"/$1"$version"/; write_file($app, $content); } sub update_deps_versions { my ($config_path, %deps) = @_; my $config = slurp $config_path; for (keys %deps) { $config =~ s/(\{\s*$_\s*,\s*".*?"\s*,\s*\{\s*git\s*,\s*".*?"\s*,\s*)(?:{\s*tag\s*,\s*"(.*?)"\s*}|"(.*?)")/$1\{tag, "$deps{$_}"}/s; } write_file($config_path, $config); } sub cmp_ver { my @a = split /(\d+)/, $a; my @b = split /(\d+)/, $b; my $is_num = 1; return - 1 if $#a == 0; return 1 if $#b == 0; while (1) { my $ap = shift @a; my $bp = shift @b; $is_num = 1 - $is_num; if (defined $ap) { if (defined $bp) { if ($is_num) { next if $ap == $bp; return 1 if $ap > $bp; return - 1; } else { next if $ap eq $bp or $ap eq "" or $bp eq ""; return 1 if $ap gt $bp; return - 1; } } else { return 1; } } elsif (defined $bp) { return - 1; } else { return 0; } } } sub deps_git_info { state %info; state $my_epoch = $epoch; if (not %info or $my_epoch != $epoch) { $my_epoch = $epoch; my $deps = top_deps(); for my $dep (keys %{$deps}) { my $dir = ".deps-update/$dep"; my @tags = `git -C "$dir" tag`; chomp(@tags); @tags = sort cmp_ver @tags; my $last_tag = $tags[$#tags]; my @new = `git -C $dir log --oneline $last_tag..origin/master`; my $new_tag = $last_tag; $new_tag =~ s/(\d+)$/$1+1/e; chomp(@new); my $cl = ".deps-update/$dep/CHANGELOG.md"; my $content = slurp($cl, err_mode => "quiet") // ""; if ($content =~ /^# Version (\S+)/) { if (!grep({$_ eq $1} @tags) && $1 ne $new_tag) { $new_tag = $1; } } $info{$dep} = { last_tag => $last_tag, new_commits => \@new, new_tag => $new_tag }; } } return { %info, %info_updates }; } sub show_commands { my %commands = @_; my @keys; while (@_) { push @keys, shift; shift; } for (@keys) { say color("red"), $_, color("reset"), ") $commands{$_}"; } ReadMode(4); my $wkey = ""; while (1) { my $key = ReadKey(0); $wkey = substr($wkey.$key, -2); if (defined $commands{uc($key)}) { ReadMode(0); say ""; return uc($key); } elsif (defined $commands{uc($wkey)}) { ReadMode(0); say ""; return uc($wkey); } } } sub schedule_operation { my ($type, $dep, $tag, $reason, $op) = @_; my $idx = first { $operations[$_]->{dep} eq $dep } 0..$#operations; if (defined $idx) { my $mop = $operations[$idx]; if (defined $op) { my $oidx = first { $mop->{operations}->[$_]->[0] eq $op->[0] } 0..$#{$mop->{operations}}; if (defined $oidx) { $mop->{reasons}->[$oidx] = $reason; $mop->{operations}->[$oidx] = $op; } else { push @{$mop->{reasons}}, $reason; push @{$mop->{operations}}, $op; } } return if $type eq "update"; $mop->{type} = $type; $info_updates{$dep}->{new_commits} = []; return; } my $info = deps_git_info(); $top_deps_updates{$dep} = {commit => $tag}; $info_updates{$dep} = {last_tag => $tag, new_tag => $tag, new_commits => $type eq "tupdate" ? [] : $info->{$dep}->{new_commits}}; my $rev_deps = rev_deps(); @operations = sort { exists $rev_deps->{$a->{dep}}->{indirect}->{$b->{dep}} ? -1 : exists $rev_deps->{$b->{dep}}->{indirect}->{$a->{dep}} ? 1 : $a->{dep} cmp $b->{dep} } (@operations, { type => $type, dep => $dep, version => $tag, reasons => ($reason ? [$reason] : []), operations => ($op ? [$op] : [])} ); my $sub_deps = sub_deps(); for (keys %{$rev_deps->{$dep}->{direct}}) { schedule_operation("update", $_, $info->{$_}->{new_tag}, "Updating $dep to version $tag.", [$dep, $tag]); $sub_deps_updates{$_} = $sub_deps_updates{$_} || clone($sub_deps->{$_}); $sub_deps_updates{$_}->{$dep}->{commit} = $tag; } } sub git_tag { my ($dep, $ver, $msg) = @_; system("git", "-C", ".deps-update/$dep", "commit", "-a", "-m", $msg); system("git", "-C", ".deps-update/$dep", "tag", $ver, "-a", "-m", $msg); } sub git_push { my ($dep) = @_; system("git", "-C", ".deps-update/$dep", "push"); system("git", "-C", ".deps-update/$dep", "push", "--tags"); } sub check_hex_files { my ($dep) = @_; my $app = ".deps-update/$dep/src/$dep.app.src"; return if not -f $app; my $content = slurp($app); my @paths; if ($content =~ /{\s*files\s*,\s*\[([^\]]+)\]/) { my $list = $1; push @paths, $1 while $list =~ /"([^"]*?)"/g; } else { @paths = ( "src", "c_src", "include", "rebar.config.script", "priv", "rebar.config", "rebar.lock", "README*", "readme*", "LICENSE*", "license*", "NOTICE"); } local $CWD = ".deps-update/$dep"; my @interesting_files = map {File::Spec->canonpath($_)} glob("rebar.config* src/*.erl src/*.app.src c_src/*.c c_src/*.cpp \ c_src/*.h c_src/*.hpp include/*.hrl"); my @matching_files; for my $path (@paths) { if (-d $path) { push @matching_files, map {File::Spec->canonpath($_)} glob("$path/*"); } else { push @matching_files, map {File::Spec->canonpath($_)} glob($path); } } my %diff; @diff{ @interesting_files } = undef; delete @diff{ @matching_files }; my @diff = keys %diff; if (@diff) { print color("red"), "Dependency ", color("bold red"), $dep, color("reset"), color("red"), " files section doesn't match: ", join(" ", @diff), color("reset"), "\n"; } } update_deps_repos(); MAIN: while (1) { my $top_deps = top_deps(); my $git_info = deps_git_info(); print color("bold blue"), "Dependences with newer tags:\n", color("reset"); my $old_deps = 0; for my $dep (sort keys %$top_deps) { next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit}; say color("red"), "$dep", color("reset"), ": $top_deps->{$dep}->{commit} -> $git_info->{$dep}->{last_tag}"; $old_deps = 1; } say "(none)" if not $old_deps; say ""; print color("bold blue"), "Dependences that have commits after last tags:\n", color("reset"); my $changed_deps = 0; for my $dep (sort keys %$top_deps) { next unless @{$git_info->{$dep}->{new_commits}}; say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit}):"; say " $_" for @{$git_info->{$dep}->{new_commits}}; $changed_deps = 1; } say "(none)" if not $changed_deps; say ""; for my $dep (sort keys %$top_deps) { check_hex_files($dep); } my $cmd = show_commands($old_deps ? (U => "Update dependency") : (), $changed_deps ? (T => "Tag new release") : (), @operations ? (A => "Apply changes") : (), R => "Refresh repositiories", H => "What release to Hex", E => "Exit"); last if $cmd eq "E"; if ($cmd eq "U") { while (1) { my @deps_to_update; my @od; my $idx = 1; for my $dep (sort keys %$top_deps) { next unless $git_info->{$dep}->{last_tag} ne $top_deps->{$dep}->{commit}; $od[$idx] = $dep; push @deps_to_update, $idx++, "Update $dep to $git_info->{$dep}->{last_tag}"; } last if $idx == 1; my $cmd = show_commands(@deps_to_update, E => "Exit"); last if $cmd eq "E"; my $dep = $od[$cmd]; schedule_operation("update", $dep, $git_info->{$dep}->{last_tag}); $top_deps = top_deps(); $git_info = deps_git_info(); } } if ($cmd eq "R") { update_deps_repos(1); } if ($cmd eq "H") { my $ua = LWP::UserAgent->new(); for my $dep (sort keys %$top_deps) { say "checking https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}"; my $res = $ua->head("https://hex.pm/packages/$dep/$git_info->{$dep}->{last_tag}"); if ($res->code == 404) { say color("red"), "$dep", color("reset"), " ($top_deps->{$dep}->{commit})"; } } } if ($cmd eq "T") { while (1) { my @deps_to_tag; my @od; my $idx = 1; my $count = 0; for my $dep (sort keys %$top_deps) { next unless @{$git_info->{$dep}->{new_commits}}; $count++; } for my $dep (sort keys %$top_deps) { next unless @{$git_info->{$dep}->{new_commits}}; $od[$idx] = $dep; my $id = $idx++; $id = sprintf "%02d", $id if $count > 9; push @deps_to_tag, $id, "Tag $dep with version $git_info->{$dep}->{new_tag}"; } last if $idx == 1; my $cmd = show_commands(@deps_to_tag, E => "Exit"); last if $cmd eq "E"; my $dep = $od[$cmd]; my $d = $git_info->{$dep}; schedule_operation("tupdate", $dep, $d->{new_tag}); $top_deps = top_deps(); $git_info = deps_git_info(); } } my $changelog_updated = 0; if ($cmd eq "A") { APPLY: { $top_deps = top_deps(); $git_info = deps_git_info(); my $sub_deps = sub_deps(); for my $dep (keys %$top_deps) { for my $sdep (keys %{$sub_deps->{$dep}}) { next if not defined $top_deps->{$sdep} or $sub_deps->{$dep}->{$sdep}->{commit} eq $top_deps->{$sdep}->{commit}; say "$dep $sdep ", $sub_deps->{$dep}->{$sdep}->{commit}, " <=> $sdep ", $top_deps->{$sdep}->{commit}; schedule_operation("update", $dep, $git_info->{$dep}->{new_tag}, "Updating $sdep to version $top_deps->{$sdep}->{commit}.", [ $sdep, $top_deps->{$sdep}->{commit} ]); } } %info_updates = (); %top_deps_updates = (); %sub_deps_updates = (); $top_deps = top_deps(); $git_info = deps_git_info(); $sub_deps = sub_deps(); print color("bold blue"), "List of operations:\n", color("reset"); for my $op (@operations) { print color("red"), $op->{dep}, color("reset"), " ($top_deps->{$op->{dep}}->{commit} -> $op->{version})"; if (@{$op->{operations}}) { say ":"; say " $_->[0] -> $_->[1]" for @{$op->{operations}}; } else { say ""; } } say ""; my %to_tag; if (not $changelog_updated) { for my $op (@operations) { if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { $to_tag{$op->{dep}} = $op->{version}; } } } my $cmd = show_commands(A => "Apply", (%to_tag ? (U => "Update Changelogs") : ()), E => "Exit"); if ($cmd eq "U") { for my $dep (keys %to_tag) { edit_changelog($dep, $to_tag{$dep}); } redo APPLY; } elsif ($cmd eq "A") { my %top_changes; for my $op (@operations) { update_changelog($op->{dep}, $op->{version}, @{$op->{reasons}}) if @{$op->{reasons}}; update_deps_versions(".deps-update/$op->{dep}/rebar.config", map {@{$_}[0,1] } @{$op->{operations}}) if @{$op->{operations}}; if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { update_app_src($op->{dep}, $op->{version}); git_tag($op->{dep}, $op->{version}, "Release $op->{version}"); } $top_changes{$op->{dep}} = $op->{version}; } update_deps_versions("rebar.config", %top_changes); for my $op (@operations) { if ($git_info->{$op->{dep}}->{last_tag} ne $op->{version}) { git_push($op->{dep}); } } last MAIN; } } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/���������������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�014431� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/lite.sql�������������������������������������������������������������������������0000644�0002322�0002322�00000032162�14154362354�016113� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- CREATE TABLE users ( username text PRIMARY KEY, password text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE last ( username text PRIMARY KEY, seconds text NOT NULL, state text NOT NULL ); CREATE TABLE rosterusers ( username text NOT NULL, jid text NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, type text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers (username, jid); CREATE INDEX i_rosteru_username ON rosterusers (username); CREATE INDEX i_rosteru_jid ON rosterusers (jid); CREATE TABLE rostergroups ( username text NOT NULL, jid text NOT NULL, grp text NOT NULL ); CREATE INDEX pk_rosterg_user_jid ON rostergroups (username, jid); CREATE TABLE sr_group ( name text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_sr_group_name ON sr_group (name); CREATE TABLE sr_user ( jid text NOT NULL, grp text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user (jid, grp); CREATE INDEX i_sr_user_jid ON sr_user (jid); CREATE INDEX i_sr_user_grp ON sr_user (grp); CREATE TABLE spool ( username text NOT NULL, xml text NOT NULL, seq INTEGER PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_despool ON spool (username); CREATE TABLE archive ( username text NOT NULL, timestamp BIGINT UNSIGNED NOT NULL, peer text NOT NULL, bare_peer text NOT NULL, xml text NOT NULL, txt text, id INTEGER PRIMARY KEY AUTOINCREMENT, kind text, nick text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_username_timestamp ON archive(username, timestamp); CREATE INDEX i_archive_username_peer ON archive (username, peer); CREATE INDEX i_archive_username_bare_peer ON archive (username, bare_peer); CREATE INDEX i_timestamp ON archive(timestamp); CREATE TABLE archive_prefs ( username text NOT NULL PRIMARY KEY, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE vcard ( username text PRIMARY KEY, vcard text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE vcard_search ( username text NOT NULL, lusername text PRIMARY KEY, fn text NOT NULL, lfn text NOT NULL, family text NOT NULL, lfamily text NOT NULL, given text NOT NULL, lgiven text NOT NULL, middle text NOT NULL, lmiddle text NOT NULL, nickname text NOT NULL, lnickname text NOT NULL, bday text NOT NULL, lbday text NOT NULL, ctry text NOT NULL, lctry text NOT NULL, locality text NOT NULL, llocality text NOT NULL, email text NOT NULL, lemail text NOT NULL, orgname text NOT NULL, lorgname text NOT NULL, orgunit text NOT NULL, lorgunit text NOT NULL ); CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); CREATE TABLE privacy_default_list ( username text PRIMARY KEY, name text NOT NULL ); CREATE TABLE privacy_list ( username text NOT NULL, name text NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_privacy_list_username ON privacy_list (username); CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list (username, name); CREATE TABLE privacy_list_data ( id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ); CREATE TABLE private_storage ( username text NOT NULL, namespace text NOT NULL, data text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_private_storage_username ON private_storage (username); CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage (username, namespace); CREATE TABLE roster_version ( username text PRIMARY KEY, version text NOT NULL ); CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, nodeid INTEGER PRIMARY KEY AUTOINCREMENT ); CREATE INDEX i_pubsub_node_parent ON pubsub_node (parent); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node (host, node); CREATE TABLE pubsub_node_option ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, name text NOT NULL, val text NOT NULL ); CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option (nodeid); CREATE TABLE pubsub_node_owner ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, owner text NOT NULL ); CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner (nodeid); CREATE TABLE pubsub_state ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', stateid INTEGER PRIMARY KEY AUTOINCREMENT ); CREATE INDEX i_pubsub_state_jid ON pubsub_state (jid); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state (nodeid, jid); CREATE TABLE pubsub_item ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload text NOT NULL DEFAULT '' ); CREATE INDEX i_pubsub_item_itemid ON pubsub_item (itemid); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item (nodeid, itemid); CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ); CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt (subid, opt_name); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, nick text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_muc_registered_nick ON muc_registered (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered (jid, host); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room (name, host); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, node text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); CREATE INDEX i_muc_online_users_us ON muc_online_users (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, host text NOT NULL, jid text NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( username text PRIMARY KEY, xml text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE caps_features ( node text NOT NULL, subnode text NOT NULL, feature text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_caps_features_node_subnode ON caps_features (node, subnode); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username text NOT NULL, resource text NOT NULL, priority text NOT NULL, info text NOT NULL ); CREATE UNIQUE INDEX i_sm_sid ON sm(usec, pid); CREATE INDEX i_sm_node ON sm(node); CREATE INDEX i_sm_username ON sm(username); CREATE TABLE oauth_token ( token text NOT NULL PRIMARY KEY, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ); CREATE TABLE oauth_client ( client_id text PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ); CREATE TABLE route ( domain text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ); CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); CREATE INDEX i_route_domain ON route(domain); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ); CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid); CREATE INDEX i_proxy65_jid ON proxy65 (jid_i); CREATE TABLE push_session ( username text NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL ); CREATE UNIQUE INDEX i_push_usn ON push_session (username, service, node); CREATE UNIQUE INDEX i_push_ut ON push_session (username, timestamp); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); CREATE INDEX i_mix_channel_serv ON mix_channel (service); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); CREATE INDEX i_mix_pam_us ON mix_pam (username); CREATE TABLE mqtt_pub ( username text NOT NULL, resource text NOT NULL, topic text NOT NULL, qos smallint NOT NULL, payload blob NOT NULL, payload_format smallint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data blob NOT NULL, user_properties blob NOT NULL, expiry bigint NOT NULL ); CREATE UNIQUE INDEX i_mqtt_topic ON mqtt_pub (topic); ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/mysql.old-to-new.sql�������������������������������������������������������������0000644�0002322�0002322�00000033625�14154362354�020314� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������SET @DEFAULT_HOST = ''; -- Please fill with name of your current host name BEGIN; DELIMITER ## CREATE PROCEDURE update_server_host(IN DEFAULT_HOST TEXT) BEGIN START TRANSACTION; SET FOREIGN_KEY_CHECKS = 0; SET @DEFAULT_HOST = DEFAULT_HOST; IF DEFAULT_HOST = '' THEN SELECT 'Please fill @DEFAULT_HOST parameter' as Error; ROLLBACK; ELSE ALTER TABLE `push_session` DROP INDEX `i_push_usn`; ALTER TABLE `push_session` DROP INDEX `i_push_ut`; ALTER TABLE `push_session` ADD COLUMN `server_host` VARCHAR (191) NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `push_session` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `push_session` ADD PRIMARY KEY (`server_host`, `username`(191), `timestamp`); ALTER TABLE `push_session` ADD UNIQUE INDEX `i_push_session_susn` (`server_host`, `username`(191), `service`(191), `node`(191)); ALTER TABLE `roster_version` DROP PRIMARY KEY; ALTER TABLE `roster_version` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `roster_version` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `roster_version` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `muc_online_room` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_online_room` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `motd` DROP PRIMARY KEY; ALTER TABLE `motd` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `motd` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `motd` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_username`; ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_jid`; ALTER TABLE `rosterusers` DROP INDEX `i_rosteru_user_jid`; ALTER TABLE `rosterusers` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `rosterusers` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `rosterusers` ADD UNIQUE INDEX `i_rosteru_sh_user_jid` (`server_host`, `username`(75), `jid`(75)); ALTER TABLE `rosterusers` ADD INDEX `i_rosteru_sh_username` (`server_host`, `username`); ALTER TABLE `rosterusers` ADD INDEX `i_rosteru_sh_jid` (`server_host`, `jid`); ALTER TABLE `private_storage` DROP INDEX `i_private_storage_username_namespace`; ALTER TABLE `private_storage` DROP INDEX `i_private_storage_username`; ALTER TABLE `private_storage` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `private_storage` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `private_storage` ADD PRIMARY KEY (`server_host`, `username`, `namespace`); ALTER TABLE `private_storage` ADD INDEX `i_private_storage_sh_username` USING BTREE (`server_host`, `username`); ALTER TABLE `mqtt_pub` DROP INDEX `i_mqtt_topic`; ALTER TABLE `mqtt_pub` ADD COLUMN `server_host` VARCHAR (191) NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `mqtt_pub` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `mqtt_pub` ADD UNIQUE INDEX `i_mqtt_topic_server` (`topic`(191), `server_host`); ALTER TABLE `vcard_search` DROP PRIMARY KEY; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lgiven`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lmiddle`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lnickname`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lbday`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lctry`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lfn`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lemail`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lorgunit`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_llocality`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lorgname`; ALTER TABLE `vcard_search` DROP INDEX `i_vcard_search_lfamily`; ALTER TABLE `vcard_search` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `lusername`; ALTER TABLE `vcard_search` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lfn` (`server_host`, `lfn`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_llocality` (`server_host`, `llocality`); ALTER TABLE `vcard_search` ADD PRIMARY KEY (`server_host`, `lusername`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lnickname` (`server_host`, `lnickname`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lctry` (`server_host`, `lctry`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lgiven` (`server_host`, `lgiven`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lmiddle` (`server_host`, `lmiddle`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lorgname` (`server_host`, `lorgname`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lfamily` (`server_host`, `lfamily`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lbday` (`server_host`, `lbday`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lemail` (`server_host`, `lemail`); ALTER TABLE `vcard_search` ADD INDEX `i_vcard_search_sh_lorgunit` (`server_host`, `lorgunit`); ALTER TABLE `last` DROP PRIMARY KEY; ALTER TABLE `last` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `last` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `last` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `sr_group` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `name`; ALTER TABLE `sr_group` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `sr_group` ADD UNIQUE INDEX `i_sr_group_sh_name` (`server_host`, `name`); ALTER TABLE `sr_group` ADD PRIMARY KEY (`server_host`, `name`); ALTER TABLE `muc_registered` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_registered` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `sm` DROP INDEX `i_node`; ALTER TABLE `sm` DROP INDEX `i_username`; ALTER TABLE `sm` DROP INDEX `i_sid`; ALTER TABLE `sm` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `sm` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `sm` ADD INDEX `i_sm_node` (`node`(75)); ALTER TABLE `sm` ADD INDEX `i_sm_sh_username` (`server_host`, `username`); ALTER TABLE `sm` ADD PRIMARY KEY (`usec`, `pid`(75)); ALTER TABLE `privacy_list` DROP INDEX `i_privacy_list_username_name`; ALTER TABLE `privacy_list` DROP INDEX `i_privacy_list_username`; ALTER TABLE `privacy_list` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `privacy_list` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `privacy_list` ADD UNIQUE INDEX `i_privacy_list_sh_username_name` USING BTREE (`server_host`, `username`(75), `name`(75)); ALTER TABLE `privacy_list` ADD INDEX `i_privacy_list_sh_username` USING BTREE (`server_host`, `username`); ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid`; ALTER TABLE `sr_user` DROP INDEX `i_sr_user_grp`; ALTER TABLE `sr_user` DROP INDEX `i_sr_user_jid_group`; ALTER TABLE `sr_user` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `jid`; ALTER TABLE `sr_user` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `sr_user` ADD UNIQUE INDEX `i_sr_user_sh_jid_group` (`server_host`, `jid`, `grp`); ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_jid` (`server_host`, `jid`); ALTER TABLE `sr_user` ADD INDEX `i_sr_user_sh_grp` (`server_host`, `grp`); ALTER TABLE `sr_user` ADD PRIMARY KEY (`server_host`, `jid`, `grp`); ALTER TABLE `muc_online_users` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_online_users` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `vcard` DROP PRIMARY KEY; ALTER TABLE `vcard` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `vcard` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `vcard` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `archive_prefs` DROP PRIMARY KEY; ALTER TABLE `archive_prefs` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `archive_prefs` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `archive_prefs` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `mix_pam` DROP INDEX `i_mix_pam`; ALTER TABLE `mix_pam` DROP INDEX `i_mix_pam_u`; ALTER TABLE `mix_pam` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `mix_pam` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `mix_pam` ADD UNIQUE INDEX `i_mix_pam` (`username`(191), `server_host`, `channel`(191), `service`(191)); ALTER TABLE `mix_pam` ADD INDEX `i_mix_pam_us` (`username`(191), `server_host`); ALTER TABLE `route` CHANGE COLUMN `server_host` `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL; ALTER TABLE `users` DROP PRIMARY KEY; ALTER TABLE `users` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `users` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `users` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `privacy_default_list` DROP PRIMARY KEY; ALTER TABLE `privacy_default_list` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `privacy_default_list` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `privacy_default_list` ADD PRIMARY KEY (`server_host`, `username`); ALTER TABLE `rostergroups` DROP INDEX `pk_rosterg_user_jid`; ALTER TABLE `rostergroups` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `rostergroups` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `rostergroups` ADD INDEX `i_rosterg_sh_user_jid` (`server_host`, `username`(75), `jid`(75)); ALTER TABLE `muc_room` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `host`; ALTER TABLE `muc_room` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `spool` DROP INDEX `i_despool`; ALTER TABLE `spool` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `spool` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `spool` ADD INDEX `i_spool_sh_username` USING BTREE (`server_host`, `username`); ALTER TABLE `archive` DROP INDEX `i_username_timestamp`; ALTER TABLE `archive` DROP INDEX `i_username_peer`; ALTER TABLE `archive` DROP INDEX `i_username_bare_peer`; ALTER TABLE `archive` DROP INDEX `i_timestamp`; ALTER TABLE `archive` ADD COLUMN `server_host` VARCHAR (191) COLLATE `utf8mb4_unicode_ci` NOT NULL DEFAULT @DEFAULT_HOST AFTER `username`; ALTER TABLE `archive` ALTER COLUMN `server_host` DROP DEFAULT; ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_bare_peer` USING BTREE (`server_host`, `username`, `bare_peer`); ALTER TABLE `archive` ADD INDEX `i_archive_sh_timestamp` USING BTREE (`server_host`, `timestamp`); ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_timestamp` USING BTREE (`server_host`, `username`, `timestamp`); ALTER TABLE `archive` ADD INDEX `i_archive_sh_username_peer` USING BTREE (`server_host`, `username`, `peer`); END IF; SET FOREIGN_KEY_CHECKS = 1; END; ## DELIMITER ; CALL update_server_host(@DEFAULT_HOST); DROP PROCEDURE update_server_host; COMMIT; �����������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/pg.new.sql�����������������������������������������������������������������������0000644�0002322�0002322�00000057133�14154362354�016361� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- -- To update from the old schema, replace <HOST> with the host's domain: -- ALTER TABLE users ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE users DROP CONSTRAINT users_pkey; -- ALTER TABLE users ADD PRIMARY KEY (server_host, username); -- ALTER TABLE users ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE last ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE last DROP CONSTRAINT last_pkey; -- ALTER TABLE last ADD PRIMARY KEY (server_host, username); -- ALTER TABLE last ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE rosterusers ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_rosteru_user_jid; -- DROP INDEX i_rosteru_username; -- DROP INDEX i_rosteru_jid; -- CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers USING btree (server_host, username, jid); -- CREATE INDEX i_rosteru_sh_username ON rosterusers USING btree (server_host, username); -- CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); -- ALTER TABLE rosterusers ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE rostergroups ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX pk_rosterg_user_jid; -- CREATE INDEX i_rosterg_sh_user_jid ON rostergroups USING btree (server_host, username, jid); -- ALTER TABLE rostergroups ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE sr_group ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE sr_group ADD PRIMARY KEY (server_host, name); -- ALTER TABLE sr_group ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE sr_user ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_sr_user_jid_grp; -- DROP INDEX i_sr_user_jid; -- DROP INDEX i_sr_user_grp; -- ALTER TABLE sr_user ADD PRIMARY KEY (server_host, jid, grp); -- CREATE INDEX i_sr_user_sh_jid ON sr_user USING btree (server_host, jid); -- CREATE INDEX i_sr_user_sh_grp ON sr_user USING btree (server_host, grp); -- ALTER TABLE sr_user ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE spool ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_despool; -- CREATE INDEX i_spool_sh_username ON spool USING btree (server_host, username); -- ALTER TABLE spool ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE archive ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_username_timestamp; -- DROP INDEX i_username_peer; -- DROP INDEX i_username_bare_peer; -- DROP INDEX i_timestamp; -- CREATE INDEX i_archive_sh_username_timestamp ON archive USING btree (server_host, username, timestamp); -- CREATE INDEX i_archive_sh_username_peer ON archive USING btree (server_host, username, peer); -- CREATE INDEX i_archive_sh_username_bare_peer ON archive USING btree (server_host, username, bare_peer); -- CREATE INDEX i_archive_sh_timestamp ON archive USING btree (server_host, timestamp); -- ALTER TABLE archive ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE archive_prefs ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE archive_prefs DROP CONSTRAINT archive_prefs_pkey; -- ALTER TABLE archive_prefs ADD PRIMARY KEY (server_host, username); -- ALTER TABLE archive_prefs ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE vcard ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE vcard DROP CONSTRAINT vcard_pkey; -- ALTER TABLE vcard ADD PRIMARY KEY (server_host, username); -- ALTER TABLE vcard ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE vcard_search ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE vcard_search DROP CONSTRAINT vcard_search_pkey; -- DROP INDEX i_vcard_search_lfn; -- DROP INDEX i_vcard_search_lfamily; -- DROP INDEX i_vcard_search_lgiven; -- DROP INDEX i_vcard_search_lmiddle; -- DROP INDEX i_vcard_search_lnickname; -- DROP INDEX i_vcard_search_lbday; -- DROP INDEX i_vcard_search_lctry; -- DROP INDEX i_vcard_search_llocality; -- DROP INDEX i_vcard_search_lemail; -- DROP INDEX i_vcard_search_lorgname; -- DROP INDEX i_vcard_search_lorgunit; -- ALTER TABLE vcard_search ADD PRIMARY KEY (server_host, username); -- CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); -- CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); -- CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); -- CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); -- CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); -- CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); -- CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); -- CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); -- CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); -- CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); -- CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); -- ALTER TABLE vcard_search ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE privacy_default_list ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE privacy_default_list DROP CONSTRAINT privacy_default_list_pkey; -- ALTER TABLE privacy_default_list ADD PRIMARY KEY (server_host, username); -- ALTER TABLE privacy_default_list ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE privacy_list ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_privacy_list_username; -- DROP INDEX i_privacy_list_username_name; -- CREATE INDEX i_privacy_list_sh_username ON privacy_list USING btree (server_host, username); -- CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list USING btree (server_host, username, name); -- ALTER TABLE privacy_list ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE private_storage ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_private_storage_username; -- DROP INDEX i_private_storage_username_namespace; -- ALTER TABLE private_storage ADD PRIMARY KEY (server_host, username, namespace); -- CREATE INDEX i_private_storage_sh_username ON private_storage USING btree (server_host, username); -- ALTER TABLE private_storage ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE roster_version ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE roster_version DROP CONSTRAINT roster_version_pkey; -- ALTER TABLE roster_version ADD PRIMARY KEY (server_host, username); -- ALTER TABLE roster_version ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE muc_room ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE muc_room ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE muc_registered ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE muc_registered ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE muc_online_room ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE muc_online_room ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE muc_online_users ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE muc_online_users ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE motd ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- ALTER TABLE motd DROP CONSTRAINT motd_pkey; -- ALTER TABLE motd ADD PRIMARY KEY (server_host, username); -- ALTER TABLE motd ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE sm ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_sm_sid; -- DROP INDEX i_sm_username; -- ALTER TABLE sm ADD PRIMARY KEY (usec, pid); -- CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username); -- ALTER TABLE sm ALTER COLUMN server_host DROP DEFAULT; -- ALTER TABLE push_session ADD COLUMN server_host text NOT NULL DEFAULT '<HOST>'; -- DROP INDEX i_push_usn; -- DROP INDEX i_push_ut; -- ALTER TABLE push_session ADD PRIMARY KEY (server_host, username, timestamp); -- CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node); CREATE TABLE users ( username text NOT NULL, server_host text NOT NULL, "password" text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, username) ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: -- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( username text NOT NULL, server_host text NOT NULL, seconds text NOT NULL, state text NOT NULL, PRIMARY KEY (server_host, username) ); CREATE TABLE rosterusers ( username text NOT NULL, server_host text NOT NULL, jid text NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, "type" text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers USING btree (server_host, username, jid); CREATE INDEX i_rosteru_sh_username ON rosterusers USING btree (server_host, username); CREATE INDEX i_rosteru_sh_jid ON rosterusers USING btree (server_host, jid); CREATE TABLE rostergroups ( username text NOT NULL, server_host text NOT NULL, jid text NOT NULL, grp text NOT NULL ); CREATE INDEX i_rosterg_sh_user_jid ON rostergroups USING btree (server_host, username, jid); CREATE TABLE sr_group ( name text NOT NULL, server_host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, name) ); CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group USING btree (server_host, name); CREATE TABLE sr_user ( jid text NOT NULL, server_host text NOT NULL, grp text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, jid, grp) ); CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user USING btree (server_host, jid, grp); CREATE INDEX i_sr_user_sh_jid ON sr_user USING btree (server_host, jid); CREATE INDEX i_sr_user_sh_grp ON sr_user USING btree (server_host, grp); CREATE TABLE spool ( username text NOT NULL, server_host text NOT NULL, xml text NOT NULL, seq SERIAL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_spool_sh_username ON spool USING btree (server_host, username); CREATE TABLE archive ( username text NOT NULL, server_host text NOT NULL, timestamp BIGINT NOT NULL, peer text NOT NULL, bare_peer text NOT NULL, xml text NOT NULL, txt text, id SERIAL, kind text, nick text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_archive_sh_username_timestamp ON archive USING btree (server_host, username, timestamp); CREATE INDEX i_archive_sh_username_peer ON archive USING btree (server_host, username, peer); CREATE INDEX i_archive_sh_username_bare_peer ON archive USING btree (server_host, username, bare_peer); CREATE INDEX i_archive_sh_timestamp ON archive USING btree (server_host, timestamp); CREATE TABLE archive_prefs ( username text NOT NULL, server_host text NOT NULL, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, username) ); CREATE TABLE vcard ( username text NOT NULL, server_host text NOT NULL, vcard text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, username) ); CREATE TABLE vcard_search ( username text NOT NULL, lusername text NOT NULL, server_host text NOT NULL, fn text NOT NULL, lfn text NOT NULL, "family" text NOT NULL, lfamily text NOT NULL, given text NOT NULL, lgiven text NOT NULL, middle text NOT NULL, lmiddle text NOT NULL, nickname text NOT NULL, lnickname text NOT NULL, bday text NOT NULL, lbday text NOT NULL, ctry text NOT NULL, lctry text NOT NULL, locality text NOT NULL, llocality text NOT NULL, email text NOT NULL, lemail text NOT NULL, orgname text NOT NULL, lorgname text NOT NULL, orgunit text NOT NULL, lorgunit text NOT NULL, PRIMARY KEY (server_host, lusername) ); CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); CREATE TABLE privacy_default_list ( username text NOT NULL, server_host text NOT NULL, name text NOT NULL, PRIMARY KEY (server_host, username) ); CREATE TABLE privacy_list ( username text NOT NULL, server_host text NOT NULL, name text NOT NULL, id SERIAL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_privacy_list_sh_username ON privacy_list USING btree (server_host, username); CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list USING btree (server_host, username, name); CREATE TABLE privacy_list_data ( id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ); CREATE INDEX i_privacy_list_data_id ON privacy_list_data USING btree (id); CREATE TABLE private_storage ( username text NOT NULL, server_host text NOT NULL, namespace text NOT NULL, data text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, username, namespace) ); CREATE INDEX i_private_storage_sh_username ON private_storage USING btree (server_host, username); CREATE TABLE roster_version ( username text NOT NULL, server_host text NOT NULL, version text NOT NULL, PRIMARY KEY (server_host, username) ); -- To update from 0.9.8: -- CREATE SEQUENCE spool_seq_seq; -- ALTER TABLE spool ADD COLUMN seq integer; -- ALTER TABLE spool ALTER COLUMN seq SET DEFAULT nextval('spool_seq_seq'); -- UPDATE spool SET seq = DEFAULT; -- ALTER TABLE spool ALTER COLUMN seq SET NOT NULL; -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, nodeid SERIAL UNIQUE ); CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); CREATE TABLE pubsub_node_option ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, name text NOT NULL, val text NOT NULL ); CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); CREATE TABLE pubsub_node_owner ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, owner text NOT NULL ); CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); CREATE TABLE pubsub_state ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', stateid SERIAL UNIQUE ); CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); CREATE TABLE pubsub_item ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload text NOT NULL DEFAULT '' ); CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ); CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, server_host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, server_host text NOT NULL, nick text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room USING btree (name, host); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, server_host text NOT NULL, node text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); CREATE INDEX i_muc_online_users_us ON muc_online_users USING btree (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, host text NOT NULL, jid text NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( username text NOT NULL, server_host text NOT NULL, xml text, created_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (server_host, username) ); CREATE TABLE caps_features ( node text NOT NULL, subnode text NOT NULL, feature text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_caps_features_node_subnode ON caps_features USING btree (node, subnode); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username text NOT NULL, server_host text NOT NULL, resource text NOT NULL, priority text NOT NULL, info text NOT NULL, PRIMARY KEY (usec, pid) ); CREATE INDEX i_sm_node ON sm USING btree (node); CREATE INDEX i_sm_sh_username ON sm USING btree (server_host, username); CREATE TABLE oauth_token ( token text NOT NULL, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ); CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); CREATE TABLE oauth_client ( client_id text PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ); CREATE TABLE route ( domain text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ); CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); CREATE INDEX i_route_domain ON route USING btree (domain); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_bosh_sid ON bosh USING btree (sid); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ); CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 USING btree (sid); CREATE INDEX i_proxy65_jid ON proxy65 USING btree (jid_i); CREATE TABLE push_session ( username text NOT NULL, server_host text NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL, PRIMARY KEY (server_host, username, timestamp) ); CREATE UNIQUE INDEX i_push_session_susn ON push_session USING btree (server_host, username, service, node); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); CREATE INDEX i_mix_channel_serv ON mix_channel (service); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, server_host text NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); CREATE TABLE mqtt_pub ( username text NOT NULL, server_host text NOT NULL, resource text NOT NULL, topic text NOT NULL, qos smallint NOT NULL, payload bytea NOT NULL, payload_format smallint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data bytea NOT NULL, user_properties bytea NOT NULL, expiry bigint NOT NULL ); CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/mysql.sql������������������������������������������������������������������������0000644�0002322�0002322�00000042704�14154362354�016326� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- CREATE TABLE users ( username varchar(191) PRIMARY KEY, password text NOT NULL, serverkey varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Add support for SCRAM auth to a database created before ejabberd 16.03: -- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( username varchar(191) PRIMARY KEY, seconds text NOT NULL, state text NOT NULl ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE rosterusers ( username varchar(191) NOT NULL, jid varchar(191) NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, type text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers(username(75), jid(75)); CREATE INDEX i_rosteru_username ON rosterusers(username); CREATE INDEX i_rosteru_jid ON rosterusers(jid); CREATE TABLE rostergroups ( username varchar(191) NOT NULL, jid varchar(191) NOT NULL, grp text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX pk_rosterg_user_jid ON rostergroups(username(75), jid(75)); CREATE TABLE sr_group ( name varchar(191) NOT NULL, opts text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sr_group_name ON sr_group(name); CREATE TABLE sr_user ( jid varchar(191) NOT NULL, grp varchar(191) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sr_user_jid_group ON sr_user(jid(75), grp(75)); CREATE INDEX i_sr_user_jid ON sr_user(jid); CREATE INDEX i_sr_user_grp ON sr_user(grp); CREATE TABLE spool ( username varchar(191) NOT NULL, xml mediumtext NOT NULL, seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_despool USING BTREE ON spool(username); CREATE INDEX i_spool_created_at USING BTREE ON spool(created_at); CREATE TABLE archive ( username varchar(191) NOT NULL, timestamp BIGINT UNSIGNED NOT NULL, peer varchar(191) NOT NULL, bare_peer varchar(191) NOT NULL, xml mediumtext NOT NULL, txt mediumtext, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, kind varchar(10), nick varchar(191), created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE FULLTEXT INDEX i_text ON archive(txt); CREATE INDEX i_username_timestamp USING BTREE ON archive(username(191), timestamp); CREATE INDEX i_username_peer USING BTREE ON archive(username(191), peer(191)); CREATE INDEX i_username_bare_peer USING BTREE ON archive(username(191), bare_peer(191)); CREATE INDEX i_timestamp USING BTREE ON archive(timestamp); CREATE TABLE archive_prefs ( username varchar(191) NOT NULL PRIMARY KEY, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard ( username varchar(191) PRIMARY KEY, vcard mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard_search ( username varchar(191) NOT NULL, lusername varchar(191) PRIMARY KEY, fn text NOT NULL, lfn varchar(191) NOT NULL, family text NOT NULL, lfamily varchar(191) NOT NULL, given text NOT NULL, lgiven varchar(191) NOT NULL, middle text NOT NULL, lmiddle varchar(191) NOT NULL, nickname text NOT NULL, lnickname varchar(191) NOT NULL, bday text NOT NULL, lbday varchar(191) NOT NULL, ctry text NOT NULL, lctry varchar(191) NOT NULL, locality text NOT NULL, llocality varchar(191) NOT NULL, email text NOT NULL, lemail varchar(191) NOT NULL, orgname text NOT NULL, lorgname varchar(191) NOT NULL, orgunit text NOT NULL, lorgunit varchar(191) NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); CREATE TABLE privacy_default_list ( username varchar(191) PRIMARY KEY, name varchar(191) NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE privacy_list ( username varchar(191) NOT NULL, name varchar(191) NOT NULL, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_privacy_list_username USING BTREE ON privacy_list(username); CREATE UNIQUE INDEX i_privacy_list_username_name USING BTREE ON privacy_list (username(75), name(75)); CREATE TABLE privacy_list_data ( id bigint, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_privacy_list_data_id ON privacy_list_data(id); CREATE TABLE private_storage ( username varchar(191) NOT NULL, namespace varchar(191) NOT NULL, data text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_private_storage_username USING BTREE ON private_storage(username); CREATE UNIQUE INDEX i_private_storage_username_namespace USING BTREE ON private_storage(username(75), namespace(75)); -- Not tested in mysql CREATE TABLE roster_version ( username varchar(191) PRIMARY KEY, version text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent VARCHAR(191) NOT NULL DEFAULT '', plugin text NOT NULL, nodeid bigint auto_increment primary key ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(71), node(120)); CREATE TABLE pubsub_node_option ( nodeid bigint, name text NOT NULL, val text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_node_owner ( nodeid bigint, owner text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_state ( nodeid bigint, jid text NOT NULL, affiliation character(1), subscriptions VARCHAR(191) NOT NULL DEFAULT '', stateid bigint auto_increment primary key ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_item ( nodeid bigint, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload mediumtext NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, opts mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, node text NOT NULL, pid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_online_room_name_host USING BTREE ON muc_online_room(name(75), host(75)); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, node text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_online_users USING BTREE ON muc_online_users(username(75), server(75), resource(75), name(75), host(75)); CREATE INDEX i_muc_online_users_us USING BTREE ON muc_online_users(username(75), server(75)); CREATE TABLE muc_room_subscribers ( room varchar(191) NOT NULL, host varchar(191) NOT NULL, jid varchar(191) NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY i_muc_room_subscribers_host_room_jid (host, room, jid) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); CREATE TABLE motd ( username varchar(191) PRIMARY KEY, xml text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE caps_features ( node varchar(191) NOT NULL, subnode varchar(191) NOT NULL, feature text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_caps_features_node_subnode ON caps_features(node(75), subnode(75)); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username varchar(191) NOT NULL, resource varchar(191) NOT NULL, priority text NOT NULL, info text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sid ON sm(usec, pid(75)); CREATE INDEX i_node ON sm(node(75)); CREATE INDEX i_username ON sm(username); CREATE TABLE oauth_token ( token varchar(191) NOT NULL PRIMARY KEY, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE oauth_client ( client_id varchar(191) NOT NULL PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE route ( domain text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_route ON route(domain(75), server_host(75), node(75), pid(75)); CREATE INDEX i_route_domain ON route(domain(75)); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid(75)); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid(191)); CREATE INDEX i_proxy65_jid ON proxy65 (jid_i(191)); CREATE TABLE push_session ( username text NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_push_usn ON push_session (username(191), service(191), node(191)); CREATE UNIQUE INDEX i_push_ut ON push_session (username(191), timestamp); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel(191), service(191)); CREATE INDEX i_mix_channel_serv ON mix_channel (service(191)); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel(191), service(191)); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel(153), service(153), username(153), domain(153), node(153)); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel(191), service(191)); CREATE TABLE mix_pam ( username text NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username(191), channel(191), service(191)); CREATE INDEX i_mix_pam_u ON mix_pam (username(191)); CREATE TABLE mqtt_pub ( username varchar(191) NOT NULL, resource varchar(191) NOT NULL, topic text NOT NULL, qos tinyint NOT NULL, payload blob NOT NULL, payload_format tinyint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data blob NOT NULL, user_properties blob NOT NULL, expiry int unsigned NOT NULL, UNIQUE KEY i_mqtt_topic (topic(191)) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ������������������������������������������������������������ejabberd-21.12/sql/mysql.new.sql��������������������������������������������������������������������0000644�0002322�0002322�00000046610�14154362354�017116� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- CREATE TABLE users ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, password text NOT NULL, serverkey varchar(128) NOT NULL DEFAULT '', salt varchar(128) NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- Add support for SCRAM auth to a database created before ejabberd 16.03: -- ALTER TABLE users ADD COLUMN serverkey varchar(64) NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN salt varchar(64) NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, seconds text NOT NULL, state text NOT NULL, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE rosterusers ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, jid varchar(191) NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, type text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers(server_host(191), username(75), jid(75)); CREATE INDEX i_rosteru_sh_username ON rosterusers(server_host(191), username); CREATE INDEX i_rosteru_sh_jid ON rosterusers(server_host(191), jid); CREATE TABLE rostergroups ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, jid varchar(191) NOT NULL, grp text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_rosterg_sh_user_jid ON rostergroups(server_host(191), username(75), jid(75)); CREATE TABLE sr_group ( name varchar(191) NOT NULL, server_host varchar(191) NOT NULL, opts text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), name) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group(server_host(191), name); CREATE TABLE sr_user ( jid varchar(191) NOT NULL, server_host varchar(191) NOT NULL, grp varchar(191) NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), jid, grp) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_sr_user_sh_jid_group ON sr_user(server_host(191), jid, grp); CREATE INDEX i_sr_user_sh_jid ON sr_user(server_host(191), jid); CREATE INDEX i_sr_user_sh_grp ON sr_user(server_host(191), grp); CREATE TABLE spool ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, xml mediumtext NOT NULL, seq BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_spool_sh_username USING BTREE ON spool(server_host(191), username); CREATE INDEX i_spool_created_at USING BTREE ON spool(created_at); CREATE TABLE archive ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, timestamp BIGINT UNSIGNED NOT NULL, peer varchar(191) NOT NULL, bare_peer varchar(191) NOT NULL, xml mediumtext NOT NULL, txt mediumtext, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, kind varchar(10), nick varchar(191), created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE FULLTEXT INDEX i_text ON archive(txt); CREATE INDEX i_archive_sh_username_timestamp USING BTREE ON archive(server_host(191), username(191), timestamp); CREATE INDEX i_archive_sh_username_peer USING BTREE ON archive(server_host(191), username(191), peer(191)); CREATE INDEX i_archive_sh_username_bare_peer USING BTREE ON archive(server_host(191), username(191), bare_peer(191)); CREATE INDEX i_archive_sh_timestamp USING BTREE ON archive(server_host(191), timestamp); CREATE TABLE archive_prefs ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, vcard mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE vcard_search ( username varchar(191) NOT NULL, lusername varchar(191) NOT NULL, server_host varchar(191) NOT NULL, fn text NOT NULL, lfn varchar(191) NOT NULL, family text NOT NULL, lfamily varchar(191) NOT NULL, given text NOT NULL, lgiven varchar(191) NOT NULL, middle text NOT NULL, lmiddle varchar(191) NOT NULL, nickname text NOT NULL, lnickname varchar(191) NOT NULL, bday text NOT NULL, lbday varchar(191) NOT NULL, ctry text NOT NULL, lctry varchar(191) NOT NULL, locality text NOT NULL, llocality varchar(191) NOT NULL, email text NOT NULL, lemail varchar(191) NOT NULL, orgname text NOT NULL, lorgname varchar(191) NOT NULL, orgunit text NOT NULL, lorgunit varchar(191) NOT NULL, PRIMARY KEY (server_host(191), lusername) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host(191), lfn); CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host(191), lfamily); CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host(191), lgiven); CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host(191), lmiddle); CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host(191), lnickname); CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host(191), lbday); CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host(191), lctry); CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host(191), llocality); CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host(191), lemail); CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host(191), lorgname); CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host(191), lorgunit); CREATE TABLE privacy_default_list ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, name varchar(191) NOT NULL, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE privacy_list ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, name varchar(191) NOT NULL, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_privacy_list_sh_username USING BTREE ON privacy_list(server_host(191), username); CREATE UNIQUE INDEX i_privacy_list_sh_username_name USING BTREE ON privacy_list (server_host(191), username(75), name(75)); CREATE TABLE privacy_list_data ( id bigint, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_privacy_list_data_id ON privacy_list_data(id); CREATE TABLE private_storage ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, namespace varchar(191) NOT NULL, data text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), username, namespace) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_private_storage_sh_username USING BTREE ON private_storage(server_host(191), username); -- Not tested in mysql CREATE TABLE roster_version ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, version text NOT NULL, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text AFTER ask; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent VARCHAR(191) NOT NULL DEFAULT '', plugin text NOT NULL, nodeid bigint auto_increment primary key ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_parent ON pubsub_node(parent(120)); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node(host(71), node(120)); CREATE TABLE pubsub_node_option ( nodeid bigint, name text NOT NULL, val text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option(nodeid); ALTER TABLE `pubsub_node_option` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_node_owner ( nodeid bigint, owner text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner(nodeid); ALTER TABLE `pubsub_node_owner` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_state ( nodeid bigint, jid text NOT NULL, affiliation character(1), subscriptions VARCHAR(191) NOT NULL DEFAULT '', stateid bigint auto_increment primary key ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_state_jid ON pubsub_state(jid(60)); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state(nodeid, jid(60)); ALTER TABLE `pubsub_state` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_item ( nodeid bigint, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload mediumtext NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_pubsub_item_itemid ON pubsub_item(itemid(36)); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item(nodeid, itemid(36)); ALTER TABLE `pubsub_item` ADD FOREIGN KEY (`nodeid`) REFERENCES `pubsub_node` (`nodeid`) ON DELETE CASCADE; CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt(subid(32), opt_name(32)); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, server_host varchar(191) NOT NULL, opts mediumtext NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_room_name_host USING BTREE ON muc_room(name(75), host(75)); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, server_host varchar(191) NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_registered_nick USING BTREE ON muc_registered(nick(75)); CREATE UNIQUE INDEX i_muc_registered_jid_host USING BTREE ON muc_registered(jid(75), host(75)); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, server_host varchar(191) NOT NULL, node text NOT NULL, pid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_online_room_name_host USING BTREE ON muc_online_room(name(75), host(75)); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, server_host varchar(191) NOT NULL, node text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_muc_online_users USING BTREE ON muc_online_users(username(75), server(75), resource(75), name(75), host(75)); CREATE INDEX i_muc_online_users_us USING BTREE ON muc_online_users(username(75), server(75)); CREATE TABLE muc_room_subscribers ( room varchar(191) NOT NULL, host varchar(191) NOT NULL, jid varchar(191) NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY i_muc_room_subscribers_host_room_jid (host, room, jid) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_muc_room_subscribers_host_jid USING BTREE ON muc_room_subscribers(host, jid); CREATE TABLE motd ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, xml text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host(191), username) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE caps_features ( node varchar(191) NOT NULL, subnode varchar(191) NOT NULL, feature text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_caps_features_node_subnode ON caps_features(node(75), subnode(75)); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, resource varchar(191) NOT NULL, priority text NOT NULL, info text NOT NULL, PRIMARY KEY (usec, pid(75)) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE INDEX i_sm_node ON sm(node(75)); CREATE INDEX i_sm_sh_username ON sm(server_host(191), username); CREATE TABLE oauth_token ( token varchar(191) NOT NULL PRIMARY KEY, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE oauth_client ( client_id varchar(191) NOT NULL PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE route ( domain text NOT NULL, server_host varchar(191) NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_route ON route(domain(75), server_host(75), node(75), pid(75)); CREATE INDEX i_route_domain ON route(domain(75)); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid(75)); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid(191)); CREATE INDEX i_proxy65_jid ON proxy65 (jid_i(191)); CREATE TABLE push_session ( username text NOT NULL, server_host varchar(191) NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL, PRIMARY KEY (server_host(191), username(191), timestamp) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_push_session_susn ON push_session (server_host(191), username(191), service(191), node(191)); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel(191), service(191)); CREATE INDEX i_mix_channel_serv ON mix_channel (service(191)); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel(191), service(191)); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel(153), service(153), username(153), domain(153), node(153)); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel(191), service(191), username(191), domain(191)); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel(191), service(191), node(191)); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel(191), service(191)); CREATE TABLE mix_pam ( username text NOT NULL, server_host varchar(191) NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username(191), server_host(191), channel(191), service(191)); CREATE INDEX i_mix_pam_us ON mix_pam (username(191), server_host(191)); CREATE TABLE mqtt_pub ( username varchar(191) NOT NULL, server_host varchar(191) NOT NULL, resource varchar(191) NOT NULL, topic text NOT NULL, qos tinyint NOT NULL, payload blob NOT NULL, payload_format tinyint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data blob NOT NULL, user_properties blob NOT NULL, expiry int unsigned NOT NULL, UNIQUE KEY i_mqtt_topic_server (topic(191), server_host) ) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/mssql.sql������������������������������������������������������������������������0000644�0002322�0002322�00000057676�14154362354�016336� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- SET ANSI_PADDING OFF; SET ANSI_NULLS ON; SET QUOTED_IDENTIFIER ON; SET ANSI_PADDING ON; CREATE TABLE [dbo].[archive] ( [username] [varchar] (250) NOT NULL, [timestamp] [bigint] NOT NULL, [peer] [varchar] (250) NOT NULL, [bare_peer] [varchar] (250) NOT NULL, [xml] [ntext] NOT NULL, [txt] [ntext] NULL, [id] [bigint] IDENTITY(1,1) NOT NULL, [kind] [varchar] (10) NULL, [nick] [varchar] (250) NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [archive_PK] PRIMARY KEY CLUSTERED ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [archive_username_timestamp] ON [archive] (username, timestamp) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [archive_username_peer] ON [archive] (username, peer) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [archive_username_bare_peer] ON [archive] (username, bare_peer) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [archive_timestamp] ON [archive] (timestamp) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[archive_prefs] ( [username] [varchar] (250) NOT NULL, [def] [text] NOT NULL, [always] [text] NOT NULL, [never] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [archive_prefs_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[caps_features] ( [node] [varchar] (250) NOT NULL, [subnode] [varchar] (250) NOT NULL, [feature] [text] NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ) TEXTIMAGE_ON [PRIMARY]; CREATE CLUSTERED INDEX [caps_features_node_subnode] ON [caps_features] (node, subnode) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[last] ( [username] [varchar] (250) NOT NULL, [seconds] [text] NOT NULL, [state] [text] NOT NULL, CONSTRAINT [last_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[motd] ( [username] [varchar] (250) NOT NULL, [xml] [text] NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [motd_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[muc_registered] ( [jid] [varchar] (255) NOT NULL, [host] [varchar] (255) NOT NULL, [nick] [varchar] (255) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ); CREATE INDEX [muc_registered_nick] ON [muc_registered] (nick) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE CLUSTERED INDEX [muc_registered_jid_host] ON [muc_registered] (jid, host) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[muc_room] ( [name] [varchar] (250) NOT NULL, [host] [varchar] (250) NOT NULL, [opts] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ) TEXTIMAGE_ON [PRIMARY]; CREATE UNIQUE CLUSTERED INDEX [muc_room_name_host] ON [muc_room] (name, host) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[muc_online_room] ( [name] [varchar] (250) NOT NULL, [host] [varchar] (250) NOT NULL, [node] [text] NOT NULL, [pid] [text] NOT NULL ); CREATE UNIQUE CLUSTERED INDEX [muc_online_room_name_host] ON [muc_online_room] (name, host) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[muc_online_users] ( [username] [varchar] (250) NOT NULL, [server] [varchar] (250) NOT NULL, [resource] [varchar] (250) NOT NULL, [name] [varchar] (250) NOT NULL, [host] [varchar] (250) NOT NULL, node text NOT NULL ); CREATE UNIQUE INDEX [muc_online_users_i] ON [muc_online_users] (username, server, resource, name, host) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE CLUSTERED INDEX [muc_online_users_us] ON [muc_online_users] (username, server) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[muc_room_subscribers] ( [room] [varchar] (191) NOT NULL, [host] [varchar] (191) NOT NULL, [jid] [varchar] (191) NOT NULL, [nick] [text] NOT NULL, [nodes] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ); CREATE UNIQUE CLUSTERED INDEX [muc_room_subscribers_host_room_jid] ON [muc_room_subscribers] (host, room, jid); CREATE INDEX [muc_room_subscribers_host_jid] ON [muc_room_subscribers] (host, jid); CREATE TABLE [dbo].[privacy_default_list] ( [username] [varchar] (250) NOT NULL, [name] [varchar] (250) NOT NULL, CONSTRAINT [privacy_default_list_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ); CREATE TABLE [dbo].[privacy_list] ( [username] [varchar] (250) NOT NULL, [name] [varchar] (250) NOT NULL, [id] [bigint] IDENTITY(1,1) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [privacy_list_PK] PRIMARY KEY CLUSTERED ( [id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ); CREATE INDEX [privacy_list_username] ON [privacy_list] (username) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE INDEX [privacy_list_username_name] ON [privacy_list] (username, name) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[privacy_list_data] ( [id] [bigint] NULL, [t] [char] (1) NOT NULL, [value] [text] NOT NULL, [action] [char] (1) NOT NULL, [ord] [smallint] NOT NULL, [match_all] [smallint] NOT NULL, [match_iq] [smallint] NOT NULL, [match_message] [smallint] NOT NULL, [match_presence_in] [smallint] NOT NULL, [match_presence_out] [smallint] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE CLUSTERED INDEX [privacy_list_data_id] ON [privacy_list_data] (id) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[private_storage] ( [username] [varchar] (250) NOT NULL, [namespace] [varchar] (250) NOT NULL, [data] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [private_storage_username] ON [private_storage] (username) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE CLUSTERED INDEX [private_storage_username_namespace] ON [private_storage] (username, namespace) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_item] ( [nodeid] [bigint] NULL, [itemid] [varchar] (255) NOT NULL, [publisher] [varchar] (250) NOT NULL, [creation] [varchar] (32) NOT NULL, [modification] [varchar] (32) NOT NULL, [payload] [text] NOT NULL DEFAULT '' ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [pubsub_item_itemid] ON [pubsub_item] (itemid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE CLUSTERED INDEX [pubsub_item_nodeid_itemid] ON [pubsub_item] (nodeid, itemid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_node_option] ( [nodeid] [bigint] NULL, [name] [text] NOT NULL, [val] [text] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE CLUSTERED INDEX [pubsub_node_option_nodeid] ON [pubsub_node_option] (nodeid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_node_owner] ( [nodeid] [bigint] NULL, [owner] [text] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE CLUSTERED INDEX [pubsub_node_owner_nodeid] ON [pubsub_node_owner] (nodeid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_state] ( [nodeid] [bigint] NULL, [jid] [varchar] (255) NOT NULL, [affiliation] [char] (1) NOT NULL, [subscriptions] [text] NOT NULL DEFAULT '', [stateid] [bigint] IDENTITY(1,1) NOT NULL, CONSTRAINT [pubsub_state_PRIMARY] PRIMARY KEY CLUSTERED ( [stateid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [pubsub_state_jid] ON [pubsub_state] (jid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE INDEX [pubsub_state_nodeid_jid] ON [pubsub_state] (nodeid, jid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_subscription_opt] ( [subid] [varchar] (255) NOT NULL, [opt_name] [varchar] (32) NOT NULL, [opt_value] [text] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE UNIQUE CLUSTERED INDEX [pubsub_subscription_opt_subid_opt_name] ON [pubsub_subscription_opt] (subid, opt_name) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[pubsub_node] ( [host] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [parent] [varchar] (255) NOT NULL DEFAULT '', [plugin] [text] NOT NULL, [nodeid] [bigint] IDENTITY(1,1) NOT NULL, CONSTRAINT [pubsub_node_PRIMARY] PRIMARY KEY CLUSTERED ( [nodeid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [pubsub_node_parent] ON [pubsub_node] (parent) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE INDEX [pubsub_node_host_node] ON [pubsub_node] (host, node) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[roster_version] ( [username] [varchar] (250) NOT NULL, [version] [text] NOT NULL, CONSTRAINT [roster_version_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[rostergroups] ( [username] [varchar] (250) NOT NULL, [jid] [varchar] (250) NOT NULL, [grp] [text] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE CLUSTERED INDEX [rostergroups_username_jid] ON [rostergroups] ([username], [jid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[rosterusers] ( [username] [varchar] (250) NOT NULL, [jid] [varchar] (250) NOT NULL, [nick] [text] NOT NULL, [subscription] [char] (1) NOT NULL, [ask] [char] (1) NOT NULL, [askmessage] [text] NOT NULL, [server] [char] (1) NOT NULL, [subscribe] [text] NOT NULL, [type] [text] NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ) TEXTIMAGE_ON [PRIMARY]; CREATE UNIQUE CLUSTERED INDEX [rosterusers_username_jid] ON [rosterusers] ([username], [jid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [rosterusers_username] ON [rosterusers] ([username]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [rosterusers_jid] ON [rosterusers] ([jid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[sm] ( [usec] [bigint] NOT NULL, [pid] [varchar] (100) NOT NULL, [node] [varchar] (255) NOT NULL, [username] [varchar] (255) NOT NULL, [resource] [varchar] (255) NOT NULL, [priority] [text] NOT NULL, [info] [text] NOT NULL ) TEXTIMAGE_ON [PRIMARY]; CREATE UNIQUE CLUSTERED INDEX [sm_sid] ON [sm] (usec, pid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [sm_node] ON [sm] (node) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [sm_username] ON [sm] (username) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[spool] ( [username] [varchar] (250) NOT NULL, [xml] [text] NOT NULL, [seq] [bigint] IDENTITY(1,1) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [spool_PK] PRIMARY KEY CLUSTERED ( [seq] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [spool_username] ON [spool] (username) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [spool_created_at] ON [spool] (created_at) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ; CREATE TABLE [dbo].[sr_group] ( [name] [varchar] (250) NOT NULL, [opts] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [sr_group_PRIMARY] PRIMARY KEY CLUSTERED ( [name] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[sr_user] ( [jid] [varchar] (250) NOT NULL, [grp] [varchar] (250) NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE() ); CREATE UNIQUE CLUSTERED INDEX [sr_user_jid_group] ON [sr_user] ([jid], [grp]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [sr_user_jid] ON [sr_user] ([jid]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [sr_user_grp] ON [sr_user] ([grp]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[users] ( [username] [varchar] (250) NOT NULL, [password] [text] NOT NULL, [serverkey] [text] NOT NULL DEFAULT '', [salt] [text] NOT NULL DEFAULT '', [iterationcount] [smallint] NOT NULL DEFAULT 0, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [users_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[vcard] ( [username] [varchar] (250) NOT NULL, [vcard] [text] NOT NULL, [created_at] [datetime] NOT NULL DEFAULT GETDATE(), CONSTRAINT [vcard_PRIMARY] PRIMARY KEY CLUSTERED ( [username] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[vcard_search] ( [username] [varchar] (250) NOT NULL, [lusername] [varchar] (250) NOT NULL, [fn] [text] NOT NULL, [lfn] [varchar] (250) NOT NULL, [family] [text] NOT NULL, [lfamily] [varchar] (250) NOT NULL, [given] [text] NOT NULL, [lgiven] [varchar] (250) NOT NULL, [middle] [text] NOT NULL, [lmiddle] [varchar] (250) NOT NULL, [nickname] [text] NOT NULL, [lnickname] [varchar] (250) NOT NULL, [bday] [text] NOT NULL, [lbday] [varchar] (250) NOT NULL, [ctry] [text] NOT NULL, [lctry] [varchar] (250) NOT NULL, [locality] [text] NOT NULL, [llocality] [varchar] (250) NOT NULL, [email] [text] NOT NULL, [lemail] [varchar] (250) NOT NULL, [orgname] [text] NOT NULL, [lorgname] [varchar] (250) NOT NULL, [orgunit] [text] NOT NULL, [lorgunit] [varchar] (250) NOT NULL, CONSTRAINT [vcard_search_PRIMARY] PRIMARY KEY CLUSTERED ( [lusername] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE INDEX [vcard_search_lfn] ON [vcard_search] (lfn) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lfamily] ON [vcard_search] (lfamily) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lgiven] ON [vcard_search] (lgiven) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lmiddle] ON [vcard_search] (lmiddle) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lnickname] ON [vcard_search] (lnickname) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lbday] ON [vcard_search] (lbday) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lctry] ON [vcard_search] (lctry) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_llocality] ON [vcard_search] (llocality) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lemail] ON [vcard_search] (lemail) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lorgname] ON [vcard_search] (lorgname) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [vcard_search_lorgunit] ON [vcard_search] (lorgunit) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); ALTER TABLE [dbo].[pubsub_item] WITH CHECK ADD CONSTRAINT [pubsub_item_ibfk_1] FOREIGN KEY([nodeid]) REFERENCES [dbo].[pubsub_node] ([nodeid]) ON DELETE CASCADE; ALTER TABLE [dbo].[pubsub_item] CHECK CONSTRAINT [pubsub_item_ibfk_1]; ALTER TABLE [dbo].[pubsub_node_option] WITH CHECK ADD CONSTRAINT [pubsub_node_option_ibfk_1] FOREIGN KEY([nodeid]) REFERENCES [dbo].[pubsub_node] ([nodeid]) ON DELETE CASCADE; ALTER TABLE [dbo].[pubsub_node_option] CHECK CONSTRAINT [pubsub_node_option_ibfk_1]; ALTER TABLE [dbo].[pubsub_node_owner] WITH CHECK ADD CONSTRAINT [pubsub_node_owner_ibfk_1] FOREIGN KEY([nodeid]) REFERENCES [dbo].[pubsub_node] ([nodeid]) ON DELETE CASCADE; ALTER TABLE [dbo].[pubsub_node_owner] CHECK CONSTRAINT [pubsub_node_owner_ibfk_1]; ALTER TABLE [dbo].[pubsub_state] WITH CHECK ADD CONSTRAINT [pubsub_state_ibfk_1] FOREIGN KEY([nodeid]) REFERENCES [dbo].[pubsub_node] ([nodeid]) ON DELETE CASCADE; ALTER TABLE [dbo].[pubsub_state] CHECK CONSTRAINT [pubsub_state_ibfk_1]; CREATE TABLE [dbo].[oauth_token] ( [token] [varchar] (250) NOT NULL, [jid] [text] NOT NULL, [scope] [text] NOT NULL, [expire] [bigint] NOT NULL, CONSTRAINT [oauth_token_PRIMARY] PRIMARY KEY CLUSTERED ( [token] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ) TEXTIMAGE_ON [PRIMARY]; CREATE TABLE [dbo].[route] ( [domain] [varchar] (255) NOT NULL, [server_host] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [pid] [varchar](100) NOT NULL, [local_hint] [text] NOT NULL ); CREATE UNIQUE CLUSTERED INDEX [route_i] ON [route] (domain, server_host, node, pid) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE INDEX [route_domain] ON [route] (domain) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[bosh] ( [sid] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [pid] [varchar](100) NOT NULL CONSTRAINT [bosh_PRIMARY] PRIMARY KEY CLUSTERED ( [sid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ); CREATE TABLE [dbo].[push_session] ( [username] [varchar] (255) NOT NULL, [timestamp] [bigint] NOT NULL, [service] [varchar] (255) NOT NULL, [node] [varchar] (255) NOT NULL, [xml] [varchar] (255) NOT NULL ); CREATE UNIQUE CLUSTERED INDEX [i_push_usn] ON [push_session] (username, service, node) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE UNIQUE INDEX [i_push_ut] ON [push_session] (username, timestamp) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON); CREATE TABLE [dbo].[mqtt_pub]( [username] [varchar](191) NOT NULL, [server_host] [varchar](191) NOT NULL, [resource] [varchar](191) NOT NULL, [topic] [varchar](191) NOT NULL, [qos] [tinyint] NOT NULL, [payload] [varbinary](max) NOT NULL, [payload_format] [tinyint] NOT NULL, [content_type] [text] NOT NULL, [response_topic] [text] NOT NULL, [correlation_data] [varbinary](max) NOT NULL, [user_properties] [varbinary](max) NOT NULL, [expiry] [int] NOT NULL, CONSTRAINT [i_mqtt_topic_server] PRIMARY KEY CLUSTERED ( [topic] ASC, [server_host] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]; ������������������������������������������������������������������ejabberd-21.12/sql/pg.sql���������������������������������������������������������������������������0000644�0002322�0002322�00000034316�14154362354�015567� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- CREATE TABLE users ( username text PRIMARY KEY, "password" text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT now() ); -- Add support for SCRAM auth to a database created before ejabberd 16.03: -- ALTER TABLE users ADD COLUMN serverkey text NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN salt text NOT NULL DEFAULT ''; -- ALTER TABLE users ADD COLUMN iterationcount integer NOT NULL DEFAULT 0; CREATE TABLE last ( username text PRIMARY KEY, seconds text NOT NULL, state text NOT NULL ); CREATE TABLE rosterusers ( username text NOT NULL, jid text NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, "type" text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_rosteru_user_jid ON rosterusers USING btree (username, jid); CREATE INDEX i_rosteru_username ON rosterusers USING btree (username); CREATE INDEX i_rosteru_jid ON rosterusers USING btree (jid); CREATE TABLE rostergroups ( username text NOT NULL, jid text NOT NULL, grp text NOT NULL ); CREATE INDEX pk_rosterg_user_jid ON rostergroups USING btree (username, jid); CREATE TABLE sr_group ( name text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_sr_group_name ON sr_group USING btree (name); CREATE TABLE sr_user ( jid text NOT NULL, grp text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_sr_user_jid_grp ON sr_user USING btree (jid, grp); CREATE INDEX i_sr_user_jid ON sr_user USING btree (jid); CREATE INDEX i_sr_user_grp ON sr_user USING btree (grp); CREATE TABLE spool ( username text NOT NULL, xml text NOT NULL, seq SERIAL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_despool ON spool USING btree (username); CREATE TABLE archive ( username text NOT NULL, timestamp BIGINT NOT NULL, peer text NOT NULL, bare_peer text NOT NULL, xml text NOT NULL, txt text, id SERIAL, kind text, nick text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_username_timestamp ON archive USING btree (username, timestamp); CREATE INDEX i_username_peer ON archive USING btree (username, peer); CREATE INDEX i_username_bare_peer ON archive USING btree (username, bare_peer); CREATE INDEX i_timestamp ON archive USING btree (timestamp); CREATE TABLE archive_prefs ( username text NOT NULL PRIMARY KEY, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE vcard ( username text PRIMARY KEY, vcard text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE vcard_search ( username text NOT NULL, lusername text PRIMARY KEY, fn text NOT NULL, lfn text NOT NULL, "family" text NOT NULL, lfamily text NOT NULL, given text NOT NULL, lgiven text NOT NULL, middle text NOT NULL, lmiddle text NOT NULL, nickname text NOT NULL, lnickname text NOT NULL, bday text NOT NULL, lbday text NOT NULL, ctry text NOT NULL, lctry text NOT NULL, locality text NOT NULL, llocality text NOT NULL, email text NOT NULL, lemail text NOT NULL, orgname text NOT NULL, lorgname text NOT NULL, orgunit text NOT NULL, lorgunit text NOT NULL ); CREATE INDEX i_vcard_search_lfn ON vcard_search(lfn); CREATE INDEX i_vcard_search_lfamily ON vcard_search(lfamily); CREATE INDEX i_vcard_search_lgiven ON vcard_search(lgiven); CREATE INDEX i_vcard_search_lmiddle ON vcard_search(lmiddle); CREATE INDEX i_vcard_search_lnickname ON vcard_search(lnickname); CREATE INDEX i_vcard_search_lbday ON vcard_search(lbday); CREATE INDEX i_vcard_search_lctry ON vcard_search(lctry); CREATE INDEX i_vcard_search_llocality ON vcard_search(llocality); CREATE INDEX i_vcard_search_lemail ON vcard_search(lemail); CREATE INDEX i_vcard_search_lorgname ON vcard_search(lorgname); CREATE INDEX i_vcard_search_lorgunit ON vcard_search(lorgunit); CREATE TABLE privacy_default_list ( username text PRIMARY KEY, name text NOT NULL ); CREATE TABLE privacy_list ( username text NOT NULL, name text NOT NULL, id SERIAL UNIQUE, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_privacy_list_username ON privacy_list USING btree (username); CREATE UNIQUE INDEX i_privacy_list_username_name ON privacy_list USING btree (username, name); CREATE TABLE privacy_list_data ( id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ); CREATE INDEX i_privacy_list_data_id ON privacy_list_data USING btree (id); CREATE TABLE private_storage ( username text NOT NULL, namespace text NOT NULL, data text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_private_storage_username ON private_storage USING btree (username); CREATE UNIQUE INDEX i_private_storage_username_namespace ON private_storage USING btree (username, namespace); CREATE TABLE roster_version ( username text PRIMARY KEY, version text NOT NULL ); -- To update from 0.9.8: -- CREATE SEQUENCE spool_seq_seq; -- ALTER TABLE spool ADD COLUMN seq integer; -- ALTER TABLE spool ALTER COLUMN seq SET DEFAULT nextval('spool_seq_seq'); -- UPDATE spool SET seq = DEFAULT; -- ALTER TABLE spool ALTER COLUMN seq SET NOT NULL; -- To update from 1.x: -- ALTER TABLE rosterusers ADD COLUMN askmessage text; -- UPDATE rosterusers SET askmessage = ''; -- ALTER TABLE rosterusers ALTER COLUMN askmessage SET NOT NULL; CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, nodeid SERIAL UNIQUE ); CREATE INDEX i_pubsub_node_parent ON pubsub_node USING btree (parent); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node USING btree (host, node); CREATE TABLE pubsub_node_option ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, name text NOT NULL, val text NOT NULL ); CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option USING btree (nodeid); CREATE TABLE pubsub_node_owner ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, owner text NOT NULL ); CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner USING btree (nodeid); CREATE TABLE pubsub_state ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', stateid SERIAL UNIQUE ); CREATE INDEX i_pubsub_state_jid ON pubsub_state USING btree (jid); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state USING btree (nodeid, jid); CREATE TABLE pubsub_item ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload text NOT NULL DEFAULT '' ); CREATE INDEX i_pubsub_item_itemid ON pubsub_item USING btree (itemid); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item USING btree (nodeid, itemid); CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ); CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt USING btree (subid, opt_name); CREATE TABLE muc_room ( name text NOT NULL, host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room USING btree (name, host); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, nick text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_muc_registered_nick ON muc_registered USING btree (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered USING btree (jid, host); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room USING btree (name, host); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, node text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users USING btree (username, server, resource, name, host); CREATE INDEX i_muc_online_users_us ON muc_online_users USING btree (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, host text NOT NULL, jid text NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers USING btree (host, jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers USING btree (host, room, jid); CREATE TABLE motd ( username text PRIMARY KEY, xml text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE TABLE caps_features ( node text NOT NULL, subnode text NOT NULL, feature text, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX i_caps_features_node_subnode ON caps_features USING btree (node, subnode); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username text NOT NULL, resource text NOT NULL, priority text NOT NULL, info text NOT NULL ); CREATE UNIQUE INDEX i_sm_sid ON sm USING btree (usec, pid); CREATE INDEX i_sm_node ON sm USING btree (node); CREATE INDEX i_sm_username ON sm USING btree (username); CREATE TABLE oauth_token ( token text NOT NULL, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ); CREATE UNIQUE INDEX i_oauth_token_token ON oauth_token USING btree (token); CREATE TABLE oauth_client ( client_id text PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ); CREATE TABLE route ( domain text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ); CREATE UNIQUE INDEX i_route ON route USING btree (domain, server_host, node, pid); CREATE INDEX i_route_domain ON route USING btree (domain); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_bosh_sid ON bosh USING btree (sid); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ); CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 USING btree (sid); CREATE INDEX i_proxy65_jid ON proxy65 USING btree (jid_i); CREATE TABLE push_session ( username text NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL ); CREATE UNIQUE INDEX i_push_usn ON push_session USING btree (username, service, node); CREATE UNIQUE INDEX i_push_ut ON push_session USING btree (username, timestamp); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); CREATE INDEX i_mix_channel_serv ON mix_channel (service); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, channel, service); CREATE INDEX i_mix_pam_us ON mix_pam (username); CREATE TABLE mqtt_pub ( username text NOT NULL, resource text NOT NULL, topic text NOT NULL, qos smallint NOT NULL, payload bytea NOT NULL, payload_format smallint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data bytea NOT NULL, user_properties bytea NOT NULL, expiry bigint NOT NULL ); CREATE UNIQUE INDEX i_mqtt_topic ON mqtt_pub (topic); ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/sql/lite.new.sql���������������������������������������������������������������������0000644�0002322�0002322�00000035233�14154362354�016705� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-- -- ejabberd, Copyright (C) 2002-2021 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. -- CREATE TABLE users ( username text NOT NULL, server_host text NOT NULL, password text NOT NULL, serverkey text NOT NULL DEFAULT '', salt text NOT NULL DEFAULT '', iterationcount integer NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, username) ); CREATE TABLE last ( username text NOT NULL, server_host text NOT NULL, seconds text NOT NULL, state text NOT NULL, PRIMARY KEY (server_host, username) ); CREATE TABLE rosterusers ( username text NOT NULL, server_host text NOT NULL, jid text NOT NULL, nick text NOT NULL, subscription character(1) NOT NULL, ask character(1) NOT NULL, askmessage text NOT NULL, server character(1) NOT NULL, subscribe text NOT NULL, type text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_rosteru_sh_user_jid ON rosterusers (server_host, username, jid); CREATE INDEX i_rosteru_sh_username ON rosterusers (server_host, username); CREATE INDEX i_rosteru_sh_jid ON rosterusers (server_host, jid); CREATE TABLE rostergroups ( username text NOT NULL, server_host text NOT NULL, jid text NOT NULL, grp text NOT NULL ); CREATE INDEX i_rosterg_sh_user_jid ON rostergroups (server_host, username, jid); CREATE TABLE sr_group ( name text NOT NULL, server_host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, name) ); CREATE UNIQUE INDEX i_sr_group_sh_name ON sr_group (server_host, name); CREATE TABLE sr_user ( jid text NOT NULL, server_host text NOT NULL, grp text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, jid, grp) ); CREATE UNIQUE INDEX i_sr_user_sh_jid_grp ON sr_user (server_host, jid, grp); CREATE INDEX i_sr_user_sh_jid ON sr_user (server_host, jid); CREATE INDEX i_sr_user_sh_grp ON sr_user (server_host, grp); CREATE TABLE spool ( username text NOT NULL, server_host text NOT NULL, xml text NOT NULL, seq INTEGER PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_spool_sh_username ON spool (server_host, username); CREATE TABLE archive ( username text NOT NULL, server_host text NOT NULL, timestamp BIGINT UNSIGNED NOT NULL, peer text NOT NULL, bare_peer text NOT NULL, xml text NOT NULL, txt text, id INTEGER PRIMARY KEY AUTOINCREMENT, kind text, nick text, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_archive_sh_username_timestamp ON archive (server_host, username, timestamp); CREATE INDEX i_archive_sh_username_peer ON archive (server_host, username, peer); CREATE INDEX i_archive_sh_username_bare_peer ON archive (server_host, username, bare_peer); CREATE INDEX i_archive_sh_timestamp ON archive (server_host, timestamp); CREATE TABLE archive_prefs ( username text NOT NULL, server_host text NOT NULL, def text NOT NULL, always text NOT NULL, never text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, username) ); CREATE TABLE vcard ( username text NOT NULL, server_host text NOT NULL, vcard text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, username) ); CREATE TABLE vcard_search ( username text NOT NULL, lusername text NOT NULL, server_host text NOT NULL, fn text NOT NULL, lfn text NOT NULL, family text NOT NULL, lfamily text NOT NULL, given text NOT NULL, lgiven text NOT NULL, middle text NOT NULL, lmiddle text NOT NULL, nickname text NOT NULL, lnickname text NOT NULL, bday text NOT NULL, lbday text NOT NULL, ctry text NOT NULL, lctry text NOT NULL, locality text NOT NULL, llocality text NOT NULL, email text NOT NULL, lemail text NOT NULL, orgname text NOT NULL, lorgname text NOT NULL, orgunit text NOT NULL, lorgunit text NOT NULL, PRIMARY KEY (server_host, lusername) ); CREATE INDEX i_vcard_search_sh_lfn ON vcard_search(server_host, lfn); CREATE INDEX i_vcard_search_sh_lfamily ON vcard_search(server_host, lfamily); CREATE INDEX i_vcard_search_sh_lgiven ON vcard_search(server_host, lgiven); CREATE INDEX i_vcard_search_sh_lmiddle ON vcard_search(server_host, lmiddle); CREATE INDEX i_vcard_search_sh_lnickname ON vcard_search(server_host, lnickname); CREATE INDEX i_vcard_search_sh_lbday ON vcard_search(server_host, lbday); CREATE INDEX i_vcard_search_sh_lctry ON vcard_search(server_host, lctry); CREATE INDEX i_vcard_search_sh_llocality ON vcard_search(server_host, llocality); CREATE INDEX i_vcard_search_sh_lemail ON vcard_search(server_host, lemail); CREATE INDEX i_vcard_search_sh_lorgname ON vcard_search(server_host, lorgname); CREATE INDEX i_vcard_search_sh_lorgunit ON vcard_search(server_host, lorgunit); CREATE TABLE privacy_default_list ( username text NOT NULL, server_host text NOT NULL, name text NOT NULL, PRIMARY KEY (server_host, username) ); CREATE TABLE privacy_list ( username text NOT NULL, server_host text NOT NULL, name text NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_privacy_list_sh_username ON privacy_list (server_host, username); CREATE UNIQUE INDEX i_privacy_list_sh_username_name ON privacy_list (server_host, username, name); CREATE TABLE privacy_list_data ( id bigint REFERENCES privacy_list(id) ON DELETE CASCADE, t character(1) NOT NULL, value text NOT NULL, action character(1) NOT NULL, ord NUMERIC NOT NULL, match_all boolean NOT NULL, match_iq boolean NOT NULL, match_message boolean NOT NULL, match_presence_in boolean NOT NULL, match_presence_out boolean NOT NULL ); CREATE TABLE private_storage ( username text NOT NULL, server_host text NOT NULL, namespace text NOT NULL, data text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, username, namespace) ); CREATE INDEX i_private_storage_sh_username ON private_storage (server_host, username); CREATE TABLE roster_version ( username text NOT NULL, server_host text NOT NULL, version text NOT NULL, PRIMARY KEY (server_host, username) ); CREATE TABLE pubsub_node ( host text NOT NULL, node text NOT NULL, parent text NOT NULL DEFAULT '', plugin text NOT NULL, nodeid INTEGER PRIMARY KEY AUTOINCREMENT ); CREATE INDEX i_pubsub_node_parent ON pubsub_node (parent); CREATE UNIQUE INDEX i_pubsub_node_tuple ON pubsub_node (host, node); CREATE TABLE pubsub_node_option ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, name text NOT NULL, val text NOT NULL ); CREATE INDEX i_pubsub_node_option_nodeid ON pubsub_node_option (nodeid); CREATE TABLE pubsub_node_owner ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, owner text NOT NULL ); CREATE INDEX i_pubsub_node_owner_nodeid ON pubsub_node_owner (nodeid); CREATE TABLE pubsub_state ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, jid text NOT NULL, affiliation character(1), subscriptions text NOT NULL DEFAULT '', stateid INTEGER PRIMARY KEY AUTOINCREMENT ); CREATE INDEX i_pubsub_state_jid ON pubsub_state (jid); CREATE UNIQUE INDEX i_pubsub_state_tuple ON pubsub_state (nodeid, jid); CREATE TABLE pubsub_item ( nodeid bigint REFERENCES pubsub_node(nodeid) ON DELETE CASCADE, itemid text NOT NULL, publisher text NOT NULL, creation varchar(32) NOT NULL, modification varchar(32) NOT NULL, payload text NOT NULL DEFAULT '' ); CREATE INDEX i_pubsub_item_itemid ON pubsub_item (itemid); CREATE UNIQUE INDEX i_pubsub_item_tuple ON pubsub_item (nodeid, itemid); CREATE TABLE pubsub_subscription_opt ( subid text NOT NULL, opt_name varchar(32), opt_value text NOT NULL ); CREATE UNIQUE INDEX i_pubsub_subscription_opt ON pubsub_subscription_opt (subid, opt_name); CREATE TABLE muc_room ( name text NOT NULL, server_host text NOT NULL, host text NOT NULL, opts text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_muc_room_name_host ON muc_room (name, host); CREATE TABLE muc_registered ( jid text NOT NULL, host text NOT NULL, server_host text NOT NULL, nick text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_muc_registered_nick ON muc_registered (nick); CREATE UNIQUE INDEX i_muc_registered_jid_host ON muc_registered (jid, host); CREATE TABLE muc_online_room ( name text NOT NULL, host text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_room_name_host ON muc_online_room (name, host); CREATE TABLE muc_online_users ( username text NOT NULL, server text NOT NULL, resource text NOT NULL, name text NOT NULL, host text NOT NULL, server_host text NOT NULL, node text NOT NULL ); CREATE UNIQUE INDEX i_muc_online_users ON muc_online_users (username, server, resource, name, host); CREATE INDEX i_muc_online_users_us ON muc_online_users (username, server); CREATE TABLE muc_room_subscribers ( room text NOT NULL, host text NOT NULL, jid text NOT NULL, nick text NOT NULL, nodes text NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_muc_room_subscribers_host_jid ON muc_room_subscribers(host, jid); CREATE UNIQUE INDEX i_muc_room_subscribers_host_room_jid ON muc_room_subscribers(host, room, jid); CREATE TABLE motd ( username text NOT NULL, server_host text NOT NULL, xml text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (server_host, username) ); CREATE TABLE caps_features ( node text NOT NULL, subnode text NOT NULL, feature text, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX i_caps_features_node_subnode ON caps_features (node, subnode); CREATE TABLE sm ( usec bigint NOT NULL, pid text NOT NULL, node text NOT NULL, username text NOT NULL, server_host text NOT NULL, resource text NOT NULL, priority text NOT NULL, info text NOT NULL, PRIMARY KEY (usec, pid) ); CREATE INDEX i_sm_node ON sm(node); CREATE INDEX i_sm_sh_username ON sm (server_host, username); CREATE TABLE oauth_token ( token text NOT NULL PRIMARY KEY, jid text NOT NULL, scope text NOT NULL, expire bigint NOT NULL ); CREATE TABLE oauth_client ( client_id text PRIMARY KEY, client_name text NOT NULL, grant_type text NOT NULL, options text NOT NULL ); CREATE TABLE route ( domain text NOT NULL, server_host text NOT NULL, node text NOT NULL, pid text NOT NULL, local_hint text NOT NULL ); CREATE UNIQUE INDEX i_route ON route(domain, server_host, node, pid); CREATE INDEX i_route_domain ON route(domain); CREATE TABLE bosh ( sid text NOT NULL, node text NOT NULL, pid text NOT NULL ); CREATE UNIQUE INDEX i_bosh_sid ON bosh(sid); CREATE TABLE proxy65 ( sid text NOT NULL, pid_t text NOT NULL, pid_i text NOT NULL, node_t text NOT NULL, node_i text NOT NULL, jid_i text NOT NULL ); CREATE UNIQUE INDEX i_proxy65_sid ON proxy65 (sid); CREATE INDEX i_proxy65_jid ON proxy65 (jid_i); CREATE TABLE push_session ( username text NOT NULL, server_host text NOT NULL, timestamp bigint NOT NULL, service text NOT NULL, node text NOT NULL, xml text NOT NULL, PRIMARY KEY (server_host, username, timestamp) ); CREATE UNIQUE INDEX i_push_session_susn ON push_session (server_host, username, service, node); CREATE TABLE mix_channel ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, hidden boolean NOT NULL, hmac_key text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_channel ON mix_channel (channel, service); CREATE INDEX i_mix_channel_serv ON mix_channel (service); CREATE TABLE mix_participant ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, jid text NOT NULL, id text NOT NULL, nick text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_participant ON mix_participant (channel, service, username, domain); CREATE INDEX i_mix_participant_chan_serv ON mix_participant (channel, service); CREATE TABLE mix_subscription ( channel text NOT NULL, service text NOT NULL, username text NOT NULL, domain text NOT NULL, node text NOT NULL, jid text NOT NULL ); CREATE UNIQUE INDEX i_mix_subscription ON mix_subscription (channel, service, username, domain, node); CREATE INDEX i_mix_subscription_chan_serv_ud ON mix_subscription (channel, service, username, domain); CREATE INDEX i_mix_subscription_chan_serv_node ON mix_subscription (channel, service, node); CREATE INDEX i_mix_subscription_chan_serv ON mix_subscription (channel, service); CREATE TABLE mix_pam ( username text NOT NULL, server_host text NOT NULL, channel text NOT NULL, service text NOT NULL, id text NOT NULL, created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX i_mix_pam ON mix_pam (username, server_host, channel, service); CREATE INDEX i_mix_pam_us ON mix_pam (username, server_host); CREATE TABLE mqtt_pub ( username text NOT NULL, server_host text NOT NULL, resource text NOT NULL, topic text NOT NULL, qos smallint NOT NULL, payload blob NOT NULL, payload_format smallint NOT NULL, content_type text NOT NULL, response_topic text NOT NULL, correlation_data blob NOT NULL, user_properties blob NOT NULL, expiry bigint NOT NULL ); CREATE UNIQUE INDEX i_mqtt_topic_server ON mqtt_pub (topic, server_host); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/configure.ac�������������������������������������������������������������������������0000644�0002322�0002322�00000023774�14154362354�016135� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ(2.53) AC_INIT(ejabberd, m4_esyscmd([echo `git describe --tags 2>/dev/null || echo 0.0` | sed 's/-g.*//;s/-/./' | tr -d '\012']), [ejabberd@process-one.net], [ejabberd]) REQUIRE_ERLANG_MIN="8.3 (Erlang/OTP 19.3)" REQUIRE_ERLANG_MAX="100.0.0 (No Max)" AC_CONFIG_MACRO_DIR([m4]) # Checks for programs. AC_PROG_MAKE_SET AC_PROG_INSTALL AC_PROG_SED if test "x$GCC" = "xyes"; then CFLAGS="$CFLAGS -Wall" fi # Checks Erlang runtime and compiler AC_ARG_WITH(erlang, AC_HELP_STRING([--with-erlang=dir], [search for erlang in dir]), [if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_erlang" = "X"; then extra_erl_path="" else extra_erl_path="$with_erlang:$with_erlang/bin:" fi ]) AC_ARG_WITH(rebar, AC_HELP_STRING([--with-rebar=bin], [use the rebar/rebar3/mix binary specified]), [if test "$withval" = "yes" -o "$withval" = "no" -o "X$with_rebar" = "X"; then rebar="rebar" else rebar="$with_rebar" fi ], [rebar="rebar"]) AC_PATH_TOOL(ERL, erl, , [${extra_erl_path}$PATH]) AC_PATH_TOOL(ERLC, erlc, , [${extra_erl_path}$PATH]) AC_PATH_TOOL(EPMD, epmd, , [${extra_erl_path}$PATH]) AC_ERLANG_NEED_ERL AC_ERLANG_NEED_ERLC # Checks and sets ERLANG_ROOT_DIR and ERLANG_LIB_DIR variable AC_ERLANG_SUBST_ROOT_DIR # AC_ERLANG_SUBST_LIB_DIR #locating escript AC_PATH_PROG([ESCRIPT], [escript], [], [$ERLANG_ROOT_DIR/bin]) #locating make AC_CHECK_PROG([MAKE], [make], [make], []) if test "x$ESCRIPT" = "x"; then AC_MSG_ERROR(['escript' was not found]) fi if test "x$MAKE" = "x"; then AC_MSG_ERROR(['make' was not found]) fi # Change default prefix AC_PREFIX_DEFAULT(/usr/local) AC_CONFIG_FILES([Makefile vars.config]) AC_ARG_ENABLE(all, [AC_HELP_STRING([--enable-all], [same as --enable-odbc --enable-mysql --enable-pgsql --enable-sqlite --enable-pam --enable-zlib --enable-redis --enable-elixir --enable-stun --enable-sip --enable-debug --enable-lua --enable-tools (useful for Dialyzer checks, default: no)])], [case "${enableval}" in yes) odbc=true mysql=true pgsql=true sqlite=true pam=true zlib=true redis=true elixir=true stun=true sip=true debug=true lua=true tools=true ;; no) odbc=false mysql=false pgsql=false sqlite=false pam=false zlib=false redis=false elixir=false stun=false sip=false debug=false lua=false tools=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-all) ;; esac],[]) AC_ARG_ENABLE(debug, [AC_HELP_STRING([--enable-debug], [enable debug information (default: yes)])], [case "${enableval}" in yes) debug=true ;; no) debug=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-debug) ;; esac],[if test "x$debug" = "x"; then debug=true; fi]) AC_ARG_ENABLE(elixir, [AC_HELP_STRING([--enable-elixir], [enable Elixir support (default: no)])], [case "${enableval}" in yes) elixir=true ;; no) elixir=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-elixir) ;; esac],[if test "x$elixir" = "x"; then elixir=false; fi]) AC_ARG_ENABLE(erlang-version-check, [AC_HELP_STRING([--enable-erlang-version-check], [Check Erlang/OTP version (default: yes)])]) case "$enable_erlang_version_check" in yes|'') ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX]) ;; no) ERLANG_VERSION_CHECK([$REQUIRE_ERLANG_MIN],[$REQUIRE_ERLANG_MAX],[warn]) ;; esac AC_ARG_ENABLE(full_xml, [AC_HELP_STRING([--enable-full-xml], [use XML features in XMPP stream (ex: CDATA) (default: no, requires XML compliant clients)])], [case "${enableval}" in yes) full_xml=true ;; no) full_xml=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-full-xml) ;; esac],[full_xml=false]) ENABLEGROUP="" AC_ARG_ENABLE(group, [AS_HELP_STRING([--enable-group[[[[=GROUP]]]]], [allow this system group to start ejabberd (default: no)])], [case "${enableval}" in yes) ENABLEGROUP=`groups |head -n 1` ;; no) ENABLEGROUP="" ;; *) ENABLEGROUP=$enableval esac], []) if test "$ENABLEGROUP" != ""; then echo "allow this system group to start ejabberd: $ENABLEGROUP" AC_SUBST([INSTALLGROUP], [$ENABLEGROUP]) fi AC_ARG_ENABLE(latest_deps, [AC_HELP_STRING([--enable-latest-deps], [makes rebar use latest commits for dependencies instead of tagged versions (default: no)])], [case "${enableval}" in yes) latest_deps=true ;; no) latest_deps=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-latest-deps) ;; esac],[if test "x$latest_deps" = "x"; then latest_deps=false; fi]) AC_ARG_ENABLE(lua, [AC_HELP_STRING([--enable-lua], [enable Lua support, to import from Prosody (default: no)])], [case "${enableval}" in yes) lua=true ;; no) lua=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-lua) ;; esac],[if test "x$lua" = "x"; then lua=false; fi]) AC_ARG_ENABLE(mssql, [AC_HELP_STRING([--enable-mssql], [use Microsoft SQL Server database (default: no, requires --enable-odbc)])], [case "${enableval}" in yes) db_type=mssql; mssql=true ;; no) db_type=generic; mssql=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-mssql) ;; esac],[db_type=generic]) AC_ARG_ENABLE(mysql, [AC_HELP_STRING([--enable-mysql], [enable MySQL support (default: no)])], [case "${enableval}" in yes) mysql=true ;; no) mysql=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-mysql) ;; esac],[if test "x$mysql" = "x"; then mysql=false; fi]) AC_ARG_ENABLE(new_sql_schema, [AC_HELP_STRING([--enable-new-sql-schema], [use new SQL schema (default: no)])], [case "${enableval}" in yes) new_sql_schema=true ;; no) new_sql_schema=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-new-sql-schema) ;; esac],[new_sql_schema=false]) AC_ARG_ENABLE(odbc, [AC_HELP_STRING([--enable-odbc], [enable pure ODBC support (default: no)])], [case "${enableval}" in yes) odbc=true ;; no) odbc=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-odbc) ;; esac],[if test "x$odbc" = "x"; then odbc=false; fi]) AC_ARG_ENABLE(pam, [AC_HELP_STRING([--enable-pam], [enable PAM support (default: no)])], [case "${enableval}" in yes) pam=true ;; no) pam=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-pam) ;; esac],[if test "x$pam" = "x"; then pam=false; fi]) AC_ARG_ENABLE(pgsql, [AC_HELP_STRING([--enable-pgsql], [enable PostgreSQL support (default: no)])], [case "${enableval}" in yes) pgsql=true ;; no) pgsql=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-pgsql) ;; esac],[if test "x$pgsql" = "x"; then pgsql=false; fi]) AC_ARG_ENABLE(redis, [AC_HELP_STRING([--enable-redis], [enable Redis support (default: no)])], [case "${enableval}" in yes) redis=true ;; no) redis=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-redis) ;; esac],[if test "x$redis" = "x"; then redis=false; fi]) AC_ARG_ENABLE(roster_gateway_workaround, [AC_HELP_STRING([--enable-roster-gateway-workaround], [turn on workaround for processing gateway subscriptions (default: no)])], [case "${enableval}" in yes) roster_gateway_workaround=true ;; no) roster_gateway_workaround=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-roster-gateway-workaround) ;; esac],[roster_gateway_workaround=false]) AC_ARG_ENABLE(sip, [AC_HELP_STRING([--enable-sip], [enable SIP support (default: no)])], [case "${enableval}" in yes) sip=true ;; no) sip=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-sip) ;; esac],[if test "x$sip" = "x"; then sip=false; fi]) AC_ARG_ENABLE(sqlite, [AC_HELP_STRING([--enable-sqlite], [enable SQLite support (default: no)])], [case "${enableval}" in yes) sqlite=true ;; no) sqlite=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-sqlite) ;; esac],[if test "x$sqlite" = "x"; then sqlite=false; fi]) AC_ARG_ENABLE(stun, [AC_HELP_STRING([--enable-stun], [enable STUN/TURN support (default: yes)])], [case "${enableval}" in yes) stun=true ;; no) stun=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-stun) ;; esac],[if test "x$stun" = "x"; then stun=true; fi]) AC_ARG_ENABLE(system_deps, [AC_HELP_STRING([--enable-system-deps], [makes rebar use locally installed dependencies instead of downloading them (default: no)])], [case "${enableval}" in yes) system_deps=true ;; no) system_deps=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-system-deps) ;; esac],[if test "x$system_deps" = "x"; then system_deps=false; fi]) AC_ARG_ENABLE(tools, [AC_HELP_STRING([--enable-tools], [build development tools (default: no)])], [case "${enableval}" in yes) tools=true ;; no) tools=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-tools) ;; esac],[if test "x$tools" = "x"; then tools=false; fi]) ENABLEUSER="" AC_ARG_ENABLE(user, [AS_HELP_STRING([--enable-user[[[[=USER]]]]], [allow this system user to start ejabberd (default: no)])], [case "${enableval}" in yes) ENABLEUSER=`whoami` ;; no) ENABLEUSER="" ;; *) ENABLEUSER=$enableval esac], []) if test "$ENABLEUSER" != ""; then echo "allow this system user to start ejabberd: $ENABLEUSER" AC_SUBST([INSTALLUSER], [$ENABLEUSER]) fi AC_ARG_ENABLE(zlib, [AC_HELP_STRING([--enable-zlib], [enable Stream Compression (XEP-0138) using zlib (default: yes)])], [case "${enableval}" in yes) zlib=true ;; no) zlib=false ;; *) AC_MSG_ERROR(bad value ${enableval} for --enable-zlib) ;; esac],[if test "x$zlib" = "x"; then zlib=true; fi]) case "`uname`" in "Darwin") # Darwin (macos) erlang-sqlite is built using amalgamated lib, so no external dependency ;; *) if test "$sqlite" = "true"; then AX_LIB_SQLITE3([3.6.19]) if test "x$SQLITE3_VERSION" = "x"; then AC_MSG_ERROR(SQLite3 library >= 3.6.19 was not found) fi fi ;; esac AC_SUBST(roster_gateway_workaround) AC_SUBST(new_sql_schema) AC_SUBST(full_xml) AC_SUBST(db_type) AC_SUBST(odbc) AC_SUBST(mysql) AC_SUBST(pgsql) AC_SUBST(sqlite) AC_SUBST(pam) AC_SUBST(zlib) AC_SUBST(rebar) AC_SUBST(redis) AC_SUBST(elixir) AC_SUBST(stun) AC_SUBST(sip) AC_SUBST(debug) AC_SUBST(lua) AC_SUBST(tools) AC_SUBST(latest_deps) AC_SUBST(system_deps) AC_SUBST(CFLAGS) AC_SUBST(CPPFLAGS) AC_SUBST(LDFLAGS) AC_OUTPUT ����ejabberd-21.12/autogen.sh���������������������������������������������������������������������������0000755�0002322�0002322�00000000064�14154362354�015633� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# generate a new autoconf aclocal -I m4 autoconf -f ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/��������������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�014612� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/lua/����������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015373� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/lua/redis_sm.lua����������������������������������������������������������������0000644�0002322�0002322�00000001055�14154362354�017704� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������redis.replicate_commands() local cursor = redis.call('GET', KEYS[3]) or 0 local scan_result = redis.call('HSCAN', KEYS[1], cursor, 'COUNT', ARGV[1]) local newcursor = scan_result[1] local cursor = redis.call('SET', KEYS[3], newcursor) redis.call('EXPIRE', KEYS[3], 30) for key,value in ipairs(scan_result[2]) do local uskey, sidkey = string.match(value, '(.*)||(.*)') if uskey and sidkey then redis.call('HDEL', uskey, sidkey) redis.call('HDEL', KEYS[1], value) else redis.call('HDEL', KEYS[2], value) end end return newcursor �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/����������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015366� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/valid-xhtml10.png�����������������������������������������������������������0000644�0002322�0002322�00000004556�14154362354�020500� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���X�����#D��PLTE���{9ތBBcZZބB{{ޥ{޵RssޭRZs{Rc19Jc��猭RZ)J!{{{ZZJs9JZRJsssJ�k1!��c1kkkBs9ƜJccck1c1kcRk99J֜�kk��ƥZε9999R){9{c1s9111J!�1{J�k1�){)))ZֵBƽZ9))csZΥZkcRcR)cJ)�9c�1{9Zs1Z{skZB{BJJsBsBBJJ֌kB9skR)!sZ1kB1scRkZRB!ΥRkR)ZcsRcZZZZ9BJZ)ZJRRR!Z�J1kk)�BJ{JJJ{ֵccZR֭cBBBޭc91c޵Z{sR1){J9{kR1!{ZJ!֭R)!sZ)ZZB!c֥R{RcckR!sZsZRJ!޽9sJRRJ9kRJJ!R޵c�B?���tRNSlIt y]�Dq>`��IDATxڕV_E˻aFڊQY DTrDiPP"|94B":$4G):[F~3w~=fs@Ŵ{"Ք5CYeF$WPVFt'1|NcDc>1Y Õ-{~BI`[%[#I$H%aD|D cO(Loau( )y*aҺ)}*`&lf0wvӿfXmM>Dزn9kZ2ps!acf&Ia:Tqsr=4On XL|Ԥjar|c9G+@yuJ\#1twA#G4H_5A =xb9*t<l>5o{x5I,M*: AN�(Y$Im9Hbmi feNBK~nfk~[pcD16)@hi)M?R&ȅHY)m4ZdTt#4~"T%.eVv%_ȥXd^.ЪhlzfRJdds*tYF\8LhX:֝U" _7e[ʆigJ;ܽqg7:AQOzr`7PaaaL፞g4@> n;4h[l``'N !Ҩ ύWO<s]9`ro:-Ų~sr;t;@7?܌ o}YU5C+m nf�O_SVQI'f_텇V/*<JUEST힩*pQxrxN,)-؜XLqa|9)%HR?$lM0Π<kr2Bj!zO-1 V1mY qtG'^/'܌Z0Q]åGXCТd]L.U`e}B{zzR"+1!װkq+(%Y[e.ܚP)Xg*5?XNmut'vy#/JYH˕$*.^ׄ yǫKKm;} yW4G=+;<8#-ƬWTXSRaeUae[osOTeѸ [{T<th5sS]cRXf5aIy< 6Ѡ#AZbIb>*oхG`f3 ;y_+'H6ssGAoѪh9A q>g<rzN ]68 Dž. /;<h$<[.+fah#M!Hc !P9DAaMɪ;�����IENDB`��������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/vcss.png��������������������������������������������������������������������0000644�0002322�0002322�00000002156�14154362354�017056� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���X������T��)PLTE���## GYUJvYތE dg}}}}Qmmm߷>!]]]$\UH\UHOOOݲjT|?<5����ΧQK$s]K$^KNK@NK@̸OOQD<,$^n Ep__>lz^}e1޴X 6dmX+&YϒL>!U<1LLLWSP<<<9$ D//,,,ME͝>fE)s r8 L���ctRNS�0� 00z��IDATx}S@j RDEAROĈQ'p.eL+rl/_{vf?pMrx://ǼbDQ`Rg FDۊEsT89@&W~ѧ4t�R�̨|D^NV3h|{/oc@TGliY<jm ͏ 12B4kn${{߀#ӰNvy WTd(kNzn .}"i8ePD` C1q6.xvu5rOI ͐]JF?t4`u$P:)\Um_ -@#e0uA,5zIpf3^O XW&8~sl_e]-hIz#@(-G'j%ܽ.3Cߚ0,)lQVU[fhx˥Q yFUVWJ'[ Dr!v õ]ƆϞ.S'L /si\pJ@7:mA0<="3=?WToq  rxw~[ 3h4ٟy {bN韓cnx}n yrܛ>>^a'����IENDB`������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/bosh-logo.png���������������������������������������������������������������0000644�0002322�0002322�00000012757�14154362354�020001� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��,���@���<J��IDATxyxT݀; ȢVAe*UZPjEpi`Q(|*T@XQ "V@,# {AHB$d~c9w3gy!ν瞹W�ĹP2r'@$$5嘏@i }wZ0+mJ#|4]Nut3dP'%P78ZWU3x[$I\2. 9oƨS"q$�XDɃ6Y%IP#39!|E�U$f=KT5O0:�ڋ}đ[b2g85} *-Cq>d-O`ۓ,X)Ddj !# TS Pukbe b]䄄KdSЍT<qk6j @&48i7b?䅉*TeL8RMJ+@#q1r@|Jl?S?Xz?slc%$ ։�n$loSI:`%U4U`% V2XW V2X`UK&tK]${˒2,AV]G-^ ڽX!VW,\84//;meb;v|Ufo۱?iqե!qwrh(vb+97XLkEUk.`r}/t6-1M<Xb:X+'_.X+~,}W8_\.U*êӀY<\u5M~qX#^=x'#qxKCxw&)n;byz6=L _4wvZ+NЅw,UxVA]` ZT.8}p%E>oYK惵[%筇DRlvVdʛQ ?,i(V TP+V9rE62ܓ&62,DEK] R0X76A`SQ˂쨂 ݏz31` y{rAW0W`PFrsw`P)n웸gSEAh. gcXKE7YF�!cCM1[ID(C8Ep`ŗ\(/`aa??E4npXOWw(Ȇ>Oj>Lopje`Pkk+,?ˉHLeuSJM` X1rjZN&&^܌sfkx'@UhQ `= 5O3(R 4| -8GL<OL<!H9_=A7@؈0Z S!xD  ,!~X02ޱenk1gW* gm]| <SV EcT%CH`Mpu&e| hSX_oqB!lpJE:'*+E|޾߉-7s}  a<ǽgaxgXn)(Gžn#D-z#^m\wc#[a@S|"!u¹(A? A95p lI_kTg6/ǖyc`5W5+WV +_%A:;L̂9EmwRy2tO )h|-=T͜9~gGEpd,=j{R\GPk5_Zn`6qyŮk6lŖ`ؒV\R 7>3'fQ'rqU~؁-sghGo 5:G_89}㬍lJ :Xт iהGf@+`9Zvm:#XUAg)|Ly�肵%w\Y h)8 5%MODo:�>w:~mnvY*,f6W,lY&z}D`uSTlƙo?UG؞?6SQ�~ҎU/i%O߿!X|GEAW8۠n�bOnE9p({( B/6p}qK傕Z [(8 CN5�i﮸y2=ЦLJ;XaA7(yjU6A-XGU(Wrh`徧 P CK\J�C3QzJpDP{a%vUU=hI\0 kP_ªag V?"N ;BGeCv�֮mЬg үò΃U5mue!DH qf,6̑ž UvZ\d2Z?UL)tlIDee7is`QhyFe`f2ahcw=Tdv^;V֛ 3aZ޽J�&:Hst5J|Ngȗ 4GY)^fr.^ߥ RlfP<`Ou_A>'Hw@dSI:ݜ+$f^8g 2?Ǟe*�`BU :).g* D^<SYg# U{Qa?. k*+M9 V>2m Ka?|{K`{RU~m!9Rj(!(+pG/s6X+?8jw0_{O*L6<a2^7U%>SpbF*,@6bi+beh{p9ME:X[`m V/U2)*:ڝ&*L1<EDFu�jA&XS*qD/`{u\}y:SxK,+Z c}Udjo*柮* V`4X_b-VRɀNj `l8K$ic<^M`'#سVP+,^) o2|QSH;Gy_պt7Q'�m,8JDR'U|ÊqF'ݿAJ& !w{֧ ȱؓX22H ܣ_~ _R8"F~)@ e?ZVeE~ Mfmh) 8Ҥlgјo%OݣCiRC<Kl-m;8cE<5 nFK`eC0G%wai.ɭ$7P#Vw纨JWs. =-k"b"3)dAv}* ˃S�K6t:DׅC0`#X("a<.nۖp^eɧv5h I|ۣnȪ*q {HEI;+L,G#6\>0fxR&X:"~(s,%$O --�A-UQCˑEU$u6).Qi/*(XÞ'ՀiU7 -}JЏxk2 D6!}O *Yg-4ڋ",4m�Z!kB#J�*~؃ðZ�/?Zh=E+AZ*6CY(U ]m(Rz�{6x;Rf<Y ibC#pbj3h_A#+|8ŽңnF q kᚠ(O7u\<_;w+A,뱀RUJ0,=#aB].4 謪2:b tmn;{;0, N [ʏ3wUd" ԍcfVj q`!{U/5 4 w#B`EcA`ع4BѺJgW_Ĺb`͂>O˙ o#<�-8"9Ic�ġbxXVؿ&Q.X"N`Pcp_�Wlk:8D|A\(fHT@{ ?GıS.mM\>k/CL o(Sq'ɇ*@>wwqJ5eb~]MNpS&3,{J sUyg 8cWùT$~ 57"bJ+[9gvLvyUy`>X5!₿^⽺ `yX�-;) h@ qV q#i(;f@*1l9Abރk(�o&dEL/[xXOs}f̣U!}b,[(z#Xb?tqQUĄjG!؍gOr]Ȥ V&6O{* W�ۉ >~%x=G^1 V31,s2&Q=r#&{3D hO\eU0 (VX]oqqᡳ( B%[%=쀌nJ0,|@e1;Je" c#>h�[kfnkloox$ ,& ub$>rg= fچ?P loо¦(vj_VbaPATX[!'k. rF*oS.y:ٯñr!Cpb\qt;l%̦*}ByĂsCpv( ڪ4 lWaQ'A,3=F ܳ3&× *o`fX Y`-_o�j*�5b~Lq%X tTq$[p/0M\ı@yrX v6)�>y4 ~Lߓ%OׅjŅԲ`8_A o[Nx[. .e.YJQ;ȀVsNQ.vU9xv1LʼnR "~~yzqXB O;UN@ljwհpxJ b3y jpVھb- �<u2O( 2,$#׉yD_`cÂF $IMN^ƨ$I$uJԱ�blXd{Oq/[XIYmݢS=_}I+$ܙ&!a%vHrX+$9:Д*!I u%}R)$zНnt }ӈj!I/ kO јc]թⰞ3yX'$%VFHRaUiK͐*S+H0R֯����IENDB`�����������������ejabberd-21.12/priv/img/favicon.png�����������������������������������������������������������������0000644�0002322�0002322�00000002476�14154362354�017532� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� �(�����(������ ���� ���������������������������5�C���������������������������������������������������������E��h�i�i�i�j�\��9��� ���������������������B^�����������������B�������������B��h��?N�y���������B��������������������(�����B��� ���!������B�o���������B��=������������������������� �����B�J����P�U�T�TY���/���� �������+�������3������������@����T����$�����M����������CU��������������������������������s���!��������������������d��� ��������������������������������R�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/powered-by-ejabberd.png�����������������������������������������������������0000644�0002322�0002322�00000001672�14154362354�021713� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���e������Z���PLTE���#-9B��M��\��H j��(%{����()'I��0/ 55�531I+*87CA�DA>JH,RN�RP`CBXHJYYOQNa`�[[Fb`)\[Zhh%pmih8sacljHngeklV|w��wvwxwo����� �ǛR���tRNS�@f���bKGD�H��� pHYs�� �� ����tIME @��pIDATHǽr0AHbZ]ZEV׆?UAHv:;;J䞛 q-Jp8dv?A(=%@ȄGg_quc붴Djga6;lpd~nOuN&TiGБzB/!(%emP /O?DZJ&}}J]y_�q8^ Twcq܊.ZL$|v>`sT �N˲{ D/3) qa^~AƱ1w eThFeQc O2ͨx{RJ^Rnk(ث‹9>t,Q}oSP0+�a)%,VrռX4h6ߧ� 1mkeI5#kKD rx72{ Y)IO) &ɽ񣬾~;Aţ(E9se*98//}BT5O$ľ0pχKoɥ4D?՘U2IDBLZl]a!@w 8{+;ZcDM E»eh6#%?b/L6y\La1׶|4Gr=q/.Fw����IENDB`����������������������������������������������������������������������ejabberd-21.12/priv/img/admin-logo.png��������������������������������������������������������������0000644�0002322�0002322�00000064030�14154362354�020125� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��������C�� AiCCPICC Profile��H wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺�PtX4X\XffGD=HƳ.d,P&s"7C$� E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI�(L�0_&l2E�9r9h�xgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/�$ZU�m@O ��ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h<X.d 6'~khu_�}9PIo=�C#$n?z}[1 Ⱦhs2z \nLA"S dr%,߄lt 4.0,` 3p �H.Hi@A>� A1vjp�ԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a �ٰ;GDxJ>�,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 <y}'ZZ։6i{L{ӝ-?|gKϑ9w~Bƅ:Wt>ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~��@�IDATx} UnL A�"B>-*k|v3{l!|5>[|v+A[ABV 4ބ d sn׹;sn {ڻ߷j^{+ XDQ4 & 1 AiT3C;F]: j"@ʌv+v4E{I&�4M_@ D"/Z WpPΏWSJ' T7>[ "7l{=U/!TޛL_{B@� 9# L27̲K{Ԗ+ 7 4=T7-.bZL_;jN�  }Mf/RŨMƲ FH2_.IT"@E ZV?]#h�rgj7͍8@GZqA=<~D"/%_Rՠo%3AHPR"05h)oWJ it*Vle/휖rTпǘ� Dl4ֳjm. ZoJ T4Lxٶ'UVuVy= Dsz_eǕ{@{]٫2f(=s5GM_CLj� DClkXM א৫( \yS/7Xw�ORSϨ&׉!D"@z!5:`˘ӱ5v$#"`J6.ZMb|.ME@`WgǮKf=: Z[&0!z] O'k}_zrB`#U<,1CC!Anfઃ0A gO.00B#t f>#@Gq G@lNI˔@F}vy\G@,``)1U`v=rslAWN P5kkPSPl0ʒU/6[QNOGm̆ڐ(T"@� G�,ٯyVK`-Kz*>*A|"]'Y�pA׆& \mLkLN^H� #`oH6CP "П`~be -  %@X Sr0Be:(_r%D"Z@g2+)g[[[H64"@"/": E?KD"@"u*kA,̸ͯA(Ѵe'vcdžd_}eՖ0n'"@��A?߂MS+@/AqJ;*C@ff Tî ĬHҦX� Dx�3AA_> 4DGib?àtĐ+ `jqK0-$k=ŃD"@F�A؇@O@EaWy4G@6z= :yU%Du!WA >3+aꙙِ D ȟN((C�ρ(F"P6&c{w]΄؝% ("@]dM eWz!~LM/4vnMg%30|<d6 D�!%yhQhuWօ!@,)x}x޳1}=8HށO"@� #@4(hh `{=y`~fPs"P8T_R `a9S qD�(.@J'->F'RD=;x�~hzVo=Iʼn� C�A̲>bК? ξJ&M`$D"@BЗQ4)�+_ �ǪD)mLD"@��I3@z" Hv*N@OD"@� D@DgBXg`?W|_/%D  \ IVD"@�h�_4sN p(x>{̀`#M� D>naAv5;ćMM@x>ED"@C u~ZT%󏫾Lv%k"`F`$"@� DG�8,x/Mu@!`Z7"@� D5LNn'5ŕ÷ �H�T""@� D4F�18$H>YaΓ;e!n#mP;"@� D3̴rV3%PWfӗLp "B쎆lQ~0H^- fրvv6VVVag!@� Z jb^MT9]Nm6}�p9HֶS\QlSi+I#Է'#Wt5+6@]� PE{q%s/觠G ڀ8t 8npwK kq韄aPCM?� n_>R/=zPpgàr6UVPo`p;<Z 55Ж"> }44=4m]ѠQ /;IɒSܘBm�0t =(I>Q_> Q` q É ~+5=Wspz/D8O|gt.VYAׂN2K-`8.2hu8bP+HT !^΅$h3&q2zPցߏ�>8"~�ZbTd 뺖:QG@2XX"j/[# \`"p'w<CKBVdU ^{.1!&U!!`#f_Xf33c{f.j 0 '銢@ %ɍŒ~T-aP%"D i#ce f`R:lphag3)"> Eo }ZIh苠wT&9w4ApPvV4Jlc>`G@yK.՚Hl@kbl\�ʼn~ ' cwu<]EvE-W/Ɔƥ`X­^&@];[_k4NN[: cr},rW ;EA UjHps/W$?V:33pf*БuB)JN(MFh4̝:=/ 0Ea8qC$,ʠCf ϓUȒKef (2/\M_ka@+*H,lbi@ˠA'@Io|L$K -p*gi:(0]US|0N\Ğs\Ƭ1<>nU>CdQ fDfNv�:t�cJkmho ɇґp|@{1.% `RՕAu׹lUUS&d\(^Ľz ] H|[ 4p@g@2X'}3SDflO+˵}Q$iA7U!:@7uCV2 $`M=] ȪW8r,eeڗ�R77*Ss<69՗2Wj 딻SII}=rS5!=&t*e�Z7S�@_#Lw0EٮAPx1׮k wdc@AxQM܉6rVvϒ%M`jPH6NeQamjpA{"̵gM[:䛆z7Zge,(҆NZ#GVóW<휇ޅ=kʬs7.\8 G  &؉u9ꫭwZ]{GyI8ޞL~Cxȷ�:˻)ehĮ^AWۊNMJj&;_6&`P{k_+CQU6l{V9+`ƈ#veϣ?kཕ#e1jŔUմ{cyg1ȳ]N^@ҧǾٝBP%/> pF~J7�}]f5uVDǠ=зaۨt@ry  R؏VZe2ܩNm A'굒ђ-E1]>j>f-p6tӰw])#ܤ$,%|!)ՇzB:R~MS@F0M|DRWcoʠiy}]4_<t_@ɞ-æNWal/r6C(C؏.Wki Z=;ߖ EKVzQ\DZ2~ztJ=h cS=.g;jŨʋzJ6 8KK_/s ZMW@_cf .Z~D`ub*H!68jimaN-̲by1.~ڏT=B`V$HxK=+~D|-NU[v)kͧTASf>M>[22|6:[Zg?gYBwtTIY*(nX 4I2kXꃠ:U-pޝ~%Sg4v6 =<^Mciu40d(.7l܃5}QF^biR EleuِPц>ÆPͿ=ki2`0kH&sOf00"u` |{ز\+û%ejZ._:#t(sހX@HDY<@FєkKTf]s?(kd-oW%_ A+BWoD[Q{jɦ7o>8Gzt6[]hu&rMqj laSBWEˬ `NѣԚ<5F(yT@ٕ(k䝯e~f{ /"P䫊[ҁ#]g,uO_}=G~fz'Wac! bMd"Z$u̽vQҫXwdbB߭qp~;,8X#.]�ez% جgrL/Mz\/ﱞu/"c2ڣ,345.4QBM1 #lڴsAq$hit(^%kZnQZfX .);<fF*v@sA:%/"߶% > ,ņ!O"Vu0h#׼ݠ$u3pT/y-n}O3٤rl@tp/ S`%-_';m3O' Jx"K ߍ?$!)pE Ҩ0x<7ϋY<A@RK{^ 2+WaO)^'C-_#3c_~đ3= 4E�ɦMJ76hhcC튌' zj^k5a8=Q2K M9 R2 ׫jW:㰮6+{0SKL;Sx65͕\G`\캏L甯/U ER^?w.(Vٕkk%ֱz Rki0^W('ȆM21i]x8wVx(A4[Y뼛9#Sk<c hʢ|$UvuF>F�Oն:=/:T 2h0zFGL3 rפ \f9Bh:g( RFS+(xY+ya3s/S?w_=j~!U9Ąb S}t=**{+7!$d& ZB sd90~\9@@qx6||~ѥ0j�#+0K6*2ˈǯ KӀEvNcFI$˷o}t4m/5B?{38 th2T 3#nFHc&LP;Ҋu}r {>༣E8hQ9u}\(xG_#t9̢�}leUR,(�gf~cqv I$I_,#`49 ߓ XmU/u Wvr^bˡjA6 9VCAb4VZ *d;Rװ#/گ4 j{]ojrGpm h6ncњ})@=m|hTlpDЂ(ڌONJ] sK8xƁƎhL}mցNAgdmwA?=z' Br^; H~GMذ :( [ɐL1%uT4ZͤԳ1P- *H?xo8om仺u$\[A4MPiqml�vუ)rȃ==3gS}he<i0~66. Gtlu�[X_7~ \kg@$}g+x<[ 9Q}�). L 'L ;R* |V#VVR}]kG;=: GE*à.1@w;hx˔X$-~wAnoMRq {d=w)rrap"xq~EN%}Ʉg!N n]vn_j^L2?*}mjA@".ǽo AE_=<ݠ_lе E>28dzۃ!U¡6U~! b{tHR=mq] U%8Ue56XghyZ+@wAֈT<7vI8�i<k[[xҙzc$`v@V;XY[rJJpMu#RI3*w^7 �d ME({}ke_#x#96h�l#u~Z W*k30췖.)\gǴR.j6!P:NIi&J>\ebU DB_b*W8z~:ІtJ]%YPԘ�.».KG.1 y�'D.KM22+G̰܈"교=5י2h!~˱꾖z~KdRfm.A]L@ -O@y͂f<-8צ, ^{2v-�<Ǿv d[֓iT{o#'.lJz >؃>4HR7 \svp2MAv)RA/h OZG`c͟j];/p9 !;vFM=yx]oУz$ژLuo`}}Qoyֿ݈ 5DY*vKlZh;` Q8 { '-|+dAN#V-<; G{jӑ<U?eg4pš]Wᤙh*I};nMY{ "pnߘJ"Ͳ6:F]K>i'rkts# r<Kյo?Ųll{m.:F q򚼼J^hS8ly? :Ԧ>:AKc5Vϱ `&ySaGz}q=?=]h䜵8xT` L#} P9Y0$A #CY*V֜8 jH68I+obs &i'<f\PYIs={{<]nE9j rE?;n,:'۳fqN:+kwFހ! ҿnpZAijE@v뻼UU%Ռ)g'QXJs?lZp_'Ӏ$9U,1-& H _K`;HdWxI6i%. N6̺_)Q_Ro>XwZLiTOƬA=Qu5;_ ;@cʦ26S]F_Cz}E-ww+aծ[7ϰ+349dftCM,1Q^:ю,Ji�+.q{e=a8՛^ aO}9BwF)@qb*VnxT0.zx[\+#wF+t<A_լQUtsvzVp�1Y3^!rw?'f[ r]Xۮ`U*dBqKJ \9:l>)rֵ,oumJVNТyFE Ztje"x]WCd*)'Vur] 8}IA TH(CsUMvɾ ^<5Nj̴ZhOYIõ=t \9T&X'c&NY_bg+cw;tbt~Q= g87Z|Â'.}}jROGzp>Kv  id5s!:st0r̶"+?Փbt~D["d<-mrыU[P'.�sëtfZ1or{ՎC)ۗњ2%x=2yWֺ\Jx{rL3}ͨBo1f,tjw6jUf]zp#|�61jdzA=Qxv_#�vR\USA<w?bA>L@a 7̤l7$Ƽd0nдs<tyt/;,})C=*}I%|P%:xza:euk$o8xD�?$S}UpoO/Wo#>X{wƟg:[0BH}]7Tg[uO e";KSEQy|0ϘmH�5e4ius_z-҇S_O"~mf]uG [lpQBǽ 5岓4)#ܗ=z$yJEֵ+fߌtj-"z慖Zv-dPx+FeB_gr_?9FP'ujܿPf^;g)mekx"w~!I's2U҇;}س$t^+!mR$/85ƮOc;P]FR;"]C-c}9H7;)F:Aa1ֲi7&]̾uI%I_?kxt)fٌ'̝ fd2߂Y"y; c֋7F_{ gV/'t|6q`t9a}k\fo\~oHĽ0y=ٞcYkhspF iCд�L&󒢥E ɣ]hAv}L0t/YUO%}ץ5I~OVͶ.Ɇ7!+p5r̫/x.+q3<6�5п02pp&5-9պЇ( FԚ71)9kv3z,%W+_gEǗxsOJ5HpGOv`Iez&9 mg6Mvk��t&_a^d>E琢]d-k]TTLmΘ ̾qX~\4x MdG$]"]np\v>b=zP7486"qOi~~N}v-Mph E-_v<=9M4k~fuIa,| da}<g/8^ߩcSw3hhesFD̀6sn%e; QӁ[`i}(̌=Z]B])_�wTxd,IFf-<bk-x[^11a_YՐhan "~j_Z*jZ鷌kC=8X@~2(LQ} pK훫p fz!%|쾹qvuA~ G!=ULZXY1Tk]-^oM R( 6mŸHy̺EˎP]3q si}m"(HcPfF tdZ| $X.?sL3_695ͷoӳõLP_:Hf*erWڣV[V׶ q#<dˠr'3uЊ剕Ap!^Ifğ:T/aص1nR=s;Ye<t\Oѭ0mqCE_;lAA:g]*22FZָVi`$*fIMS. ׿Aqv`3yw4J!3`[[J_< >o~!0YZY>¯Y6c#J�uV \'P�7λJ??@S%ݔL?s]5hSrhD@}M|͓r%�Wn0y\ɐ9qiͪ- tو{0U WakY˟leFi!8~І0B@/ĂS8 .wBVG5I]JŬِ C78wZ!,=}rhSx?luV&uϡ]! g[TWeM y{W6~P!L}Z1ZԢpM+F]y;&)N_;%r/cEN9 dntWf$ߝE"fR]02:X&_Z`9JP09iw9V=Ze5Ap@u_!49Vd驥L-9d7|?k}z+c[}yynCUC̡ņXa#![iQd5tƆ\0x /Rmt:1yr`}4ewje;}="z ?@kt*vDӏ9$r;rouQol<^ZpN PA`w )iBNE} \0nߐʶLAsx'L_ Z: lK,Y7 /w74&m[tg;S[dBd͍T /i0 Z^(=)&vuS `)m:^"}qk֣u}6b,h%%@Xʢ�F䬴If)*,,)3t3X7ۣV=kbs0J )5Zc#E,# oYh6Ӗ<T�ZaCkF@yx6MwjXXzKQLC{"`k>7%݋;Mk|>Zpz#zMkY&´G#lU3gw"*M/r%`)>>fc<PˋUG.s [Rto[bv-ew7\5yzHe</(Fom"- \u]3p5wGPw@ڴ@P2>.B~#qb]b;I# $@@B�@+k,8ւ+cd/pL򬵡X r m߀6*ҙG .8RwWaqp p=1S%jQI FK@ t׸mϨC<oIBYQ1K d]r#] ?K~U+*[ pT}f炯Cdeɯ>5zYs (xգ=er.\)l, 8c⻔aiqI&^<:?};/SbPn @9XKɩ̧ >F~?K2@O(2VKk%k H-)U#]SqʓVE!rsI[}dm~δƱR:.:8Ɉ_?2`eG\FMdA;Xs?Z3;!* (YF 1rC\ ~wʩu;eź(wr?\]gXiv傯XP SB͉)׌c1Y4<`9M7[xGܮlA=7wlCqyMŮ9:qu)j=sTqׂ3]OF4_eBu ]񵾥M%`A_@9g7=#_ܙEGzP#лkhA!HR;OI֩!Dt'=S5dZEK]۬hɟ*00g֢*=RG_tZźL  hc9)XL>S6S 8\TMQIfE�?}A@WO‚;h[O5v7`$!RL$٣LO Ϻh,pv 7d}˟Tz.:F�Lִ>"V6cQ%!y8Iu;>\1<>kj+ Q�?}@`2]4AM#7 9 6y˯m_GFѨApbHAٸ]qM)cZ3H!uSxtٌ|љ95N0 zN& θ$L>(O2B_5iďk++v.# q)mZ*o~?Z78ʨnʴ Fc&XyvڮF˒ۚkֳ4g`ʿVtW99k-gދGFusQ4v0xRVy c)-KB0^)a8(0Zyj ٠I-7"]vͱ*HE<`FY,*J%'a}9a8U}}, 6}N_h :!f.djG.V$ST$g%,JhX,%& 3U{lB{ ǐeNR7a8E5d\|.,%N=khLQ0zKg{a/hΜLoﺣ߅,4(j]D׉[I_U^{tnk/0_$\#A[$ iB7>j\>:196XvTQGO?p?/IҶ'3o uMFbm\xJ{Q#w:]T ]=`vmjʤ N]8HQh_ Glb*"^&b链]O>+k\ѪΔ[D h(:ojƎk�$'.x$I +ZQ4[-M+w6`f8,EYv`+n$0n]I"ּ˾nY:9U'<x%Eu:WAl/~nԽZPl+c@T<+|elՕA"ѹׂאboW�lj SsT�h1 : jq׍rFPjoqf7[A=sʃ2\qVW,_g[+M{zx p=ŔrY߆(#{e^^̸#0zS'M<obv~k~oyHTJ:-17o5ͭ1kpnג gx�ӄ BtE5`Uӥm(̩ `5}e$!ꆾ~^OY5=k;ۻo1;i#')X̏ƌ{wUwnշ)#8*aWm}:#6p<KӲϢ)i/:g)g B%C|Z+&ģ 0[}uSWNSԷ2ay+2熬a#_Jƽ~ZԷ7KJkOaIņYٴujdU(+6: z얦VWǻ"T[*k9�g\M:o@dIyɦoWGۣJen{ 5jtp?Fd w|>RF[phA%}/Ȳ#ec͝WّfP RH{ rljEK #‚evU�&msS^z`s4kdYWσh!ou, ጀAAy;ʌNRgW]gVONU* Yol_Xʊ�ދ:/Í>#r;:<|k#/]~&Hslh)mq%v?BouRĺt<O2©e NHS闆Ȫy"ZDk6fo,a zp \[eZHEYǭ ps(-R9HҦakF8a"xAqO Mbe G9 4 k+Vvv#>z@vydIfx!xPw|=s 3݅ L=g쵳Gt ^iճ_k`}8q`; sNaEQ@F{=ȣbNog^.B ~=O~�[ ^Dplo Mホo񚲃%-qPG I50*ϫf>ܛxN w5?pEMlaf1d28E;QuA u4I0|<ta5j(4jֲ3ݐ0\5Fcλ<d@N>\C'}":*3=b@*ka>l+hWW^ ZÍS 0Un*tm'*Iib KC 30R${Ef[gީ!oYWhic'h?x1mE~x9Q#f yi lL#zְ|fX$_7TUq0G~zӖEF p"LyKDMlu~,sivѵ<�]mV zuWNǟԻ@ <NtV=7 ��IDATl$vgeWE9 ^�q f]ۭ]㐙 |~lotd׍-[~o.QՙO_\6H-gJj/SaqUM%֑^g 2AhK7 \es^Q݁#L_ +r} ~ߧ n ]!W,\d,Rrj{݊Ki>Wua6ܽ#lǠ]{$k[϶$5$uKm~"k+}DCa_5Ldzi<eEGy(3ֺTx~U fC+&}w $a*nu#^E;fwT/%+-QԆFv]V^鮟T`teJf Y2_="\P_ V|tb.?9ʬg/~4aC뷀,߲x-^#З r[r)'&{ߴ_N:xoy\MW~#n#[![fVA+AmEfnv}9&Z}8G64q;vvڤ)Y!`O_5K{3kT*GXv=qv37s$}]_7k羆{j^.Ł@u_FErU0cEnU& d(XrGΘz!%axx>{U8%?޳ Y%A9vOw ov+^^"G;sqD>8Y*BVfK7Srö(<oQE7*ű9}uD7<ADmtzs*t䧭k+ Cq+u(D$)Jw,wdߕk@O^*هN̜u%`tIO92!'/"k5:F#0Ε"JS,٠.kJSpЕrǹQ6l݂6 Zy =t"œ�٣jVX5!mO\ ӑW5Eu^7kX1&/K珯ll| ecРf,<wST"m;|Gf~r~=ߙmV^AӆP3e0xP:~kJ#�ٶȊ֨(᳣{CpPTxN|- O^*bsTc'pfbq}A\ }a5w5pd4P�r)H;8@AC؍^< 2pmNs=+M[A\'Ks@)'pZI|pDܫ;Y!8ڎ:7~ Z=ґ? TGy-.=;C@Ҙ$]H=gsHa8#/VanӛI%�%P;H<qWj? kyHT 3rrW($W=.k�zm*Nnh|TR) >rOZ[\PXsB͐N̬ދֵiB?.Ui=Hz00е9/R4(y8Zɽ#}S]ge Ԛ<~ Bv[ $EnI\+Bie0(<L&'ڐ�sF <9ι(krp\ؐP(k<]\gp28S dt$(nYOV؆W)k8I'p3s"}Cw&  !P ǡoN*"Q*1'TO9)(?qwc5a8y)n`&ΖI^ p|>j7Mk9u\;c1i:8_!.ߑm+{>pw'6t`zV  ;c&wZYSU`ApHf%Fgsv{7+iۖʫo;޶f#ڊ󹭷m[9 \ױ¤XZ6݆s.iD_kyCמdT _7lv-g 7UhuXWa:'bJnweJ2%+wf]>7\akvN8C+!X \ErJJ+Fuf UMCkW3f4EN],rVO74׌hb3$,-),Xw&kSTW>8R,̕iBDS Ad!bٵe^绩D�); WgldMm_4o# ;5AVE']٤U.8zb1%YV"] ذ/LRtNkt!- Khvcgʰ0,\暑E°  Cf\lP YkC~׆,MFKXVCßFA.fL=aK6$hzF6W\a M(R|;&m#~b._& PApUkX� 3 ֜% Leu#_򺭟7A~*sQ x]/N.U9>?fV13S* Ыx/6Q5tpF8Z32 k_TJqŋK`AmA:ȋf5}]C"ᧇNhꇁ޽ސq٨^W^Dg@E"╠ݜ6s*Ly-Xukm;:b/25<]ZaM Ù\:yW³籯Zj;aHbi]=|-"wBZzO6`/]%/ CS3I#R|DxN{*O{x=.ԙz5fPoQGwnfMxpb:�~K0̷(&xd+>-K!m/=86 ytg"r۝N_ l57?%셙8 :3#7C79QʿuSM-9y>Uf`Yz#_v9e8 IȢ«r 3h3xb4p 18QbF@eΔ}s+87WBhSM>]3knȪ+̂u;e/X>,AzqHy'<# /2=~x0CeUb<ph*55,WI2Qپ$,<;OdíW%U 3P�s2yY>oK{뚥?_Z( }O:[u_uaU?,'qk0atF \8!@HsnV>An“ 0 /7gInkz\_ _n_^Mll']E_.=A*dVFm?,5q255U<Yxߨك7dqGV>4f=Z ୘UcF7- 濡Ll$wcU6wXb͕m-;6LܘbKW!̾,k凮pK�j(@|>k]xx{)ͪng=6&N,5F0S5O&̍O >2æ:ꁫKZ̦qw F./.v sm.;a5Ddy }LbzA P~: &^�,du~ڝ5ryӰfYХVYBZUF :,֏_x`3StdmG\vٙV8K-gjf:叏` lLijv<B,}Xc&W׾^ WÃtKϾ ~0X+a +k/J@q"�zzT>>gtmKJm7| 6.}`c$>?"Y2{Xr7)ۺEmh5,[tpƵhn]ڬ?:12neƵS`vLcWam&Lt0h<1ҏio^QyA*6 Z5u` HI`=d4p,2vսepdŹ=S=_ % {cwee<Ld"P`)i/A*?i 8*`g}2X8Lw<|lc~uQtw>xw=@=-( }*lC̺5n\<\#`Һ/3=m<'B�vrS�c@e*;a CUgԇte/.;U&VDtj5ޮȒ 94ߣV+',�8)N]o<@�u.9z%(vR}[iY`�( /E@:QٌM濅Z1.@ *#/.Z+lܟRMT=tu8Cio:@tPIuMOi>KٗK{�Rڳ<(e=#.P>X)_X9nu:S=$^̼*U2'/HgQD�O]i,tE9 #;Hf]<= ?r )YkL_0 <G:͆SH 8 A{ yY~׭gc(^|6lЭ=`<׀bk΄Ҕ$XF I ?lZ�XzaѸ'J<�%h)%@ V)TT@߁p\D+ٕ= ؕ=W(_93 ]}$\^34UKgE*i? "+m/˸]əQ~s�J ٠@9m20BdRl¯a+hA Я@OԯNUouq:hлA@ǀdWC^E,:7߁~Z wJҡ$;A}{{[WݍpTɭ<vл+Q%kNj:&P=2*X3]^)sc<ݏO_% Ay@:̃"ӝ (3( 8cV7o?{P~p|l|Ҡ^-zNٛoS3%ؒ I0P큇mJ}#ݪk@ z ,}�Vu(uk "@�PA�s \Fv,I, }L\OąAjJY�V9_d{'HvݥBC9$ D"< \SHWA+@ :BC@%/K&/XIl"&ڔE� D�\/{`zYt'>(bzޠD"@� >" Aξ OQO"@� D>5` NDED� D"9Zes#"@`.Y"@� Dn,48�WAU� D"@@?Z%xe!D \-LD"@� AIXrVy=  `j h!D"@�`\`è. D"@@.Eк+JJ�ג7�O� D"0 OA D 0X+"@� Dտ@MDo?jO� D"K`{uؓ+ I`pX"@� D |FȠ W_<E=� D"@耀#`X�p9"@� D Z哅`c:D"@� V i�Ġ*F!Dp2 D"@@x\B"BF"@� D4A`-] ZSD!: B� D"~p&mXQDDEF$3"@� DG)[/8"D  \SD"@� ^#t-缶# "@� DMwG@`Z:`"@� D=vuX k% #%Dk N� D"P0$ .X6C�`f@� D"#@E>@��X& D"@@xlIfWE*/"P4"@� " Qc!Dd0p-i. D"@F@v~$AO P݅O"@J�ג7�O� D܇^B*,D^0p"@� D@ ,_5»I�6|_ `u>Y� ` &V"D"@@,2ЯA]BԶ"@R 5h"@� Ud)A"@݃O"@�0�W` D"P;An(4"@� D"@th (mن tN� D"@� E�Agu'h|iD"@� D"`f("@� D"@J@%=�"@� D"@�@Qo]("@� D"@� p݅("@� D"@� !$p]sM1 &D"@� D" 4\W,"D� D"@� D wqD"@� D"@>|Md� D"@� D8@u?3Q"@� D"@� @ D"@� DOssQ!"@� D"@� XG%D"@� D"@� Dcxn����IENDB`��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/powered-by-erlang.png�������������������������������������������������������0000644�0002322�0002322�00000001560�14154362354�021421� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���e������h��7IDAThZ?LQ1ES$Bbbr ڤ0i"MHRVցX u6ݨƄ.EkIS:8o:~.w߻q ( I�`ARV1$1 :8==nQ*#�PQ* zўo(�ɺa1 otԄw8JĢ evs60&}q@b+D?Jm6RkF)yIQ~:. v"mse]+3qb чĢE<# Q kxهu(gL,<UQ9GY.& 7&98E963:O; 5!~p&~`Ζ8S,wfשkݺ{_(V1H>XQ/m-rp'.e%sJ}]˫*e ."9e[}7pvA)+yRV-fu(*rH\Io)8Fh/ o iwӚ]z nZ`DXLV+9N4K&݂]/M~㓦`�0 !,&t+kd5YJL)$[-Qu;%+kpOB wjǠS SԮPogF}Jֺ|#,[|?,.* R/w0T:eOa�7b1F!ͷJ~u(_p9u����IENDB`������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/admin-logo-fill.png���������������������������������������������������������0000644�0002322�0002322�00000000261�14154362354�021045� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR������7���&s���sRGB����PLTEv*Jjʌ֦���AIDATeK A>$G,` Xp <]]P+Jl %DyPP:Rzf����IENDB`�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/img/oauth-logo.png��������������������������������������������������������������0000644�0002322�0002322�00000012757�14154362354�020166� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��,���@���<J��IDATxyxT݀; ȢVAe*UZPjEpi`Q(|*T@XQ "V@,# {AHB$d~c9w3gy!ν瞹W�ĹP2r'@$$5嘏@i }wZ0+mJ#|4]Nut3dP'%P78ZWU3x[$I\2. 9oƨS"q$�XDɃ6Y%IP#39!|E�U$f=KT5O0:�ڋ}đ[b2g85} *-Cq>d-O`ۓ,X)Ddj !# TS Pukbe b]䄄KdSЍT<qk6j @&48i7b?䅉*TeL8RMJ+@#q1r@|Jl?S?Xz?slc%$ ։�n$loSI:`%U4U`% V2XW V2X`UK&tK]${˒2,AV]G-^ ڽX!VW,\84//;meb;v|Ufo۱?iqե!qwrh(vb+97XLkEUk.`r}/t6-1M<Xb:X+'_.X+~,}W8_\.U*êӀY<\u5M~qX#^=x'#qxKCxw&)n;byz6=L _4wvZ+NЅw,UxVA]` ZT.8}p%E>oYK惵[%筇DRlvVdʛQ ?,i(V TP+V9rE62ܓ&62,DEK] R0X76A`SQ˂쨂 ݏz31` y{rAW0W`PFrsw`P)n웸gSEAh. gcXKE7YF�!cCM1[ID(C8Ep`ŗ\(/`aa??E4npXOWw(Ȇ>Oj>Lopje`Pkk+,?ˉHLeuSJM` X1rjZN&&^܌sfkx'@UhQ `= 5O3(R 4| -8GL<OL<!H9_=A7@؈0Z S!xD  ,!~X02ޱenk1gW* gm]| <SV EcT%CH`Mpu&e| hSX_oqB!lpJE:'*+E|޾߉-7s}  a<ǽgaxgXn)(Gžn#D-z#^m\wc#[a@S|"!u¹(A? A95p lI_kTg6/ǖyc`5W5+WV +_%A:;L̂9EmwRy2tO )h|-=T͜9~gGEpd,=j{R\GPk5_Zn`6qyŮk6lŖ`ؒV\R 7>3'fQ'rqU~؁-sghGo 5:G_89}㬍lJ :Xт iהGf@+`9Zvm:#XUAg)|Ly�肵%w\Y h)8 5%MODo:�>w:~mnvY*,f6W,lY&z}D`uSTlƙo?UG؞?6SQ�~ҎU/i%O߿!X|GEAW8۠n�bOnE9p({( B/6p}qK傕Z [(8 CN5�i﮸y2=ЦLJ;XaA7(yjU6A-XGU(Wrh`徧 P CK\J�C3QzJpDP{a%vUU=hI\0 kP_ªag V?"N ;BGeCv�֮mЬg үò΃U5mue!DH qf,6̑ž UvZ\d2Z?UL)tlIDee7is`QhyFe`f2ahcw=Tdv^;V֛ 3aZ޽J�&:Hst5J|Ngȗ 4GY)^fr.^ߥ RlfP<`Ou_A>'Hw@dSI:ݜ+$f^8g 2?Ǟe*�`BU :).g* D^<SYg# U{Qa?. k*+M9 V>2m Ka?|{K`{RU~m!9Rj(!(+pG/s6X+?8jw0_{O*L6<a2^7U%>SpbF*,@6bi+beh{p9ME:X[`m V/U2)*:ڝ&*L1<EDFu�jA&XS*qD/`{u\}y:SxK,+Z c}Udjo*柮* V`4X_b-VRɀNj `l8K$ic<^M`'#سVP+,^) o2|QSH;Gy_պt7Q'�m,8JDR'U|ÊqF'ݿAJ& !w{֧ ȱؓX22H ܣ_~ _R8"F~)@ e?ZVeE~ Mfmh) 8Ҥlgјo%OݣCiRC<Kl-m;8cE<5 nFK`eC0G%wai.ɭ$7P#Vw纨JWs. =-k"b"3)dAv}* ˃S�K6t:DׅC0`#X("a<.nۖp^eɧv5h I|ۣnȪ*q {HEI;+L,G#6\>0fxR&X:"~(s,%$O --�A-UQCˑEU$u6).Qi/*(XÞ'ՀiU7 -}JЏxk2 D6!}O *Yg-4ڋ",4m�Z!kB#J�*~؃ðZ�/?Zh=E+AZ*6CY(U ]m(Rz�{6x;Rf<Y ibC#pbj3h_A#+|8ŽңnF q kᚠ(O7u\<_;w+A,뱀RUJ0,=#aB].4 謪2:b tmn;{;0, N [ʏ3wUd" ԍcfVj q`!{U/5 4 w#B`EcA`ع4BѺJgW_Ĺb`͂>O˙ o#<�-8"9Ic�ġbxXVؿ&Q.X"N`Pcp_�Wlk:8D|A\(fHT@{ ?GıS.mM\>k/CL o(Sq'ɇ*@>wwqJ5eb~]MNpS&3,{J sUyg 8cWùT$~ 57"bJ+[9gvLvyUy`>X5!₿^⽺ `yX�-;) h@ qV q#i(;f@*1l9Abރk(�o&dEL/[xXOs}f̣U!}b,[(z#Xb?tqQUĄjG!؍gOr]Ȥ V&6O{* W�ۉ >~%x=G^1 V31,s2&Q=r#&{3D hO\eU0 (VX]oqqᡳ( B%[%=쀌nJ0,|@e1;Je" c#>h�[kfnkloox$ ,& ub$>rg= fچ?P loо¦(vj_VbaPATX[!'k. rF*oS.y:ٯñr!Cpb\qt;l%̦*}ByĂsCpv( ڪ4 lWaQ'A,3=F ܳ3&× *o`fX Y`-_o�j*�5b~Lq%X tTq$[p/0M\ı@yrX v6)�>y4 ~Lߓ%OׅjŅԲ`8_A o[Nx[. .e.YJQ;ȀVsNQ.vU9xv1LʼnR "~~yzqXB O;UN@ljwհpxJ b3y jpVھb- �<u2O( 2,$#׉yD_`cÂF $IMN^ƨ$I$uJԱ�blXd{Oq/[XIYmݢS=_}I+$ܙ&!a%vHrX+$9:Д*!I u%}R)$zНnt }ӈj!I/ kO јc]թⰞ3yX'$%VFHRaUiK͐*S+H0R֯����IENDB`�����������������ejabberd-21.12/priv/css/����������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015402� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/css/muc.css���������������������������������������������������������������������0000644�0002322�0002322�00000003626�14154362354�016707� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������.ts {color: #AAAAAA; text-decoration: none;} .mrcm {color: #009900; font-style: italic; font-weight: bold;} .msc {color: #009900; font-style: italic; font-weight: bold;} .msm {color: #000099; font-style: italic; font-weight: bold;} .mj {color: #009900; font-style: italic;} .ml {color: #009900; font-style: italic;} .mk {color: #009900; font-style: italic;} .mb {color: #009900; font-style: italic;} .mnc {color: #009900; font-style: italic;} .mn {color: #0000AA;} .mne {color: #AA0099;} a.nav {color: #AAAAAA; font-family: monospace; letter-spacing: 3px; text-decoration: none;} div.roomtitle {border-bottom: #224466 solid 3pt; margin-left: 20pt;} div.roomtitle {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; text-decoration: none;} a.roomjid {color: #336699; font-size: 24px; font-weight: bold; font-family: sans-serif; letter-spacing: 3px; margin-left: 20pt; text-decoration: none;} div.logdate {color: #663399; font-size: 20px; font-weight: bold; font-family: sans-serif; letter-spacing: 2px; border-bottom: #224466 solid 1pt; margin-left:80pt; margin-top:20px;} div.roomsubject {color: #336699; font-size: 18px; font-family: sans-serif; margin-left: 80pt; margin-bottom: 10px;} div.rc {color: #336699; font-size: 12px; font-family: sans-serif; margin-left: 50%; text-align: right; background: #f3f6f9; border-bottom: 1px solid #336699; border-right: 4px solid #336699;} div.rct {font-weight: bold; background: #e3e6e9; padding-right: 10px;} div.rcos {padding-right: 10px;} div.rcoe {color: green;} div.rcod {color: red;} div.rcoe:after {content: ": v";} div.rcod:after {content: ": x";} div.rcot:after {} .legend {width: 100%; margin-top: 30px; border-top: #224466 solid 1pt; padding: 10px 0px 10px 0px; text-align: left; font-family: monospace; letter-spacing: 2px;} .w3c {position: absolute; right: 10px; width: 60%; text-align: right; font-family: monospace; letter-spacing: 1px;} ����������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/css/oauth.css�������������������������������������������������������������������0000644�0002322�0002322�00000003063�14154362354�017236� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������body { margin: 0; padding: 0; font-family: sans-serif; color: #fff; } h1 { font-size: 3em; color: #444; } p { line-height: 1.5em; color: #888; } a { color: #fff; } a:hover, a:active { text-decoration: underline; } em { display: inline-block; padding: 0 5px; background: #f4f4f4; border-radius: 5px; font-style: normal; font-weight: bold; color: #444; } form { color: #444; } label { display: block; font-weight: bold; } input[type=text], input[type=password] { margin-bottom: 1em; padding: 0.4em; max-width: 330px; width: 100%; border: 1px solid #c4c4c4; border-radius: 5px; outline: 0; font-size: 1.2em; } input[type=text]:focus, input[type=password]:focus, input[type=text]:active, input[type=password]:active { border-color: #41AFCA; } input[type=submit] { font-size: 1em; } .container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #424A55; background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); } .section { padding: 3em; } .white.section { background: #fff; border-bottom: 4px solid #41AFCA; } .white.section a { text-decoration: none; color: #41AFCA; } .white.section a:hover, .white.section a:active { text-decoration: underline; } .container > .section { background: #424A55; } .block { margin: 0 auto; max-width: 900px; width: 100%; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/css/register.css����������������������������������������������������������������0000644�0002322�0002322�00000001646�14154362354�017747� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@viewport { width: device-width; zoom: 1.0; } html,body { font-family: sans-serif; background: white; padding: 0.5em; margin: auto; max-width: 800px; height: 100%; } form { padding: 0.5em 0; } ul { list-style: none; } ul > li { margin-bottom: 2em; } ol { list-style: none; padding: 0; } ol > li { margin-bottom: 2em; font-weight: bold; font-size: 0.75em; } ol > li > ul { list-style: decimal; font-weight: normal; font-style: italic; } ol > li > ul > li { margin-bottom: auto; } input { display: block; padding: 0.25em; font-size: 1.5em; border: 1px solid #ccc; border-radius: 0; -webkit-appearance: none; -moz-appearance: none; } input:focus { border-color: #428bca; } input[type=submit] { padding: 0.33em 1em; background-color: #428bca; border-radius: 2px; cursor: pointer; border: none; color: #fff; } ������������������������������������������������������������������������������������������ejabberd-21.12/priv/css/bosh.css��������������������������������������������������������������������0000644�0002322�0002322�00000001557�14154362354�017057� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������body { margin: 0; padding: 0; font-family: sans-serif; color: #fff; } h1 { font-size: 3em; color: #444; } p { line-height: 1.5em; color: #888; } a { color: #fff; } a:hover, a:active { text-decoration: underline; } .container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #424A55; background-image: -webkit-linear-gradient(270deg, rgba(48,52,62,0) 24%, #30353e 100%); background-image: linear-gradient(-180deg, rgba(48,52,62,0) 24%, #30353e 100%); } .section { padding: 3em; } .white.section { background: #fff; border-bottom: 4px solid #41AFCA; } .white.section a { text-decoration: none; color: #41AFCA; } .white.section a:hover, .white.section a:active { text-decoration: underline; } .block { margin: 0 auto; max-width: 900px; width: 100%; } �������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/css/admin.css�������������������������������������������������������������������0000644�0002322�0002322�00000010622�14154362354�017205� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������html,body { margin: 0; padding: 0; height: 100%; background: #f9f9f9; font-family: sans-serif; } body { min-width: 990px; } a { text-decoration: none; color: #3eaffa; } a:hover, a:active { text-decoration: underline; } #container { position: relative; padding: 0; margin: 0 auto; max-width: 1280px; min-height: 100%; height: 100%; margin-bottom: -30px; z-index: 1; } html>body #container { height: auto; } #header h1 { width: 100%; height: 50px; padding: 0; margin: 0; background-color: #49cbc1; } #header h1 a { position: absolute; top: 0; left: 0; width: 100%; height: 50px; padding: 0; margin: 0; background: url('@BASE@logo.png') 10px center no-repeat transparent; background-size: auto 25px; display: block; text-indent: -9999px; } #clearcopyright { display: block; width: 100%; height: 30px; } #copyrightouter { position: relative; display: table; width: 100%; height: 30px; z-index: 2; } #copyright { display: table-cell; vertical-align: bottom; width: 100%; height: 30px; } #copyright a { font-weight: bold; color: #fff; } #copyright p { margin-left: 0; margin-right: 0; margin-top: 5px; margin-bottom: 0; padding-left: 0; padding-right: 0; padding-top: 5px; padding-bottom: 5px; width: 100%; color: #fff; background-color: #30353E; font-size: 0.75em; text-align: center; } #navigation { display: inline-block; vertical-align: top; width: 30%; } #navigation ul { padding: 0; margin: 0; width: 90%; background: #fff; } #navigation ul li { list-style: none; margin: 0; border-bottom: 1px solid #f9f9f9; text-align: left; } #navigation ul li a { margin: 0; display: inline-block; padding: 10px; color: #333; } ul li #navhead a, ul li #navheadsub a, ul li #navheadsubsub a { font-size: 1.5em; color: inherit; } #navitemsub { border-left: 0.5em solid #424a55; } #navitemsubsub { border-left: 2em solid #424a55; } #navheadsub, #navheadsubsub { padding-left: 0.5em; } #navhead, #navheadsub, #navheadsubsub { border-top: 3px solid #49cbc1; background: #424a55; color: #fff; } #lastactivity li { padding: 2px; margin-bottom: -1px; } thead tr td { background: #3eaffa; color: #fff; } thead tr td a { color: #fff; } td.copy { text-align: center; } tr.head { color: #fff; background-color: #3b547a; text-align: center; } tr.oddraw { color: #412c75; background-color: #ccd4df; text-align: center; } tr.evenraw { color: #412c75; background-color: #dbe0e8; text-align: center; } td.leftheader { color: #412c75; background-color: #ccccc1; padding-left: 5px; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } td.leftcontent { color: #000044; background-color: #e6e6df; padding-left: 5px; padding-right: 5px; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } td.rightcontent { color: #000044; text-align: justify; padding-left: 10px; padding-right: 10px; padding-bottom: 5px; } h1 { color: #000044; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } h2 { color: #000044; text-align: center; padding-top: 2px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } h3 { color: #000044; text-align: left; padding-top: 20px; padding-bottom: 2px; margin-top: 0px; margin-bottom: 0px; } #content ul { padding-left: 1.1em; margin-top: 1em; } #content ul li { list-style-type: disc; padding: 5px; } #content ul.nolistyle>li { list-style-type: none; } #content { display: inline-block; vertical-align: top; padding-top: 25px; width: 70%; } div.guidelink, p[dir=ltr] { display: inline-block; float: right; margin: 0; margin-right: 1em; } div.guidelink a, p[dir=ltr] a { display: inline-block; border-radius: 3px; padding: 3px; background: #3eaffa; font-size: 0.75em; color: #fff; } table { margin-top: 1em; } table tr td { padding: 0.5em; } table tr:nth-child(odd) { background: #fff; } table.withtextareas>tbody>tr>td { vertical-align: top; } textarea { margin-bottom: 1em; } input, select { font-size: 1em; } p.result { border: 1px; border-style: dashed; border-color: #FE8A02; padding: 1em; margin-right: 1em; background: #FFE3C9; } *.alignright { text-align: right; } .btn-danger:hover { color: #fff; background-color: #cb2431; } .btn-danger { color: #cb2431; transition: none; } ��������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/js/�����������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015226� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/js/admin.js���������������������������������������������������������������������0000644�0002322�0002322�00000000550�14154362354�016654� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� function selectAll() { for(i=0;i<document.forms[0].elements.length;i++) { var e = document.forms[0].elements[i]; if(e.type == 'checkbox') { e.checked = true; } } } function unSelectAll() { for(i=0;i<document.forms[0].elements.length;i++) { var e = document.forms[0].elements[i]; if(e.type == 'checkbox') { e.checked = false; } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/js/muc.js�����������������������������������������������������������������������0000644�0002322�0002322�00000000337�14154362354�016353� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������// Show/Hide an element function sh(e) { if (document.getElementById(e).style.display=='none') { document.getElementById(e).style.display='block'; } else { document.getElementById(e).style.display='none'; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/���������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015563� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/uk.msg���������������������������������������������������������������������0000644�0002322�0002322�00000072777�14154362354�016736� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," встановив(ла) тему: "}. {"A friendly name for the node","Псевдонім для вузла"}. {"A password is required to enter this room","Щоб зайти в цю конференцію, необхідно ввести пароль"}. {"Accept","Прийняти"}. {"Access denied by service policy","Доступ заборонений політикою служби"}. {"Action on user","Дія над користувачем"}. {"Add Jabber ID","Додати Jabber ID"}. {"Add New","Додати"}. {"Add User","Додати користувача"}. {"Administration of ","Адміністрування "}. {"Administration","Адміністрування"}. {"Administrator privileges required","Необхідні права адміністратора"}. {"All activity","Вся статистика"}. {"All Users","Всі користувачі"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Чи дозволити цьому Jabber ID підписатись новини наданого вузла"}. {"Allow users to change the subject","Дозволити користувачам змінювати тему"}. {"Allow users to query other users","Дозволити iq-запити до користувачів"}. {"Allow users to send invites","Дозволити користувачам надсилати запрошення"}. {"Allow users to send private messages","Дозволити приватні повідомлення"}. {"Allow visitors to change nickname","Дозволити відвідувачам змінювати псевдонім"}. {"Allow visitors to send private messages to","Дозволити відвідувачам відсилати приватні повідомлення"}. {"Allow visitors to send status text in presence updates","Дозволити відвідувачам відсилати текст статусу в оновленнях присутності"}. {"Allow visitors to send voice requests","Дозволити відвідувачам надсилати голосові запрошення"}. {"Announcements","Сповіщення"}. {"April","квітня"}. {"August","серпня"}. {"Backup Management","Керування резервним копіюванням"}. {"Backup of ~p","Резервне копіювання ~p"}. {"Backup to File at ","Резервне копіювання в файл на "}. {"Backup","Резервне копіювання"}. {"Bad format","Неправильний формат"}. {"Birthday","День народження"}. {"CAPTCHA web page","Адреса капчі"}. {"Change Password","Змінити пароль"}. {"Change User Password","Змінити Пароль Користувача"}. {"Characters not allowed:","Заборонені символи:"}. {"Chatroom configuration modified","Конфігурація кімнати змінилась"}. {"Chatroom is created","Створено кімнату"}. {"Chatroom is destroyed","Знищено кімнату"}. {"Chatroom is started","Запущено кімнату"}. {"Chatroom is stopped","Зупинено кімнату"}. {"Chatrooms","Кімнати"}. {"Choose a username and password to register with this server","Виберіть назву користувача та пароль для реєстрації на цьому сервері"}. {"Choose storage type of tables","Оберіть тип збереження таблиць"}. {"Choose whether to approve this entity's subscription.","Вирішіть, чи задовольнити запит цього об'єкту на підписку"}. {"City","Місто"}. {"Commands","Команди"}. {"Conference room does not exist","Конференція не існує"}. {"Configuration of room ~s","Конфігурація кімнати ~s"}. {"Configuration","Конфігурація"}. {"Connected Resources:","Підключені ресурси:"}. {"Country","Країна"}. {"CPU Time:","Процесорний час:"}. {"Database Tables at ~p","Таблиці бази даних на ~p"}. {"Database Tables Configuration at ","Конфігурація таблиць бази даних на "}. {"Database","База даних"}. {"December","грудня"}. {"Default users as participants","Зробити користувачів учасниками за замовчуванням"}. {"Delete message of the day on all hosts","Видалити повідомлення дня на усіх хостах"}. {"Delete message of the day","Видалити повідомлення дня"}. {"Delete Selected","Видалити виділені"}. {"Delete User","Видалити Користувача"}. {"Deliver event notifications","Доставляти сповіщення про події"}. {"Deliver payloads with event notifications","Доставляти разом з повідомленнями про публікації самі публікації"}. {"Description:","Опис:"}. {"Disc only copy","Тільки диск"}. {"Dump Backup to Text File at ","Копіювання в текстовий файл на "}. {"Dump to Text File","Копіювання в текстовий файл"}. {"Edit Properties","Змінити параметри"}. {"Either approve or decline the voice request.","Підтвердить або відхилите голосовий запит"}. {"ejabberd MUC module","ejabberd MUC модуль"}. {"ejabberd Multicast service","Мультікаст ejabberd сервіс"}. {"ejabberd Publish-Subscribe module","Модуль ejabberd Публікації-Підписки"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams модуль"}. {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Веб-інтерфейс Адміністрування ejabberd"}. {"Elements","Елементи"}. {"Email","Електронна пошта"}. {"Enable logging","Включити журнал роботи"}. {"Enable message archiving","Ввімкнути архівацію повідомлень"}. {"End User Session","Закінчити Сеанс Користувача"}. {"Enter nickname you want to register","Введіть псевдонім, який ви хочете зареєструвати"}. {"Enter path to backup file","Введіть шлях до резервного файла"}. {"Enter path to jabberd14 spool dir","Введіть шлях до директорії спула jabberd14"}. {"Enter path to jabberd14 spool file","Введіть шлях до файла зі спула jabberd14"}. {"Enter path to text file","Введіть шлях до текстового файла"}. {"Enter the text you see","Введіть текст, що ви бачите"}. {"Error","Помилка"}. {"Exclude Jabber IDs from CAPTCHA challenge","Пропускати ці Jabber ID без CAPTCHA-запиту"}. {"Export all tables as SQL queries to a file:","Експорт усіх таблиць, як SQL запити, у файл"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Експорт даних всіх користувачів сервера до файлу PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Експорт даних користувачів домена до файлу PIEFXIS (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Помилка витягнення JID з вашого схвалення голосового запиту"}. {"Family Name","Прізвище"}. {"February","лютого"}. {"Friday","П'ятниця"}. {"From","Від кого"}. {"Full Name","Повне ім'я"}. {"Get Number of Online Users","Отримати Кількість Підключених Користувачів"}. {"Get Number of Registered Users","Отримати Кількість Зареєстрованих Користувачів"}. {"Get User Last Login Time","Отримати Час Останнього Підключення Користувача"}. {"Get User Password","Отримати Пароль Користувача"}. {"Get User Statistics","Отримати Статистику по Користувачу"}. {"Grant voice to this person?","Надати голос персоні?"}. {"Groups","Групи"}. {"Group","Група"}. {"has been banned","заборонили вхід в кімнату"}. {"has been kicked because of a system shutdown","вигнано з кімнати внаслідок зупинки системи"}. {"has been kicked because of an affiliation change","вигнано з кімнати внаслідок зміни рангу"}. {"has been kicked because the room has been changed to members-only","вигнано з кімнати тому, що вона стала тільки для учасників"}. {"has been kicked","вигнали з кімнати"}. {"Host","Хост"}. {"If you don't see the CAPTCHA image here, visit the web page.","Якщо ви не бачите зображення капчі, перейдіть за за цією адресою."}. {"Import Directory","Імпорт з директорії"}. {"Import File","Імпорт з файла"}. {"Import user data from jabberd14 spool file:","Імпорт користувачів з файла спула jabberd14:"}. {"Import User from File at ","Імпортування користувача з файла на "}. {"Import users data from a PIEFXIS file (XEP-0227):","Імпорт даних користовучів з файлу PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Імпорт користувачів з діректорії спула jabberd14:"}. {"Import Users from Dir at ","Імпортування користувача з директорії на "}. {"Import Users From jabberd14 Spool Files","Імпорт користувачів з jabberd14 файлів \"Spool\""}. {"Improper message type","Неправильний тип повідомлення"}. {"Incoming s2s Connections:","Вхідні s2s-з'єднання:"}. {"Incorrect password","Неправильний пароль"}. {"IP addresses","IP адреси"}. {"is now known as","змінив(ла) псевдонім на"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Не дозволяється відправляти помилкові повідомлення в кімнату. Учасник (~s) відправив помилкове повідомлення (~s), та був виганий з кімнати"}. {"It is not allowed to send private messages of type \"groupchat\"","Не дозволяється надсилати приватні повідомлення типу \"groupchat\""}. {"It is not allowed to send private messages to the conference","Не дозволяється надсилати приватні повідомлення в конференцію"}. {"It is not allowed to send private messages","Приватні повідомлення не дозволені"}. {"Jabber ID","Jabber ID"}. {"January","січня"}. {"joins the room","увійшов(ла) в кімнату"}. {"July","липня"}. {"June","червня"}. {"Last Activity","Останнє підключення"}. {"Last login","Останнє підключення"}. {"Last month","За останній місяць"}. {"Last year","За останній рік"}. {"leaves the room","вийшов(ла) з кімнати"}. {"List of rooms","Перелік кімнат"}. {"Low level update script","Низькорівневий сценарій поновлення"}. {"Make participants list public","Зробити список учасників видимим всім"}. {"Make room CAPTCHA protected","Зробити кімнату захищеною капчею"}. {"Make room members-only","Кімната тільки для зареєтрованых учасників"}. {"Make room moderated","Зробити кімнату модерованою"}. {"Make room password protected","Зробити кімнату захищеною паролем"}. {"Make room persistent","Зробити кімнату постійною"}. {"Make room public searchable","Зробити кімнату видимою всім"}. {"March","березня"}. {"Max # of items to persist","Максимальне число збережених публікацій"}. {"Max payload size in bytes","Максимальний розмір корисного навантаження в байтах"}. {"Maximum Number of Occupants","Максимальна кількість учасників"}. {"May","травня"}. {"Membership is required to enter this room","В цю конференцію можуть входити тільки її члени"}. {"Members:","Члени:"}. {"Memory","Пам'ять"}. {"Message body","Тіло повідомлення"}. {"Middle Name","По-батькові"}. {"Minimum interval between voice requests (in seconds)","Мінімальний інтервал між голосовими запитами (в секундах)"}. {"Moderator privileges required","Необхідні права модератора"}. {"Moderator","Модератор"}. {"Modified modules","Змінені модулі"}. {"Monday","Понеділок"}. {"Multicast","Мультікаст"}. {"Multi-User Chat","Багато-користувальницький чат"}. {"Name","Назва"}. {"Name:","Назва:"}. {"Never","Ніколи"}. {"New Password:","Новий Пароль:"}. {"Nickname Registration at ","Реєстрація псевдоніма на "}. {"Nickname ~s does not exist in the room","Псевдонім ~s в кімнаті відсутній"}. {"Nickname","Псевдонім"}. {"No body provided for announce message","Тіло оголошення має бути непустим"}. {"No Data","Немає даних"}. {"No limit","Без обмежень"}. {"Node ID","ID вузла"}. {"Node not found","Вузол не знайдено"}. {"Node ~p","Вузол ~p"}. {"Nodes","Вузли"}. {"None","Немає"}. {"Not Found","не знайдено"}. {"Notify subscribers when items are removed from the node","Повідомляти абонентів про видалення публікацій із збірника"}. {"Notify subscribers when the node configuration changes","Повідомляти абонентів про зміни в конфігурації збірника"}. {"Notify subscribers when the node is deleted","Повідомляти абонентів про видалення збірника"}. {"November","листопада"}. {"Number of occupants","Кількість присутніх"}. {"Number of online users","Кількість підключених користувачів"}. {"Number of registered users","Кількість зареєстрованих користувачів"}. {"October","грудня"}. {"Offline Messages","Офлайнові повідомлення"}. {"Offline Messages:","Офлайнові повідомлення:"}. {"OK","Продовжити"}. {"Old Password:","Старий пароль:"}. {"Online Users","Підключені користувачі"}. {"Online Users:","Підключені користувачі:"}. {"Online","Підключений"}. {"Only deliver notifications to available users","Доставляти повідомленнями тільки доступним користувачам"}. {"Only members may query archives of this room","Тільки модератори можуть запитувати архіви цієї кімнати"}. {"Only moderators and participants are allowed to change the subject in this room","Тільки модератори та учасники можуть змінювати тему в цій кімнаті"}. {"Only moderators are allowed to change the subject in this room","Тільки модератори можуть змінювати тему в цій кімнаті"}. {"Only moderators can approve voice requests","Тільки модератори можуть схвалювати голосові запити"}. {"Only occupants are allowed to send messages to the conference","Тільки присутнім дозволяється надсилати повідомленняя в конференцію"}. {"Only occupants are allowed to send queries to the conference","Тільки присутнім дозволяється відправляти запити в конференцію"}. {"Only service administrators are allowed to send service messages","Тільки адміністратор сервісу може надсилати службові повідомлення"}. {"Organization Name","Назва організації"}. {"Organization Unit","Відділ організації"}. {"Outgoing s2s Connections","Вихідні s2s-з'єднання"}. {"Outgoing s2s Connections:","Вихідні s2s-з'єднання:"}. {"Owner privileges required","Необхідні права власника"}. {"Packet","Пакет"}. {"Participant","Учасник"}. {"Password Verification","Перевірка Пароля"}. {"Password Verification:","Перевірка Пароля:"}. {"Password","Пароль"}. {"Password:","Пароль:"}. {"Path to Dir","Шлях до директорії"}. {"Path to File","Шлях до файла"}. {"Pending","Очікування"}. {"Period: ","Період"}. {"Persist items to storage","Зберегати публікації до сховища"}. {"Ping","Пінг"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Зауважте, що ця опція відповідає за резервне копіювання тільки вбудованної бази даних Mnesia. Якщо Ви також використовуєте інше сховище для даних (наприклад за допомогою модуля ODBC), то його резервне копіювання потрібно робити окремо."}. {"Please, wait for a while before sending new voice request","Будь ласка, почекайте деякий час перед тим, як знову відправляти голосовий запит"}. {"Pong","Понг"}. {"Present real Jabber IDs to","Зробити реальні Jabber ID учасників видимими"}. {"private, ","приватна, "}. {"Publish-Subscribe","Публікація-Підписка"}. {"PubSub subscriber request","Запит на підписку PubSub"}. {"Purge all items when the relevant publisher goes offline","Видалити всі елементи, коли особа, що їх опублікувала, вимикається від мережі"}. {"Queries to the conference members are not allowed in this room","Запити до користувачів в цій конференції заборонені"}. {"RAM and disc copy","ОЗП та диск"}. {"RAM copy","ОЗП"}. {"Really delete message of the day?","Насправді, видалити повідомлення дня?"}. {"Recipient is not in the conference room","Адресата немає в конференції"}. {"Registered Users","Зареєстровані користувачі"}. {"Registered Users:","Зареєстровані користувачі:"}. {"Register","Реєстрація"}. {"Remote copy","не зберігаеться локально"}. {"Remove All Offline Messages","Видалити всі офлайнові повідомлення"}. {"Remove User","Видалити користувача"}. {"Remove","Видалити"}. {"Replaced by new connection","Замінено новим з'єднанням"}. {"Resources","Ресурси"}. {"Restart Service","Перезапустити Сервіс"}. {"Restart","Перезапустити"}. {"Restore Backup from File at ","Відновлення з резервної копії на "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Відновити з бінарної резервної копії при наступному запуску (потребує менше пам'яті):"}. {"Restore binary backup immediately:","Відновити з бінарної резервної копії негайно:"}. {"Restore plain text backup immediately:","Відновити з текстової резервної копії негайно:"}. {"Restore","Відновлення з резервної копії"}. {"Roles for which Presence is Broadcasted","Ролі для яких поширюється наявність"}. {"Room Configuration","Конфігурація кімнати"}. {"Room creation is denied by service policy","Створювати конференцію заборонено політикою служби"}. {"Room description","Опис кімнати"}. {"Room Occupants","Учасники кімнати"}. {"Room title","Назва кімнати"}. {"Roster groups allowed to subscribe","Дозволені для підписки групи ростера"}. {"Roster size","Кількість контактів"}. {"RPC Call Error","Помилка виклику RPC"}. {"Running Nodes","Працюючі вузли"}. {"Saturday","Субота"}. {"Script check","Перевірка сценарію"}. {"Search Results for ","Результати пошуку в "}. {"Search users in ","Пошук користувачів в "}. {"Send announcement to all online users on all hosts","Надіслати сповіщення всім підключеним користувачам на всіх віртуальних серверах"}. {"Send announcement to all online users","Надіслати сповіщення всім підключеним користувачам"}. {"Send announcement to all users on all hosts","Надіслати сповіщення до усіх користувачів на усіх хостах"}. {"Send announcement to all users","Надіслати сповіщення всім користувачам"}. {"September","вересня"}. {"Server:","Сервер:"}. {"Set message of the day and send to online users","Встановити повідомлення дня та надіслати його підключеним користувачам"}. {"Set message of the day on all hosts and send to online users","Встановити повідомлення дня на всіх хостах та надійслати його підключеним користувачам"}. {"Shared Roster Groups","Спільні групи контактів"}. {"Show Integral Table","Показати інтегральну таблицю"}. {"Show Ordinary Table","Показати звичайну таблицю"}. {"Shut Down Service","Вимкнути Сервіс"}. {"Specify the access model","Визначити модель доступу"}. {"Specify the event message type","Вкажіть тип повідомлень зі сповіщеннями про події"}. {"Specify the publisher model","Умови публікації"}. {"Statistics of ~p","Статистика вузла ~p"}. {"Statistics","Статистика"}. {"Stopped Nodes","Зупинені вузли"}. {"Stop","Зупинити"}. {"Storage Type","Тип таблиці"}. {"Store binary backup:","Зберегти бінарну резервну копію:"}. {"Store plain text backup:","Зберегти текстову резервну копію:"}. {"Subject","Тема"}. {"Submitted","Відправлено"}. {"Submit","Відправити"}. {"Subscriber Address","Адреса абонента"}. {"Subscription","Підписка"}. {"Sunday","Неділя"}. {"That nickname is already in use by another occupant","Псевдонім зайнято кимось з присутніх"}. {"That nickname is registered by another person","Псевдонім зареєстровано кимось іншим"}. {"The CAPTCHA is valid.","Перевірку капчею закінчено успішно"}. {"The CAPTCHA verification has failed","Перевірку капчею не пройдено"}. {"The collections with which a node is affiliated","Колекція, до якої входить вузол"}. {"The password is too weak","Пароль надто простий"}. {"the password is","пароль:"}. {"There was an error creating the account: ","Помилка при створенні акаунту:"}. {"There was an error deleting the account: ","Помилка при видаленні акаунту: "}. {"This room is not anonymous","Ця кімната не анонімна"}. {"Thursday","Четвер"}. {"Time delay","Час затримки"}. {"Time","Час"}. {"Too many CAPTCHA requests","Надто багато CAPTCHA-запитів"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Забагато (~p) помилок авторизації з цієї IP адреси (~s). Адресу буде розблоковано о ~s UTC"}. {"Too many unacked stanzas","Занадто багато пакетів без відповідей"}. {"Total rooms","Всього кімнат"}. {"To","Кому"}. {"Traffic rate limit is exceeded","Швидкість передачі інформації було перевищено"}. {"Transactions Aborted:","Транзакції відмінені:"}. {"Transactions Committed:","Транзакції завершені:"}. {"Transactions Logged:","Транзакції запротокольовані:"}. {"Transactions Restarted:","Транзакції перезапущені:"}. {"Tuesday","Вівторок"}. {"Unable to generate a CAPTCHA","Нема можливості згенерувати капчу"}. {"Unauthorized","Не авторизовано"}. {"Unregister","Видалити"}. {"Update message of the day (don't send)","Оновити повідомлення дня (не надсилати)"}. {"Update message of the day on all hosts (don't send)","Оновити повідомлення дня на всіх хостах (не надсилати)"}. {"Update plan","План оновлення"}. {"Update ~p","Оновлення ~p"}. {"Update script","Сценарій поновлення"}. {"Update","Обновити"}. {"Uptime:","Час роботи:"}. {"User JID","JID Користувача"}. {"User Management","Управління Користувачами"}. {"Username:","Ім'я користувача:"}. {"Users are not allowed to register accounts so quickly","Користувачам не дозволено так часто реєструвати облікові записи"}. {"Users Last Activity","Статистика останнього підключення користувачів"}. {"Users","Користувачі"}. {"User","Користувач"}. {"Validate","Затвердити"}. {"vCard User Search","Пошук користувачів по vCard"}. {"Virtual Hosts","віртуальні хости"}. {"Visitors are not allowed to change their nicknames in this room","Відвідувачам не дозволяється змінювати псевдонім в цій кімнаті"}. {"Visitors are not allowed to send messages to all occupants","Відвідувачам не дозволяється надсилати повідомлення всім присутнім"}. {"Visitor","Відвідувач"}. {"Voice requests are disabled in this conference","Голосові запити відключені в цій конференції"}. {"Voice request","Голосовий запит"}. {"Wednesday","Середа"}. {"When to send the last published item","Коли надсилати останній опублікований елемент"}. {"Whether to allow subscriptions","Дозволяти підписку"}. {"You have been banned from this room","Вам заборонено входити в цю конференцію"}. {"You must fill in field \"Nickname\" in the form","Вам необхідно заповнити поле \"Псевдонім\" у формі"}. {"You need a client that supports x:data and CAPTCHA to register","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. {"You need a client that supports x:data to register the nickname","Для реєстрації псевдоніму необхідно використовувати клієнт з підтримкою x:data"}. {"You need an x:data capable client to search","Для пошуку необхідний клієнт із підтримкою x:data"}. {"Your active privacy list has denied the routing of this stanza.","Маршрутизація цієї строфи була відмінена активним списком приватності."}. {"Your contact offline message queue is full. The message has been discarded.","Черга повідомлень, що не були доставлені, переповнена. Повідомлення не було збережено."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ваші повідомлення до ~s блокуються. Для розблокування відвідайте ~s"}. �ejabberd-21.12/priv/msgs/vi.msg���������������������������������������������������������������������0000644�0002322�0002322�00000037363�14154362354�016725� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," đã đặt chủ đề thành: "}. {"Access denied by service policy","Sự truy cập bị chặn theo chính sách phục vụ"}. {"Action on user","Hành động đối với người sử dụng"}. {"Add Jabber ID","Thêm Jabber ID"}. {"Add New","Thêm Mới"}. {"Add User","Thêm Người Sử Dụng"}. {"Administration of ","Quản trị về "}. {"Administration","Quản trị"}. {"Administrator privileges required","Yêu cầu đặc quyền của nhà quản trị"}. {"All activity","Tất cả hoạt động"}. {"All Users","Tất Cả Người Sử Dụng"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Cho phép Jabber ID đăng ký nút môđun xuất bản đăng ký này không?"}. {"Allow users to query other users","Cho phép người sử dụng hỏi người sử dụng khác"}. {"Allow users to send invites","Cho phép người sử dụng gửi lời mời"}. {"Allow users to send private messages","Cho phép người sử dụng gửi thư riêng"}. {"Announcements","Thông báo"}. {"April","Tháng Tư"}. {"August","Tháng Tám"}. {"Backup Management","Quản lý Sao Lưu Dự Phòng"}. {"Backup to File at ","Sao lưu dự phòng ra Tập Tin tại"}. {"Backup","Sao lưu dự phòng"}. {"Bad format","Định dạng hỏng"}. {"Birthday","Ngày sinh"}. {"Change Password","Thay Đổi Mật Khẩu"}. {"Change User Password","Thay Đổi Mật Khẩu Người Sử Dụng"}. {"Chatroom configuration modified","Cấu hình phòng trò chuyện được chỉnh sửa"}. {"Chatrooms","Phòng trò chuyện"}. {"Choose a username and password to register with this server","Chọn một tên truy cập và mật khẩu để đăng ký với máy chủ này"}. {"Choose storage type of tables","Chọn loại bảng lưu trữ"}. {"Choose whether to approve this entity's subscription.","Chọn có nên chấp nhận sự đăng ký của đối tượng này không"}. {"City","Thành phố"}. {"Commands","Lệnh"}. {"Conference room does not exist","Phòng họp không tồn tại"}. {"Configuration","Cấu hình"}. {"Connected Resources:","Tài Nguyên Được Kết Nối:"}. {"Country","Quốc gia"}. {"CPU Time:","Thời Gian CPU:"}. {"Database Tables Configuration at ","Cấu Hình Bảng Cơ Sở Dữ Liệu tại"}. {"Database","Cơ sở dữ liệu"}. {"December","Tháng Mười Hai"}. {"Default users as participants","Người sử dụng mặc định là người tham dự"}. {"Delete message of the day on all hosts","Xóa thư trong ngày trên tất cả các máy chủ"}. {"Delete message of the day","Xóa thư trong ngày"}. {"Delete Selected","Tùy chọn Xóa được Chọn"}. {"Delete User","Xóa Người Sử Dụng"}. {"Deliver event notifications","Đưa ra các thông báo sự kiện"}. {"Deliver payloads with event notifications","Đưa ra thông tin dung lượng với các thông báo sự kiện"}. {"Description:","Miêu tả:"}. {"Disc only copy","Chỉ sao chép vào đĩa"}. {"Dump Backup to Text File at ","Kết Xuất Sao Lưu ra Tập Tin Văn Bản tại"}. {"Dump to Text File","Kết xuất ra Tập Tin Văn Bản"}. {"Edit Properties","Chỉnh Sửa Thuộc Tính"}. {"ejabberd MUC module","Môdun ejabberd MUC Bản quyền"}. {"ejabberd Publish-Subscribe module","Môdun ejabberd Xuất Bản-Đăng Ký Bản quyền"}. {"ejabberd SOCKS5 Bytestreams module","Môdun SOCKS5 Bytestreams Bản quyền"}. {"ejabberd vCard module","Môdun ejabberd vCard Bản quyền"}. {"Email","Email"}. {"Enable logging","Cho phép ghi nhật ký"}. {"End User Session","Kết Thúc Phiên Giao Dịch Người Sử Dụng"}. {"Enter nickname you want to register","Nhập bí danh bạn muốn đăng ký"}. {"Enter path to backup file","Nhập đường dẫn đến tập tin sao lưu dự phòng"}. {"Enter path to jabberd14 spool dir","Nhập đường dẫn đến thư mục spool jabberd14"}. {"Enter path to jabberd14 spool file","Nhập đường dẫn đến tập tin spool jabberd14"}. {"Enter path to text file","Nhập đường dẫn đến tập tin văn bản"}. {"Family Name","Họ"}. {"February","Tháng Hai"}. {"Friday","Thứ Sáu"}. {"From","Từ"}. {"Full Name","Tên Đầy Đủ"}. {"Get Number of Online Users","Nhận Số Người Sử Dụng Trực Tuyến"}. {"Get Number of Registered Users","Nhận Số Người Sử Dụng Đã Đăng Ký"}. {"Get User Last Login Time","Nhận Thời Gian Đăng Nhập Cuối Cùng Của Người Sử Dụng"}. {"Get User Password","Nhận Mật Khẩu Người Sử Dụng"}. {"Get User Statistics","Nhận Thông Tin Thống Kê Người Sử Dụng"}. {"Group","Nhóm"}. {"Groups","Nhóm"}. {"has been banned","đã bị cấm"}. {"has been kicked","đã bị đẩy ra khỏi"}. {"Host","Máy chủ"}. {"Import Directory","Nhập Thư Mục"}. {"Import File","Nhập Tập Tin"}. {"Import User from File at ","Nhập Người Sử Dụng từ Tập Tin tại"}. {"Import Users from Dir at ","Nhập Người Sử Dụng từ Thư Mục tại"}. {"Import Users From jabberd14 Spool Files","Nhập Người Sử Dụng Từ Các Tập Tin Spool jabberd14"}. {"Improper message type","Loại thư không phù hợp"}. {"Incorrect password","Mật khẩu sai"}. {"IP addresses","Địa chỉ IP"}. {"is now known as","bây giờ được biết như"}. {"It is not allowed to send private messages of type \"groupchat\"","Không được phép gửi những thư riêng loại \"groupchat\""}. {"It is not allowed to send private messages to the conference","Không được phép gửi những thư riêng đến phòng họp"}. {"Jabber ID","Jabber ID"}. {"January","Tháng Một"}. {"joins the room","tham gia phòng này"}. {"July","Tháng Bảy"}. {"June","Tháng Sáu"}. {"Last Activity","Hoạt Động Cuối Cùng"}. {"Last login","Đăng nhập lần cuối"}. {"Last month","Tháng trước"}. {"Last year","Năm trước"}. {"leaves the room","rời khỏi phòng này"}. {"Low level update script","Lệnh cập nhật mức độ thấp"}. {"Make participants list public","Tạo danh sách người tham dự công khai"}. {"Make room members-only","Tạo phòng chỉ cho phép tư cách thành viên tham gia"}. {"Make room password protected","Tạo phòng được bảo vệ bằng mật khẩu"}. {"Make room persistent","Tạo phòng bền vững"}. {"Make room public searchable","Tạo phòng có thể tìm kiếm công khai"}. {"March","Tháng Ba"}. {"Max # of items to persist","Số mục tối đa để lưu trữ"}. {"Max payload size in bytes","Kích thước dung lượng byte tối đa"}. {"Maximum Number of Occupants","Số Lượng Người Tham Dự Tối Đa"}. {"May","Tháng Năm"}. {"Members:","Thành viên:"}. {"Memory","Bộ Nhớ"}. {"Message body","Thân thư"}. {"Middle Name","Họ Đệm"}. {"Moderator privileges required","Yêu cầu đặc quyền của nhà điều phối"}. {"Monday","Thứ Hai"}. {"Name","Tên"}. {"Name:","Tên:"}. {"Never","Không bao giờ"}. {"Nickname Registration at ","Đăng Ký Bí Danh tại"}. {"Nickname ~s does not exist in the room","Bí danh ~s không tồn tại trong phòng này"}. {"Nickname","Bí danh"}. {"No body provided for announce message","Không có nội dung trong thư thông báo"}. {"No Data","Không Dữ Liệu"}. {"No limit","Không giới hạn"}. {"Node ID","ID Nút"}. {"Node not found","Nút không tìm thấy"}. {"Nodes","Nút"}. {"None","Không có"}. {"Notify subscribers when items are removed from the node","Thông báo cho người đăng ký khi nào các mục chọn bị gỡ bỏ khỏi nút"}. {"Notify subscribers when the node configuration changes","Thông báo cho người đăng ký khi nào cấu hình nút thay đổi"}. {"Notify subscribers when the node is deleted","Thông báo cho người đăng ký khi nào nút bị xóa bỏ"}. {"November","Tháng Mười Một"}. {"Number of occupants","Số người tham dự"}. {"Number of online users","Số người sử dụng trực tuyến"}. {"Number of registered users","Số người sử dụng đã đăng ký"}. {"October","Tháng Mười"}. {"Offline Messages","Thư Ngoại Tuyến"}. {"Offline Messages:","Thư Ngoại Tuyến:"}. {"OK","OK"}. {"Online Users","Người Sử Dụng Trực Tuyến"}. {"Online Users:","Người Sử Dụng Trực Tuyến:"}. {"Online","Trực tuyến"}. {"Only deliver notifications to available users","Chỉ gửi thông báo đến những người sử dụng hiện có"}. {"Only occupants are allowed to send messages to the conference","Chỉ có những đối tượng tham gia mới được phép gửi thư đến phòng họp"}. {"Only occupants are allowed to send queries to the conference","Chỉ có những đối tượng tham gia mới được phép gửi yêu cầu đến phòng họp"}. {"Only service administrators are allowed to send service messages","Chỉ có người quản trị dịch vụ mới được phép gửi những thư dịch vụ"}. {"Organization Name","Tên Tổ Chức"}. {"Organization Unit","Bộ Phận"}. {"Outgoing s2s Connections","Kết Nối Bên Ngoài s2s"}. {"Outgoing s2s Connections:","Kết Nối Bên Ngoài s2s:"}. {"Owner privileges required","Yêu cầu đặc quyền của người sở hữu"}. {"Packet","Gói thông tin"}. {"Password Verification","Kiểm Tra Mật Khẩu"}. {"Password","Mật Khẩu"}. {"Password:","Mật Khẩu:"}. {"Path to Dir","Đường Dẫn đến Thư Mục"}. {"Path to File","Đường dẫn đến Tập Tin"}. {"Pending","Chờ"}. {"Period: ","Giai đoạn: "}. {"Persist items to storage","Những mục cần để lưu trữ"}. {"Ping","Ping"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Jabber ID thực tế hiện hành đến"}. {"private, ","riêng,"}. {"Publish-Subscribe","Xuất Bản-Đăng Ký"}. {"PubSub subscriber request","Yêu cầu người đăng ký môđun Xuất Bản Đăng Ký"}. {"Queries to the conference members are not allowed in this room","Không được phép gửi các yêu cầu gửi đến các thành viên trong phòng họp này"}. {"RAM and disc copy","Sao chép vào RAM và đĩa"}. {"RAM copy","Sao chép vào RAM"}. {"Really delete message of the day?","Có thực sự xóa thư trong ngày này không?"}. {"Recipient is not in the conference room","Người nhận không có trong phòng họp"}. {"Registered Users","Người Sử Dụng Đã Đăng Ký"}. {"Registered Users:","Người Sử Dụng Đã Đăng Ký:"}. {"Remote copy","Sao chép từ xa"}. {"Remove User","Gỡ Bỏ Người Sử Dụng"}. {"Remove","Gỡ bỏ"}. {"Replaced by new connection","Được thay thế bởi kết nối mới"}. {"Resources","Nguồn tài nguyên"}. {"Restart Service","Khởi Động Lại Dịch Vụ"}. {"Restart","Khởi động lại"}. {"Restore Backup from File at ","Phục hồi Sao Lưu từ Tập Tin tại "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Khôi phục bản sao lưu dự phòng dạng nhị phân sau lần khởi động ejabberd kế tiếp (yêu cầu ít bộ nhớ hơn):"}. {"Restore binary backup immediately:","Khôi phục bản sao lưu dự phòng dạng nhị phận ngay lập tức:"}. {"Restore plain text backup immediately:","Khôi phục bản sao lưu dự phòng thuần văn bản ngay lập tức:"}. {"Restore","Khôi phục"}. {"Room Configuration","Cấu Hình Phòng"}. {"Room creation is denied by service policy","Việc tạo phòng bị ngăn lại theo chính sách dịch vụ"}. {"Room title","Tên phòng"}. {"Roster size","Kích thước bảng phân công"}. {"RPC Call Error","Lỗi Gọi RPC"}. {"Running Nodes","Nút Hoạt Động"}. {"Saturday","Thứ Bảy"}. {"Script check","Lệnh kiểm tra"}. {"Search Results for ","Kết Quả Tìm Kiếm cho "}. {"Search users in ","Tìm kiếm người sử dụng trong"}. {"Send announcement to all online users on all hosts","Gửi thông báo đến tất cả người sử dụng trực tuyến trên tất cả các máy chủ"}. {"Send announcement to all online users","Gửi thông báo đến tất cả người sử dụng trực tuyến"}. {"Send announcement to all users on all hosts","Gửi thông báo đến tất cả người sử dụng trên tất cả các máy chủ"}. {"Send announcement to all users","Gửi thông báo đến tất cả người sử dụng"}. {"September","Tháng Chín"}. {"Set message of the day and send to online users","Tạo lập thư trong ngày và gửi đến những người sử dụng trực tuyến"}. {"Set message of the day on all hosts and send to online users","Tạo lập thư trong ngày trên tất cả các máy chủ và gửi đến những người sử dụng trực tuyến"}. {"Shared Roster Groups","Nhóm Phân Công Chia Sẻ"}. {"Show Integral Table","Hiển Thị Bảng Đầy Đủ"}. {"Show Ordinary Table","Hiển Thị Bảng Thường"}. {"Shut Down Service","Tắt Dịch Vụ"}. {"Specify the access model","Xác định mô hình truy cập"}. {"Specify the publisher model","Xác định mô hình nhà xuất bản"}. {"Statistics of ~p","Thống kê về ~p"}. {"Statistics","Số liệu thống kê"}. {"Stop","Dừng"}. {"Stopped Nodes","Nút Dừng"}. {"Storage Type","Loại Lưu Trữ"}. {"Store binary backup:","Lưu dữ liệu sao lưu dạng nhị phân:"}. {"Store plain text backup:","Khôi phục bản sao lưu dự phòng thuần văn bản"}. {"Subject","Tiêu đề"}. {"Submit","Gửi"}. {"Submitted","Đã gửi"}. {"Subscriber Address","Địa Chỉ Người Đăng Ký"}. {"Subscription","Đăng ký"}. {"Sunday","Chủ Nhật"}. {"the password is","mật khẩu là"}. {"This room is not anonymous","Phòng này không nặc danh"}. {"Thursday","Thứ Năm"}. {"Time delay","Thời gian trì hoãn"}. {"Time","Thời Gian"}. {"To","Đến"}. {"Traffic rate limit is exceeded","Quá giới hạn tỷ lệ lưu lượng truyền tải"}. {"Transactions Aborted:","Giao Dịch Hủy Bỏ:"}. {"Transactions Committed:","Giao Dịch Được Cam Kết:"}. {"Transactions Logged:","Giao Dịch Được Ghi Nhận:"}. {"Transactions Restarted:","Giao Dịch Khởi Động Lại:"}. {"Tuesday","Thứ Ba"}. {"Update message of the day (don't send)","Cập nhật thư trong ngày (không gửi)"}. {"Update message of the day on all hosts (don't send)","Cập nhật thư trong ngày trên tất cả các máy chủ (không gửi)"}. {"Update plan","Kế hoạch cập nhật"}. {"Update script","Cập nhận lệnh"}. {"Update","Cập Nhật"}. {"Uptime:","Thời gian tải lên:"}. {"User Management","Quản Lý Người Sử Dụng"}. {"User","Người sử dụng"}. {"Users Last Activity","Hoạt Động Cuối Cùng Của Người Sử Dụng"}. {"Users","Người sử dụng"}. {"Validate","Xác nhận hợp lệ"}. {"vCard User Search","Tìm Kiếm Người Sử Dụng vCard"}. {"Virtual Hosts","Máy Chủ Ảo"}. {"Visitors are not allowed to send messages to all occupants","Người ghé thăm không được phép gửi thư đến tất cả các người tham dự"}. {"Wednesday","Thứ Tư"}. {"When to send the last published item","Khi cần gửi mục được xuất bản cuối cùng"}. {"Whether to allow subscriptions","Xác định nên cho phép đăng ký không"}. {"You have been banned from this room","Bạn bị cấm tham gia phòng này"}. {"You must fill in field \"Nickname\" in the form","Bạn phải điền thông tin vào ô \"Nickname\" trong biểu mẫu này"}. {"You need an x:data capable client to search","Bạn cần có một trình ứng dụng khách hỗ trợ định dạng dữ liệu x: để tìm kiếm"}. {"Your contact offline message queue is full. The message has been discarded.","Danh sách chờ thư liên lạc ngoại tuyến của bạn đã đầy. Thư này đã bị loại bỏ."}. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/zh.msg���������������������������������������������������������������������0000644�0002322�0002322�00000121555�14154362354�016725� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (在字段末添加*来匹配子串)"}. {" has set the subject to: "," 已将标题设置为: "}. {"# participants","# 个参与人"}. {"A description of the node","该节点的描述"}. {"A friendly name for the node","该节点的友好名称"}. {"A password is required to enter this room","进入此房间需要密码"}. {"A Web Page","网页"}. {"Accept","接受"}. {"Access denied by service policy","访问被服务策略拒绝"}. {"Access model of authorize","授权的访问模型"}. {"Access model of open","开启的访问模型"}. {"Access model of presence","状态的访问模型"}. {"Access model of roster","花名册的访问模型"}. {"Access model of whitelist","白名单的访问模型"}. {"Access model","访问模型"}. {"Account doesn't exist","账号不存在"}. {"Action on user","对用户的动作"}. {"Add Jabber ID","添加Jabber ID"}. {"Add New","添加新用户"}. {"Add User","添加用户"}. {"Administration of ","管理 "}. {"Administration","管理"}. {"Administrator privileges required","需要管理员权限"}. {"All activity","所有活动"}. {"All Users","所有用户"}. {"Allow subscription","允许订阅"}. {"Allow this Jabber ID to subscribe to this pubsub node?","允许该Jabber ID订阅该pubsub节点?"}. {"Allow this person to register with the room?","允许此人注册到该房间?"}. {"Allow users to change the subject","允许用户更改主题"}. {"Allow users to query other users","允许用户查询其它用户"}. {"Allow users to send invites","允许用户发送邀请"}. {"Allow users to send private messages","允许用户发送私聊消息"}. {"Allow visitors to change nickname","允许用户更改昵称"}. {"Allow visitors to send private messages to","允许访客发送私聊消息至"}. {"Allow visitors to send status text in presence updates","更新在线状态时允许用户发送状态文本"}. {"Allow visitors to send voice requests","允许访客发送声音请求"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","与定义房间会员资格相关联的LDAP群组; 按群组特定于实现或特定于部署的定义, 应该是一个LDAP专有名称."}. {"Announcements","通知"}. {"Answer associated with a picture","与图片关联的回答"}. {"Answer associated with a video","与视频关联的回答"}. {"Answer associated with speech","与讲话关联的回答"}. {"Answer to a question","问题的回答"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","指定花名册群组中的人可以订阅并检索内容项"}. {"Anyone may associate leaf nodes with the collection","任何人都可以将叶子节点与集合关联"}. {"Anyone may publish","任何人都可以发布"}. {"Anyone may subscribe and retrieve items","任何人都可以订阅和检索内容项"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","对全部或来源进行了状态订阅的任何人均可订阅并检索内容项"}. {"Anyone with Voice","任何带声音的人"}. {"Anyone","任何人"}. {"April","四月"}. {"Attribute 'channel' is required for this request","此请求要求'频道'属性"}. {"Attribute 'id' is mandatory for MIX messages","对MIX消息, 'id' 属性为必填项"}. {"Attribute 'jid' is not allowed here","此处不允许 'jid' 属性"}. {"Attribute 'node' is not allowed here","此处不允许 'node' 属性"}. {"Attribute 'to' of stanza that triggered challenge","触发挑战一节的 'to' 属性"}. {"August","八月"}. {"Automatic node creation is not enabled","未启用自动节点创建"}. {"Backup Management","备份管理"}. {"Backup of ~p","~p的备份"}. {"Backup to File at ","备份文件位于 "}. {"Backup","备份"}. {"Bad format","格式错误"}. {"Birthday","出生日期"}. {"Both the username and the resource are required","用户名和资源均为必填项"}. {"Bytestream already activated","字节流已经被激活"}. {"Cannot remove active list","无法移除活跃列表"}. {"Cannot remove default list","无法移除缺省列表"}. {"CAPTCHA web page","验证码网页"}. {"Challenge ID","挑战ID"}. {"Change Password","更改密码"}. {"Change User Password","更改用户密码"}. {"Changing password is not allowed","不允许修改密码"}. {"Changing role/affiliation is not allowed","不允许修改角色/单位"}. {"Channel already exists","频道已存在"}. {"Channel does not exist","频道不存在"}. {"Channels","频道"}. {"Characters not allowed:","不允许字符:"}. {"Chatroom configuration modified","聊天室配置已修改"}. {"Chatroom is created","聊天室已被创建"}. {"Chatroom is destroyed","聊天室已被销毁"}. {"Chatroom is started","聊天室已被启动"}. {"Chatroom is stopped","聊天室已被停用"}. {"Chatrooms","聊天室"}. {"Choose a username and password to register with this server","请选择在此服务器上注册所需的用户名和密码"}. {"Choose storage type of tables","请选择表格的存储类型"}. {"Choose whether to approve this entity's subscription.","选择是否允许该实体的订阅."}. {"City","城市"}. {"Client acknowledged more stanzas than sent by server","客户端确认的节数多于服务器发送的节数"}. {"Commands","命令"}. {"Conference room does not exist","会议室不存在"}. {"Configuration of room ~s","房间~s的配置"}. {"Configuration","配置"}. {"Connected Resources:","已连接资源:"}. {"Contact Addresses (normally, room owner or owners)","联系人地址 (通常为房间持有人)"}. {"Country","国家"}. {"CPU Time:","CPU时间:"}. {"Current Discussion Topic","当前讨论话题"}. {"Database failure","数据库失败"}. {"Database Tables at ~p","位于~p的数据库表"}. {"Database Tables Configuration at ","数据库表格配置位于 "}. {"Database","数据库"}. {"December","十二月"}. {"Default users as participants","用户默认被视为参与人"}. {"Delete content","删除内容"}. {"Delete message of the day on all hosts","删除所有主机上的每日消息"}. {"Delete message of the day","删除每日消息"}. {"Delete Selected","删除已选内容"}. {"Delete table","删除表格"}. {"Delete User","删除用户"}. {"Deliver event notifications","传递事件通知"}. {"Deliver payloads with event notifications","用事件通告传输有效负载"}. {"Description:","描述:"}. {"Disc only copy","仅磁盘复制"}. {"'Displayed groups' not added (they do not exist!): ","'显示的群组' 未被添加 (它们不存在!): "}. {"Displayed:","已显示:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","不要将密码告诉任何人, 就算是XMPP服务器的管理员也不可以."}. {"Dump Backup to Text File at ","将备份转储到位于以下位置的文本文件 "}. {"Dump to Text File","转储到文本文件"}. {"Duplicated groups are not allowed by RFC6121","按照RFC6121的规则,不允许有重复的群组"}. {"Dynamically specify a replyto of the item publisher","为项目发布者动态指定一个 replyto"}. {"Edit Properties","编辑属性"}. {"Either approve or decline the voice request.","接受或拒绝声音请求."}. {"ejabberd HTTP Upload service","ejabberd HTTP 上传服务"}. {"ejabberd MUC module","ejabberd MUC 模块"}. {"ejabberd Multicast service","ejabberd多重映射服务"}. {"ejabberd Publish-Subscribe module","ejabberd 发行-订阅模块"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 字节流模块"}. {"ejabberd vCard module","ejabberd vCard模块"}. {"ejabberd Web Admin","ejabberd网页管理"}. {"ejabberd","ejabberd"}. {"Elements","元素"}. {"Email Address","电邮地址"}. {"Email","电子邮件"}. {"Enable logging","启用服务器端聊天记录"}. {"Enable message archiving","启用消息归档"}. {"Enabling push without 'node' attribute is not supported","不支持未使用'node'属性就开启推送"}. {"End User Session","结束用户会话"}. {"Enter nickname you want to register","请输入您想要注册的昵称"}. {"Enter path to backup file","请输入备份文件的路径"}. {"Enter path to jabberd14 spool dir","请输入jabberd14 spool目录的路径"}. {"Enter path to jabberd14 spool file","请输入 jabberd14 spool 文件的路径"}. {"Enter path to text file","请输入文本文件的路径"}. {"Enter the text you see","请输入您所看到的文本"}. {"Erlang XMPP Server","Erlang XMPP 服务器"}. {"Error","错误"}. {"Exclude Jabber IDs from CAPTCHA challenge","从验证码挑战中排除Jabber ID"}. {"Export all tables as SQL queries to a file:","将所有表以SQL查询语句导出到文件:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","将服务器上所有用户的数据导出到 PIEFXIS 文件 (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","将某主机的用户数据导出到 PIEFXIS 文件 (XEP-0227):"}. {"External component failure","外部组件失败"}. {"External component timeout","外部组件超时"}. {"Failed to activate bytestream","激活字节流失败"}. {"Failed to extract JID from your voice request approval","无法从你的声音请求确认信息中提取JID"}. {"Failed to map delegated namespace to external component","未能将代理命名空间映射到外部组件"}. {"Failed to parse HTTP response","HTTP响应解析失败"}. {"Failed to process option '~s'","选项'~s'处理失败"}. {"Family Name","姓氏"}. {"FAQ Entry","常见问题入口"}. {"February","二月"}. {"File larger than ~w bytes","文件大于 ~w 字节"}. {"Fill in the form to search for any matching XMPP User","填充表单来搜索任何匹配的XMPP用户"}. {"Friday","星期五"}. {"From ~ts","来自 ~ts"}. {"From","从"}. {"Full List of Room Admins","房间管理员完整列表"}. {"Full List of Room Owners","房间持有人完整列表"}. {"Full Name","全名"}. {"Get Number of Online Users","获取在线用户数"}. {"Get Number of Registered Users","获取注册用户数"}. {"Get Pending","获取挂起"}. {"Get User Last Login Time","获取用户上次登陆时间"}. {"Get User Password","获取用户密码"}. {"Get User Statistics","获取用户统计"}. {"Given Name","中间名"}. {"Grant voice to this person?","为此人授权声音?"}. {"Groups that will be displayed to the members","将显示给会员的群组"}. {"Groups","组"}. {"Group","组"}. {"has been banned","已被禁止"}. {"has been kicked because of a system shutdown","因系统关机而被踢出"}. {"has been kicked because of an affiliation change","因联属关系改变而被踢出"}. {"has been kicked because the room has been changed to members-only","因该房间改为只对会员开放而被踢出"}. {"has been kicked","已被踢出"}. {"Host unknown","主人未知"}. {"Host","主机"}. {"HTTP File Upload","HTTP文件上传"}. {"Idle connection","空闲的连接"}. {"If you don't see the CAPTCHA image here, visit the web page.","如果您在这里没有看到验证码图片, 请访问网页."}. {"Import Directory","导入目录"}. {"Import File","导入文件"}. {"Import user data from jabberd14 spool file:","从 jabberd14 Spool 文件导入用户数据:"}. {"Import User from File at ","从以下位置的文件导入用户 "}. {"Import users data from a PIEFXIS file (XEP-0227):","从 PIEFXIS 文件 (XEP-0227) 导入用户数据:"}. {"Import users data from jabberd14 spool directory:","从jabberd14 Spool目录导入用户数据:"}. {"Import Users from Dir at ","从以下位置目录导入用户 "}. {"Import Users From jabberd14 Spool Files","从 jabberd14 Spool 文件导入用户"}. {"Improper domain part of 'from' attribute","不恰当的'from'属性域名部分"}. {"Improper message type","不恰当的消息类型"}. {"Incoming s2s Connections:","入站 s2s 连接:"}. {"Incorrect CAPTCHA submit","提交的验证码不正确"}. {"Incorrect data form","数据形式不正确"}. {"Incorrect password","密码不正确"}. {"Incorrect value of 'action' attribute","'action' 属性的值不正确"}. {"Incorrect value of 'action' in data form","数据表单中 'action' 的值不正确"}. {"Incorrect value of 'path' in data form","数据表单中 'path' 的值不正确"}. {"Insufficient privilege","权限不足"}. {"Internal server error","内部服务器错误"}. {"Invalid 'from' attribute in forwarded message","转发的信息中 'from' 属性的值无效"}. {"Invalid node name","无效的节点名称"}. {"Invalid 'previd' value","无效的 'previd' 值"}. {"Invitations are not allowed in this conference","此会议不允许邀请"}. {"IP addresses","IP地址"}. {"is now known as","现在称呼为"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","不允许将错误消息发送到该房间. 参与者(~s)已发送过一条消息(~s)并已被踢出房间"}. {"It is not allowed to send private messages of type \"groupchat\"","\"群组聊天\"类型不允许发送私聊消息"}. {"It is not allowed to send private messages to the conference","不允许向会议发送私聊消息"}. {"It is not allowed to send private messages","不可以发送私聊消息"}. {"Jabber ID","Jabber ID"}. {"January","一月"}. {"JID normalization denied by service policy","JID规范化被服务策略拒绝"}. {"JID normalization failed","JID规范化失败"}. {"joins the room","加入房间"}. {"July","七月"}. {"June","六月"}. {"Just created","刚刚创建"}. {"Label:","标签:"}. {"Last Activity","上次活动"}. {"Last login","上次登陆"}. {"Last message","最近消息"}. {"Last month","上个月"}. {"Last year","上一年"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","文本的SHA-256哈希的最低有效位应等于十六进制标签"}. {"leaves the room","离开房间"}. {"List of rooms","房间列表"}. {"Logging","正在记录"}. {"Low level update script","低级别更新脚本"}. {"Make participants list public","公开参与人列表"}. {"Make room CAPTCHA protected","保护房间验证码"}. {"Make room members-only","设置房间只接收会员"}. {"Make room moderated","设置房间只接收主持人"}. {"Make room password protected","进入此房间需要密码"}. {"Make room persistent","永久保存该房间"}. {"Make room public searchable","使房间可被公开搜索"}. {"Malformed username","用户名无效"}. {"MAM preference modification denied by service policy","MAM偏好被服务策略拒绝"}. {"March","三月"}. {"Max payload size in bytes","最大有效负载字节数"}. {"Maximum file size","最大文件大小"}. {"Maximum Number of History Messages Returned by Room","房间返回的历史消息最大值"}. {"Maximum number of items to persist","持久化内容的最大条目数"}. {"Maximum Number of Occupants","允许的与会人最大数"}. {"May","五月"}. {"Members not added (inexistent vhost!): ","成员未添加 (不存在的vhost!): "}. {"Membership is required to enter this room","进入此房间需要会员身份"}. {"Members:","会员:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","记住你的密码, 或将其记到纸上并放于安全位置. 如果你忘记了密码, XMPP也没有自动恢复密码的方式."}. {"Memory","内存"}. {"Mere Availability in XMPP (No Show Value)","仅XMPP中的可用性 (不显示值)"}. {"Message body","消息主体"}. {"Message not found in forwarded payload","转发的有效载荷中找不到消息"}. {"Messages from strangers are rejected","陌生人的消息会被拒绝"}. {"Messages of type headline","标题类型的消息"}. {"Messages of type normal","普通类型的消息"}. {"Middle Name","中间名"}. {"Minimum interval between voice requests (in seconds)","声音请求的最小间隔(以秒为单位)"}. {"Moderator privileges required","需要主持人权限"}. {"Moderators Only","仅限主持人"}. {"Moderator","主持人"}. {"Modified modules","被修改模块"}. {"Module failed to handle the query","模块未能处理查询"}. {"Monday","星期一"}. {"Multicast","多重映射"}. {"Multiple <item/> elements are not allowed by RFC6121","按照 RFC6121,多个 <item/> 元素是不允许的"}. {"Multi-User Chat","多用户聊天"}. {"Name in the rosters where this group will be displayed","花名册中将显示的该分组的名称"}. {"Name","姓名"}. {"Name:","姓名:"}. {"Natural Language for Room Discussions","房间讨论的自然语言"}. {"Natural-Language Room Name","自然语言房间名称"}. {"Neither 'jid' nor 'nick' attribute found","属性 'jid' 或 'nick' 均未发现"}. {"Neither 'role' nor 'affiliation' attribute found","属性 'role' 或 'affiliation' 均未发现"}. {"Never","从未"}. {"New Password:","新密码:"}. {"Nickname can't be empty","昵称不能为空"}. {"Nickname Registration at ","昵称注册于 "}. {"Nickname ~s does not exist in the room","昵称~s不在该房间"}. {"Nickname","昵称"}. {"No address elements found","没有找到地址的各元素"}. {"No addresses element found","没有找到各地址的元素"}. {"No 'affiliation' attribute found","未发现 'affiliation' 属性"}. {"No available resource found","没发现可用资源"}. {"No body provided for announce message","通知消息无正文内容"}. {"No child elements found","没有找到子元素"}. {"No data form found","没有找到数据表单"}. {"No Data","没有数据"}. {"No features available","没有可用特征"}. {"No <forwarded/> element found","未找到 <forwarded/> 元素"}. {"No hook has processed this command","没有任何钩子已处理此命令"}. {"No info about last activity found","未找到上次活动的信息"}. {"No 'item' element found","没有找到 'item' 元素"}. {"No items found in this query","此查询中没发现任何项"}. {"No limit","不限"}. {"No module is handling this query","没有正在处理此查询的模块"}. {"No node specified","无指定节点"}. {"No 'password' found in data form","数据表单中未发现 'password'"}. {"No 'password' found in this query","此查询中未发现 'password'"}. {"No 'path' found in data form","数据表单中未发现 'path'"}. {"No pending subscriptions found","未发现挂起的订阅"}. {"No privacy list with this name found","未找到带此名称的隐私列表"}. {"No private data found in this query","此查询中未发现私有数据"}. {"No running node found","没有找到运行中的节点"}. {"No services available","无可用服务"}. {"No statistics found for this item","未找到此项的统计数据"}. {"No 'to' attribute found in the invitation","邀请中未发现 'to' 标签"}. {"Nobody","没有人"}. {"Node already exists","节点已存在"}. {"Node ID","节点ID"}. {"Node index not found","没有找到节点索引"}. {"Node not found","没有找到节点"}. {"Node ~p","节点~p"}. {"Nodeprep has failed","Nodeprep 已失效"}. {"Nodes","节点"}. {"Node","节点"}. {"None","无"}. {"Not allowed","不允许"}. {"Not Found","没有找到"}. {"Not subscribed","未订阅"}. {"Notify subscribers when items are removed from the node","当从节点删除内容条目时通知订阅人"}. {"Notify subscribers when the node configuration changes","当节点设置改变时通知订阅人"}. {"Notify subscribers when the node is deleted","当节点被删除时通知订阅人"}. {"November","十一月"}. {"Number of answers required","需要的回答数量"}. {"Number of occupants","驻留人数"}. {"Number of Offline Messages","离线消息数量"}. {"Number of online users","在线用户数"}. {"Number of registered users","注册用户数"}. {"Occupants are allowed to invite others","允许成员邀请其他人"}. {"Occupants May Change the Subject","成员可以修改主题"}. {"October","十月"}. {"Offline Messages","离线消息"}. {"Offline Messages:","离线消息:"}. {"OK","确定"}. {"Old Password:","旧密码:"}. {"Online Users","在线用户"}. {"Online Users:","在线用户:"}. {"Online","在线"}. {"Only admins can see this","仅管理员可以看见此内容"}. {"Only collection node owners may associate leaf nodes with the collection","只有集合节点所有者可以将叶子节点与集合关联"}. {"Only deliver notifications to available users","仅将通知发送给可发送的用户"}. {"Only <enable/> or <disable/> tags are allowed","仅允许 <enable/> 或 <disable/> 标签"}. {"Only <list/> element is allowed in this query","此查询中只允许 <list/> 元素"}. {"Only members may query archives of this room","只有会员可以查询本房间的存档"}. {"Only moderators and participants are allowed to change the subject in this room","只有主持人和参与人可以在此房间里更改主题"}. {"Only moderators are allowed to change the subject in this room","只有主持人可以在此房间里更改主题"}. {"Only moderators can approve voice requests","仅主持人能确认声音请求"}. {"Only occupants are allowed to send messages to the conference","只有与会人可以向大会发送消息"}. {"Only occupants are allowed to send queries to the conference","只有与会人可以向大会发出查询请求"}. {"Only publishers may publish","只有发布人可以发布"}. {"Only service administrators are allowed to send service messages","只有服务管理员可以发送服务消息"}. {"Only those on a whitelist may associate leaf nodes with the collection","仅白名单用户可以将叶节点与集合关联"}. {"Only those on a whitelist may subscribe and retrieve items","仅白名单用户可以订阅和检索内容项"}. {"Organization Name","组织名称"}. {"Organization Unit","组织单位"}. {"Outgoing s2s Connections","出站 s2s 连接"}. {"Outgoing s2s Connections:","出站 s2s 连接:"}. {"Owner privileges required","需要持有人权限"}. {"Packet relay is denied by service policy","包中继被服务策略拒绝"}. {"Packet","数据包"}. {"Participant","参与人"}. {"Password Verification:","密码确认:"}. {"Password Verification","确认密码"}. {"Password","密码"}. {"Password:","密码:"}. {"Path to Dir","目录的路径"}. {"Path to File","文件路径"}. {"Payload type","有效载荷类型"}. {"Pending","挂起"}. {"Period: ","持续时间: "}. {"Persist items to storage","持久化内容条目"}. {"Persistent","永久"}. {"Ping query is incorrect","Ping 查询不正确"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","注意:这些选项仅将备份内置的 Mnesia 数据库. 如果您正在使用 ODBC 模块, 您还需要分别备份您的数据库."}. {"Please, wait for a while before sending new voice request","请稍后再发送新的声音请求"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","按照 RFC6121, 不允许处理 'ask' 属性"}. {"Present real Jabber IDs to","将真实Jabber ID显示给"}. {"Previous session not found","上一个会话未找到"}. {"Previous session PID has been killed","上一个会话的PID已被杀掉"}. {"Previous session PID has exited","上一个会话的PID已退出"}. {"Previous session PID is dead","上一个会话的PID已死"}. {"Previous session timed out","上一个会话已超时"}. {"private, ","保密, "}. {"Public","公开"}. {"Publish model","发布模型"}. {"Publish-Subscribe","发布-订阅"}. {"PubSub subscriber request","PubSub订阅人请求"}. {"Purge all items when the relevant publisher goes offline","相关发布人离线后清除所有选项"}. {"Push record not found","没有找到推送记录"}. {"Queries to the conference members are not allowed in this room","本房间不可以查询会议成员信息"}. {"Query to another users is forbidden","禁止查询其他用户"}. {"RAM and disc copy","内存与磁盘复制"}. {"RAM copy","内存(RAM)复制"}. {"Really delete message of the day?","确实要删除每日消息吗?"}. {"Receive notification from all descendent nodes","接收所有后代节点的通知"}. {"Receive notification from direct child nodes only","仅接收所有直接子节点的通知"}. {"Receive notification of new items only","仅接收新内容项的通知"}. {"Receive notification of new nodes only","仅接收新节点的通知"}. {"Recipient is not in the conference room","接收人不在会议室"}. {"Register an XMPP account","注册XMPP帐户"}. {"Registered Users","注册用户"}. {"Registered Users:","注册用户:"}. {"Register","注册"}. {"Remote copy","远程复制"}. {"Remove All Offline Messages","移除所有离线消息"}. {"Remove User","删除用户"}. {"Remove","移除"}. {"Replaced by new connection","被新的连接替换"}. {"Request has timed out","请求已超时"}. {"Request is ignored","请求被忽略"}. {"Requested role","请求的角色"}. {"Resources","资源"}. {"Restart Service","重启服务"}. {"Restart","重启"}. {"Restore Backup from File at ","从以下位置的文件恢复备份 "}. {"Restore binary backup after next ejabberd restart (requires less memory):","在下次 ejabberd 重启后恢复二进制备份(需要的内存更少):"}. {"Restore binary backup immediately:","立即恢复二进制备份:"}. {"Restore plain text backup immediately:","立即恢复普通文本备份:"}. {"Restore","恢复"}. {"Roles and Affiliations that May Retrieve Member List","可能会检索成员列表的角色和从属关系"}. {"Roles for which Presence is Broadcasted","被广播状态的角色"}. {"Roles that May Send Private Messages","可以发送私聊消息的角色"}. {"Room Configuration","房间配置"}. {"Room creation is denied by service policy","创建房间被服务策略拒绝"}. {"Room description","房间描述"}. {"Room Occupants","房间人数"}. {"Room terminates","房间终止"}. {"Room title","房间标题"}. {"Roster groups allowed to subscribe","允许订阅的花名册组"}. {"Roster of ~ts","~ts的花名册"}. {"Roster size","花名册大小"}. {"Roster:","花名册:"}. {"RPC Call Error","RPC 调用错误"}. {"Running Nodes","运行中的节点"}. {"~s invites you to the room ~s","~s邀请你到房间~s"}. {"Saturday","星期六"}. {"Script check","脚本检查"}. {"Search from the date","从日期搜索"}. {"Search Results for ","搜索结果属于关键词 "}. {"Search the text","搜索文本"}. {"Search until the date","搜索截至日期"}. {"Search users in ","在以下位置搜索用户 "}. {"Select All","全选"}. {"Send announcement to all online users on all hosts","发送通知给所有主机的在线用户"}. {"Send announcement to all online users","发送通知给所有在线用户"}. {"Send announcement to all users on all hosts","发送通知给所有主机上的所有用户"}. {"Send announcement to all users","发送通知给所有用户"}. {"September","九月"}. {"Server:","服务器:"}. {"Service list retrieval timed out","服务列表检索超时"}. {"Session state copying timed out","会话状态复制超时"}. {"Set message of the day and send to online users","设定每日消息并发送给所有在线用户"}. {"Set message of the day on all hosts and send to online users","设置所有主机上的每日消息并发送给在线用户"}. {"Shared Roster Groups","共享的花名册组群"}. {"Show Integral Table","显示完整列表"}. {"Show Ordinary Table","显示普通列表"}. {"Shut Down Service","关闭服务"}. {"SOCKS5 Bytestreams","SOCKS5 字节流"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","某些 XMPP 客户端可以在计算机里存储你的密码. 处于安全考虑, 请仅在你的个人计算机里使用该功能."}. {"Specify the access model","指定访问范例"}. {"Specify the event message type","指定事件消息类型"}. {"Specify the publisher model","指定发布人范例"}. {"Stanza ID","节ID"}. {"Statically specify a replyto of the node owner(s)","静态指定节点所有者的回复"}. {"Statistics of ~p","~p的统计"}. {"Statistics","统计"}. {"Stopped Nodes","已经停止的节点"}. {"Stop","停止"}. {"Storage Type","存储类型"}. {"Store binary backup:","存储为二进制备份:"}. {"Store plain text backup:","存储为普通文本备份:"}. {"Stream management is already enabled","流管理已启用"}. {"Stream management is not enabled","流管理未启用"}. {"Subject","标题"}. {"Submitted","已提交"}. {"Submit","提交"}. {"Subscriber Address","订阅人地址"}. {"Subscribers may publish","订阅人可以发布"}. {"Subscription requests must be approved and only subscribers may retrieve items","订阅请求必须得到批准, 只有订阅人才能检索项目"}. {"Subscriptions are not allowed","不允许订阅"}. {"Subscription","订阅"}. {"Sunday","星期天"}. {"Text associated with a picture","与图片相关的文字"}. {"Text associated with a sound","与声音相关的文字"}. {"Text associated with a video","与视频相关的文字"}. {"Text associated with speech","与语音相关的文字"}. {"That nickname is already in use by another occupant","该昵称已被另一用户使用"}. {"That nickname is registered by another person","该昵称已被另一个人注册了"}. {"The account already exists","帐户已存在"}. {"The account was not unregistered","帐户未注册"}. {"The body text of the last received message","最后收到的消息的正文"}. {"The CAPTCHA is valid.","验证码有效."}. {"The CAPTCHA verification has failed","验证码检查失败"}. {"The captcha you entered is wrong","您输入的验证码有误"}. {"The child nodes (leaf or collection) associated with a collection","关联集合的字节点 (叶子或集合)"}. {"The collections with which a node is affiliated","加入结点的集合"}. {"The DateTime at which a leased subscription will end or has ended","租赁订阅将结束或已结束的日期时间"}. {"The datetime when the node was created","节点创建的日期时间"}. {"The default language of the node","该节点的默认语言"}. {"The feature requested is not supported by the conference","会议不支持所请求的特征"}. {"The JID of the node creator","节点创建人的JID"}. {"The JIDs of those to contact with questions","问题联系人的JID"}. {"The JIDs of those with an affiliation of owner","隶属所有人的JID"}. {"The JIDs of those with an affiliation of publisher","隶属发布人的JID"}. {"The list of JIDs that may associate leaf nodes with a collection","可以将叶节点与集合关联的JID列表"}. {"The minimum number of milliseconds between sending any two notification digests","发送任何两个通知摘要之间的最小毫秒数"}. {"The name of the node","该节点的名称"}. {"The node is a collection node","该节点是集合节点"}. {"The node is a leaf node (default)","该节点是叶子节点 (默认)"}. {"The NodeID of the relevant node","相关节点的NodeID"}. {"The number of pending incoming presence subscription requests","待处理的传入状态订阅请求数"}. {"The number of subscribers to the node","该节点的订阅用户数"}. {"The number of unread or undelivered messages","未读或未发送的消息数"}. {"The password contains unacceptable characters","密码包含不可接受的字符"}. {"The password is too weak","密码强度太弱"}. {"the password is","密码是"}. {"The password of your XMPP account was successfully changed.","你的XMPP帐户密码更新成功."}. {"The password was not changed","密码未更新"}. {"The passwords are different","密码不一致"}. {"The presence states for which an entity wants to receive notifications","实体要为其接收通知的状态"}. {"The query is only allowed from local users","仅本地用户可以查询"}. {"The query must not contain <item/> elements","查询不能包含 <item/> 元素"}. {"The room subject can be modified by participants","房间主题可以被参与者修改"}. {"The sender of the last received message","最后收到的消息的发送者"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","本节必须只含一个 <active/> 元素, <default/> 元素,或 <list/> 元素"}. {"The subscription identifier associated with the subscription request","与订阅请求关联的订阅标识符"}. {"The type of node data, usually specified by the namespace of the payload (if any)","节点数据的类型, 如果有, 通常由有效负载的名称空间指定"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","XSL转换的URL,可以将其应用于有效负载以生成适当的消息正文元素。"}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","XSL转换的URL, 可以将其应用于有效负载格式, 以生成有效的数据表单结果, 客户端可以使用通用数据表单呈现引擎来显示该结果"}. {"There was an error changing the password: ","修改密码出错: "}. {"There was an error creating the account: ","帐户创建出错: "}. {"There was an error deleting the account: ","帐户删除失败: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","此处不区分大小写: macbeth 与 MacBeth 和 Macbeth 是一样的."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","本页面允许在此服务器上注册XMPP帐户. 你的JID (Jabber ID) 的形式如下: 用户名@服务器. 请仔细阅读说明并正确填写相应字段."}. {"This page allows to unregister an XMPP account in this XMPP server.","此页面允许在此 XMPP 服务器上注销 XMPP 帐户。"}. {"This room is not anonymous","此房间不是匿名房间"}. {"This service can not process the address: ~s","此服务无法处理地址: ~s"}. {"Thursday","星期四"}. {"Time delay","时间延迟"}. {"Timed out waiting for stream resumption","等待流恢复超时"}. {"Time","时间"}. {"To register, visit ~s","要注册,请访问 ~s"}. {"To ~ts","发送到~ts"}. {"Token TTL","TTL令牌"}. {"Too many active bytestreams","活跃的字节流太多"}. {"Too many CAPTCHA requests","验证码请求太多"}. {"Too many child elements","太多子元素"}. {"Too many <item/> elements","太多 <item/> 元素"}. {"Too many <list/> elements","太多 <list/> 元素"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","来自IP地址(~p)的(~s)失败认证太多。将在UTC时间 ~s 解除对该地址的封锁"}. {"Too many receiver fields were specified","指定的接收者字段太多"}. {"Too many unacked stanzas","未被确认的节太多"}. {"Too many users in this conference","该会议的用户太多"}. {"Total rooms","所有房间"}. {"To","到"}. {"Traffic rate limit is exceeded","已经超过传输率限制"}. {"Transactions Aborted:","取消的事务:"}. {"Transactions Committed:","提交的事务:"}. {"Transactions Logged:","记入日志的事务:"}. {"Transactions Restarted:","重启的事务:"}. {"~ts's Offline Messages Queue","~ts的离线消息队列"}. {"Tuesday","星期二"}. {"Unable to generate a CAPTCHA","无法生成验证码"}. {"Unable to register route on existing local domain","在已存在的本地域上无法注册路由"}. {"Unauthorized","未认证的"}. {"Unexpected action","意外行为"}. {"Unexpected error condition: ~p","意外错误条件: ~p"}. {"Unregister an XMPP account","注销XMPP帐户"}. {"Unregister","取消注册"}. {"Unselect All","取消全选"}. {"Unsupported <index/> element","不支持的 <index/> 元素"}. {"Unsupported version","不支持的版本"}. {"Update message of the day (don't send)","更新每日消息(不发送)"}. {"Update message of the day on all hosts (don't send)","更新所有主机上的每日消息(不发送)"}. {"Update plan","更新计划"}. {"Update ~p","更新~p"}. {"Update script","更新脚本"}. {"Update","更新"}. {"Uptime:","正常运行时间:"}. {"URL for Archived Discussion Logs","已归档对话日志的URL"}. {"User already exists","用户已存在"}. {"User (jid)","用户 (jid)"}. {"User JID","用户JID"}. {"User Management","用户管理"}. {"User removed","用户已移除"}. {"User session not found","用户会话未找到"}. {"User session terminated","用户会话已终止"}. {"User ~ts","用户~ts"}. {"Username:","用户名:"}. {"Users are not allowed to register accounts so quickly","不允许用户太频繁地注册帐户"}. {"Users Last Activity","用户上次活动"}. {"Users","用户"}. {"User","用户"}. {"Validate","确认"}. {"Value 'get' of 'type' attribute is not allowed","不允许 'type' 属性的 'get' 值"}. {"Value of '~s' should be boolean","'~s' 的值应为布尔型"}. {"Value of '~s' should be datetime string","'~s' 的值应为日期时间字符串"}. {"Value of '~s' should be integer","'~s' 的值应为整数"}. {"Value 'set' of 'type' attribute is not allowed","不允许 'type' 属性的 'set' 值"}. {"vCard User Search","vCard用户搜索"}. {"View Queue","查看队列"}. {"View Roster","查看花名册"}. {"Virtual Hosts","虚拟主机"}. {"Visitors are not allowed to change their nicknames in this room","此房间不允许用户更改昵称"}. {"Visitors are not allowed to send messages to all occupants","不允许访客给所有占有者发送消息"}. {"Visitor","访客"}. {"Voice requests are disabled in this conference","该会议的声音请求已被禁用"}. {"Voice request","声音请求"}. {"Wednesday","星期三"}. {"When a new subscription is processed and whenever a subscriber comes online","当新的订阅被处理和当订阅者上线"}. {"When a new subscription is processed","当新的订阅被处理"}. {"When to send the last published item","何时发送最新发布的内容条目"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","除有效载荷格式外,实体是否还希望接收XMPP消息正文"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","实体是否要接收通知的摘要(汇总)或单独接收所有通知"}. {"Whether an entity wants to receive or disable notifications","实体是否要接收或禁用通知"}. {"Whether owners or publisher should receive replies to items","持有人或创建人是否要接收项目回复"}. {"Whether the node is a leaf (default) or a collection","节点是叶子(默认)还是集合"}. {"Whether to allow subscriptions","是否允许订阅"}. {"Whether to make all subscriptions temporary, based on subscriber presence","是否根据订阅者的存在将所有订阅设为临时"}. {"Whether to notify owners about new subscribers and unsubscribes","是否将新订阅人和退订通知所有者"}. {"Who may associate leaf nodes with a collection","谁可以将叶子节点与集合关联"}. {"Wrong parameters in the web formulary","网络配方中的参数错误"}. {"Wrong xmlns","错误的 xmlns"}. {"XMPP Account Registration","XMPP帐户注册"}. {"XMPP Domains","XMPP域"}. {"XMPP Show Value of Away","XMPP的不在显示值"}. {"XMPP Show Value of Chat","XMPP的聊天显示值"}. {"XMPP Show Value of DND (Do Not Disturb)","XMPP的DND(勿扰)显示值"}. {"XMPP Show Value of XA (Extended Away)","XMPP的XA (扩展不在)显示值"}. {"XMPP URI of Associated Publish-Subscribe Node","发布-订阅节点关联的XMPP URI"}. {"You are being removed from the room because of a system shutdown","因系统关机, 你正在被从房间移除"}. {"You are not joined to the channel","您未加入频道"}. {"You can later change your password using an XMPP client.","你可以稍后用XMPP客户端修改你的密码."}. {"You have been banned from this room","您已被禁止进入该房间"}. {"You have joined too many conferences","您加入的会议太多"}. {"You must fill in field \"Nickname\" in the form","您必须填充表单中\"昵称\"项"}. {"You need a client that supports x:data and CAPTCHA to register","您需要一个支持 x:data 和验证码的客户端进行注册"}. {"You need a client that supports x:data to register the nickname","您需要一个支持 x:data 的客户端来注册昵称"}. {"You need an x:data capable client to search","您需要一个兼容 x:data 的客户端来搜索"}. {"Your active privacy list has denied the routing of this stanza.","你的活跃私聊列表拒绝了在此房间进行路由分发."}. {"Your contact offline message queue is full. The message has been discarded.","您的联系人离线消息队列已满。消息已被丢弃。"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","您发送给~s的消息已被阻止. 要解除阻止, 请访问 ~s"}. {"Your XMPP account was successfully registered.","你的XMPP帐户注册成功."}. {"Your XMPP account was successfully unregistered.","你的XMPP帐户注销成功."}. {"You're not allowed to create nodes","您不可以创建节点"}. ���������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/ca.msg���������������������������������������������������������������������0000644�0002322�0002322�00000136747�14154362354�016700� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Afegix * al final d'un camp per a buscar subcadenes)"}. {" has set the subject to: "," ha posat el tema: "}. {"# participants","# participants"}. {"A description of the node","Una descripció del node"}. {"A friendly name for the node","Un nom per al node"}. {"A password is required to enter this room","Es necessita contrasenya per a entrar en aquesta sala"}. {"A Web Page","Una Pàgina Web"}. {"Accept","Acceptar"}. {"Access denied by service policy","Accés denegat per la política del servei"}. {"Access model of authorize","Model d'Accés de autoritzar"}. {"Access model of open","Model d'Accés de obert"}. {"Access model of presence","Model d'Accés de presència"}. {"Access model of roster","Model d'Accés de contactes"}. {"Access model of whitelist","Model d'Accés de llista blanca"}. {"Access model","Model d'Accés"}. {"Account doesn't exist","El compte no existeix"}. {"Action on user","Acció en l'usuari"}. {"Add Jabber ID","Afegir Jabber ID"}. {"Add New","Afegir nou"}. {"Add User","Afegir usuari"}. {"Administration of ","Administració de "}. {"Administration","Administració"}. {"Administrator privileges required","Es necessita tenir privilegis d'administrador"}. {"All activity","Tota l'activitat"}. {"All Users","Tots els usuaris"}. {"Allow subscription","Permetre subscripcions"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Permetre que aquesta Jabber ID es puga subscriure a aquest node pubsub?"}. {"Allow this person to register with the room?","Permetre a esta persona registrar-se a la sala?"}. {"Allow users to change the subject","Permetre que els usuaris canviïn el tema"}. {"Allow users to query other users","Permetre que els usuaris fagen peticions a altres usuaris"}. {"Allow users to send invites","Permetre que els usuaris envien invitacions"}. {"Allow users to send private messages","Permetre que els usuaris envien missatges privats"}. {"Allow visitors to change nickname","Permetre als visitants canviar el sobrenom"}. {"Allow visitors to send private messages to","Permetre als visitants enviar missatges privats a"}. {"Allow visitors to send status text in presence updates","Permetre als visitants enviar text d'estat en les actualitzacions de presència"}. {"Allow visitors to send voice requests","Permetre als visitants enviar peticions de veu"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un grup LDAP associat que defineix membresía a la sala; esto deuria ser un Nombre Distinguible de LDAP, d'acord amb una definició de grup específica d'implementació o de instal·lació."}. {"Announcements","Anuncis"}. {"Answer associated with a picture","Resposta associada amb una imatge"}. {"Answer associated with a video","Resposta associada amb un vídeo"}. {"Answer associated with speech","Resposta associada amb un parlament"}. {"Answer to a question","Resposta a una pregunta"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualsevol en el grup de contactes especificat pot subscriure's i recuperar elements"}. {"Anyone may associate leaf nodes with the collection","Qualsevol pot associar nodes fulla amb la col·lecció"}. {"Anyone may publish","Qualsevol pot publicar"}. {"Anyone may subscribe and retrieve items","Qualsevol pot publicar i recuperar elements"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualsevol amb una subscripció de presencia de 'both' o 'from' pot subscriure's i publicar elements"}. {"Anyone with Voice","Qualsevol amb Veu"}. {"Anyone","Qualsevol"}. {"April","Abril"}. {"Attribute 'channel' is required for this request","L'atribut 'channel' és necessari per a aquesta petició"}. {"Attribute 'id' is mandatory for MIX messages","L'atribut 'id' es necessari per a missatges MIX"}. {"Attribute 'jid' is not allowed here","L'atribut 'jid' no està permès ací"}. {"Attribute 'node' is not allowed here","L'atribut 'node' no està permès ací"}. {"Attribute 'to' of stanza that triggered challenge","L'atribut 'to' del paquet que va disparar la comprovació"}. {"August","Agost"}. {"Automatic node creation is not enabled","La creació automàtica de nodes no està activada"}. {"Backup Management","Gestió de còpia de seguretat"}. {"Backup of ~p","Còpia de seguretat de ~p"}. {"Backup to File at ","Desar còpia de seguretat a fitxer en "}. {"Backup","Guardar còpia de seguretat"}. {"Bad format","Format erroni"}. {"Birthday","Aniversari"}. {"Both the username and the resource are required","Es requereixen tant el nom d'usuari com el recurs"}. {"Bytestream already activated","El Bytestream ja està activat"}. {"Cannot remove active list","No es pot eliminar la llista activa"}. {"Cannot remove default list","No es pot eliminar la llista per defecte"}. {"CAPTCHA web page","Pàgina web del CAPTCHA"}. {"Challenge ID","ID de la comprovació"}. {"Change Password","Canviar Contrasenya"}. {"Change User Password","Canviar Contrasenya d'Usuari"}. {"Changing password is not allowed","No està permès canviar la contrasenya"}. {"Changing role/affiliation is not allowed","No està permès canviar el rol/afiliació"}. {"Channel already exists","El canal ja existeix"}. {"Channel does not exist","El canal no existeix"}. {"Channels","Canals"}. {"Characters not allowed:","Caràcters no permesos:"}. {"Chatroom configuration modified","Configuració de la sala de xat modificada"}. {"Chatroom is created","La sala s'ha creat"}. {"Chatroom is destroyed","La sala s'ha destruït"}. {"Chatroom is started","La sala s'ha iniciat"}. {"Chatroom is stopped","La sala s'ha aturat"}. {"Chatrooms","Sales de xat"}. {"Choose a username and password to register with this server","Tria nom d'usuari i contrasenya per a registrar-te en aquest servidor"}. {"Choose storage type of tables","Selecciona el tipus d'almacenament de les taules"}. {"Choose whether to approve this entity's subscription.","Tria si aproves aquesta entitat de subscripció."}. {"City","Ciutat"}. {"Client acknowledged more stanzas than sent by server","El client ha reconegut més paquets dels que ha enviat el servidor"}. {"Commands","Comandaments"}. {"Conference room does not exist","La sala de conferències no existeix"}. {"Configuration of room ~s","Configuració de la sala ~s"}. {"Configuration","Configuració"}. {"Connected Resources:","Recursos connectats:"}. {"Contact Addresses (normally, room owner or owners)","Adreces de contacte (normalment, propietaris de la sala)"}. {"Country","Pais"}. {"CPU Time:","Temps de CPU:"}. {"Current Discussion Topic","Assumpte de discussió actual"}. {"Database failure","Error a la base de dades"}. {"Database Tables at ~p","Taules de la base de dades en ~p"}. {"Database Tables Configuration at ","Configuració de la base de dades en "}. {"Database","Base de dades"}. {"December","Decembre"}. {"Default users as participants","Els usuaris són participants per defecte"}. {"Delete content","Eliminar contingut"}. {"Delete message of the day on all hosts","Elimina el missatge del dis de tots els hosts"}. {"Delete message of the day","Eliminar el missatge del dia"}. {"Delete Selected","Eliminar els seleccionats"}. {"Delete table","Eliminar taula"}. {"Delete User","Eliminar Usuari"}. {"Deliver event notifications","Entrega de notificacions d'events"}. {"Deliver payloads with event notifications","Enviar payloads junt a les notificacions d'events"}. {"Description:","Descripció:"}. {"Disc only copy","Còpia sols en disc"}. {"'Displayed groups' not added (they do not exist!): ","'Mostrats' no afegits (no existeixen!): "}. {"Displayed:","Mostrats:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","No li donis la teva contrasenya a ningú, ni tan sols als administradors del servidor XMPP."}. {"Dump Backup to Text File at ","Exporta còpia de seguretat a fitxer de text en "}. {"Dump to Text File","Exportar a fitxer de text"}. {"Duplicated groups are not allowed by RFC6121","No estan permesos els grups duplicats al RFC6121"}. {"Dynamically specify a replyto of the item publisher","Especifica dinàmicament l'adreça on respondre al publicador del element"}. {"Edit Properties","Editar propietats"}. {"Either approve or decline the voice request.","Aprova o denega la petició de veu."}. {"ejabberd HTTP Upload service","ejabberd - servei de HTTP Upload"}. {"ejabberd MUC module","mòdul ejabberd MUC"}. {"ejabberd Multicast service","ejabberd - servei de Multicast"}. {"ejabberd Publish-Subscribe module","ejabberd - Mòdul Publicar-Subscriure"}. {"ejabberd SOCKS5 Bytestreams module","mòdul ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","ejabberd mòdul vCard"}. {"ejabberd Web Admin","ejabberd Web d'administració"}. {"ejabberd","ejabberd"}. {"Elements","Elements"}. {"Email Address","Adreça de correu"}. {"Email","Correu"}. {"Enable logging","Habilitar el registre de la conversa"}. {"Enable message archiving","Activar l'emmagatzematge de missatges"}. {"Enabling push without 'node' attribute is not supported","No està suportat activar Push sense l'atribut 'node'"}. {"End User Session","Finalitzar Sesió d'Usuari"}. {"Enter nickname you want to register","Introdueix el sobrenom que vols registrar"}. {"Enter path to backup file","Introdueix ruta al fitxer de còpia de seguretat"}. {"Enter path to jabberd14 spool dir","Introdueix la ruta al directori de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introdueix ruta al fitxer jabberd14 spool"}. {"Enter path to text file","Introdueix ruta al fitxer de text"}. {"Enter the text you see","Introdueix el text que veus"}. {"Erlang XMPP Server","Servidor Erlang XMPP"}. {"Error","Error"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excloure Jabber IDs de la comprovació CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exporta totes les taules a un fitxer SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar dades de tots els usuaris del servidor a arxius PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dades d'usuaris d'un host a arxius PIEFXIS (XEP-0227):"}. {"External component failure","Error al component extern"}. {"External component timeout","Temps esgotat al component extern"}. {"Failed to activate bytestream","Errada al activar bytestream"}. {"Failed to extract JID from your voice request approval","No s'ha pogut extraure el JID de la teva aprovació de petició de veu"}. {"Failed to map delegated namespace to external component","Ha fallat mapejar la delegació de l'espai de noms al component extern"}. {"Failed to parse HTTP response","Ha fallat el processat de la resposta HTTP"}. {"Failed to process option '~s'","Ha fallat el processat de la opció '~s'"}. {"Family Name","Cognom"}. {"FAQ Entry","Entrada a la FAQ"}. {"February","Febrer"}. {"File larger than ~w bytes","El fitxer es més gran que ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Emplena camps per a buscar usuaris XMPP que concorden"}. {"Friday","Divendres"}. {"From ~ts","De ~ts"}. {"From","De"}. {"Full List of Room Admins","Llista completa de administradors de la sala"}. {"Full List of Room Owners","Llista completa de propietaris de la sala"}. {"Full Name","Nom complet"}. {"Get Number of Online Users","Obtenir Número d'Usuaris Connectats"}. {"Get Number of Registered Users","Obtenir Número d'Usuaris Registrats"}. {"Get Pending","Obtenir Pendents"}. {"Get User Last Login Time","Obtenir la última connexió d'Usuari"}. {"Get User Password","Obtenir Contrasenya d'usuari"}. {"Get User Statistics","Obtenir Estadístiques d'Usuari"}. {"Given Name","Nom propi"}. {"Grant voice to this person?","Concedir veu a aquesta persona?"}. {"Group","Grup"}. {"Groups that will be displayed to the members","Grups que seran mostrats als membres"}. {"Groups","Grups"}. {"has been banned","ha sigut bloquejat"}. {"has been kicked because of a system shutdown","ha sigut expulsat perquè el sistema va a apagar-se"}. {"has been kicked because of an affiliation change","ha sigut expulsat a causa d'un canvi d'afiliació"}. {"has been kicked because the room has been changed to members-only","ha sigut expulsat perquè la sala ara és només per a membres"}. {"has been kicked","ha sigut expulsat"}. {"Host unknown","Host desconegut"}. {"Host","Host"}. {"HTTP File Upload","HTTP File Upload"}. {"Idle connection","Connexió sense us"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si no veus la imatge CAPTCHA açí, visita la pàgina web."}. {"Import Directory","Importar directori"}. {"Import File","Importar fitxer"}. {"Import user data from jabberd14 spool file:","Importar dades d'usuaris de l'arxiu de spool de jabberd14:"}. {"Import User from File at ","Importa usuari des de fitxer en "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importar dades d'usuaris des d'un arxiu PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar dades d'usuaris del directori de spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuaris des del directori en "}. {"Import Users From jabberd14 Spool Files","Importar usuaris de jabberd14"}. {"Improper domain part of 'from' attribute","La part de domini de l'atribut 'from' es impròpia"}. {"Improper message type","Tipus de missatge incorrecte"}. {"Incoming s2s Connections:","Connexions s2s d'entrada:"}. {"Incorrect CAPTCHA submit","El CAPTCHA proporcionat és incorrecte"}. {"Incorrect data form","El formulari de dades és incorrecte"}. {"Incorrect password","Contrasenya incorrecta"}. {"Incorrect value of 'action' attribute","Valor incorrecte del atribut 'action'"}. {"Incorrect value of 'action' in data form","Valor incorrecte de 'action' al formulari de dades"}. {"Incorrect value of 'path' in data form","Valor incorrecte de 'path' al formulari de dades"}. {"Insufficient privilege","Privilegi insuficient"}. {"Internal server error","Error intern del servidor"}. {"Invalid 'from' attribute in forwarded message","Atribut 'from' invàlid al missatge reenviat"}. {"Invalid node name","Nom de node no vàlid"}. {"Invalid 'previd' value","Valor no vàlid de 'previd'"}. {"Invitations are not allowed in this conference","Les invitacions no estan permeses en aquesta sala de conferència"}. {"IP addresses","Adreça IP"}. {"is now known as","ara es conegut com"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","No està permés enviar missatges d'error a la sala. El participant (~s) ha enviat un missatge d'error (~s) i ha sigut expulsat de la sala"}. {"It is not allowed to send private messages of type \"groupchat\"","No està permés enviar missatges del tipus \"groupchat\""}. {"It is not allowed to send private messages to the conference","No està permès l'enviament de missatges privats a la sala"}. {"It is not allowed to send private messages","No està permés enviar missatges privats"}. {"Jabber ID","ID Jabber"}. {"January","Gener"}. {"JID normalization denied by service policy","S'ha denegat la normalització del JID per política del servei"}. {"JID normalization failed","Ha fallat la normalització del JID"}. {"joins the room","entra a la sala"}. {"July","Juliol"}. {"June","Juny"}. {"Just created","Creació recent"}. {"Label:","Etiqueta:"}. {"Last Activity","Última activitat"}. {"Last login","Últim login"}. {"Last message","Últim missatge"}. {"Last month","Últim mes"}. {"Last year","Últim any"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Els bits menys significants del hash SHA-256 del text deurien ser iguals a l'etiqueta hexadecimal"}. {"leaves the room","surt de la sala"}. {"List of rooms","Llista de sales"}. {"Logging","Registre"}. {"Low level update script","Script d'actualització de baix nivell"}. {"Make participants list public","Crear una llista de participants pública"}. {"Make room CAPTCHA protected","Crear una sala protegida per CAPTCHA"}. {"Make room members-only","Crear una sala només per a membres"}. {"Make room moderated","Crear una sala moderada"}. {"Make room password protected","Crear una sala amb contrasenya"}. {"Make room persistent","Crear una sala persistent"}. {"Make room public searchable","Crear una sala pública"}. {"Malformed username","Nom d'usuari mal format"}. {"MAM preference modification denied by service policy","Se t'ha denegat la modificació de la preferència de MAM per política del servei"}. {"March","Març"}. {"Max # of items to persist","Màxim # d'elements que persistixen"}. {"Max payload size in bytes","Màxim tamany del payload en bytes"}. {"Maximum file size","Mida màxima de fitxer"}. {"Maximum Number of History Messages Returned by Room","Numero màxim de missatges de l'historia que retorna la sala"}. {"Maximum number of items to persist","Número màxim d'elements que persistixen"}. {"Maximum Number of Occupants","Número màxim d'ocupants"}. {"May","Maig"}. {"Members not added (inexistent vhost!): ","Membres no afegits (perquè el vhost no existeix): "}. {"Membership is required to enter this room","Necessites ser membre d'aquesta sala per a poder entrar"}. {"Members:","Membre:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memoritza la teva contrasenya, o escriu-la en un paper guardat a un lloc segur. A XMPP no hi ha una forma automatitzada de recuperar la teva contrasenya si la oblides."}. {"Memory","Memòria"}. {"Mere Availability in XMPP (No Show Value)","Simplement disponibilitat a XMPP (sense valor de 'show')"}. {"Message body","Missatge"}. {"Message not found in forwarded payload","Missatge no trobat al contingut reenviat"}. {"Messages from strangers are rejected","Els missatges de desconeguts son rebutjats"}. {"Messages of type headline","Missatges de tipus titular"}. {"Messages of type normal","Missatges de tipus normal"}. {"Middle Name","Segon nom"}. {"Minimum interval between voice requests (in seconds)","Interval mínim entre peticions de veu (en segons)"}. {"Moderator privileges required","Es necessita tenir privilegis de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Només moderadors"}. {"Modified modules","Mòduls modificats"}. {"Module failed to handle the query","El modul ha fallat al gestionar la petició"}. {"Monday","Dilluns"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","No estan permesos múltiples elements <item/> per RFC6121"}. {"Multi-User Chat","Multi-Usuari Converses"}. {"Name in the rosters where this group will be displayed","Nom a les llistes de contactes on es mostrarà aquest grup"}. {"Name","Nom"}. {"Name:","Nom:"}. {"Natural Language for Room Discussions","Llengua natural per a les discussions a les sales"}. {"Natural-Language Room Name","Nom de la sala a la seua llengua natural"}. {"Neither 'jid' nor 'nick' attribute found","No s'han trobat els atributs 'jid' ni 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","No s'han trobat els atributs 'role' ni 'affiliation'"}. {"Never","Mai"}. {"New Password:","Nova Contrasenya:"}. {"Nickname can't be empty","El sobrenom no pot estar buit"}. {"Nickname Registration at ","Registre del sobrenom en "}. {"Nickname ~s does not exist in the room","El sobrenom ~s no existeix a la sala"}. {"Nickname","Sobrenom"}. {"No address elements found","No s'han trobat elements d'adreces ('address')"}. {"No addresses element found","No s'ha trobat l'element d'adreces ('addresses')"}. {"No 'affiliation' attribute found","No s'ha trobat l'atribut 'affiliation'"}. {"No available resource found","No s'ha trobat un recurs disponible"}. {"No body provided for announce message","No hi ha proveedor per al missatge anunci"}. {"No child elements found","No s'han trobat subelements"}. {"No data form found","No s'ha trobat el formulari de dades"}. {"No Data","No hi ha dades"}. {"No features available","No n'hi ha característiques disponibles"}. {"No <forwarded/> element found","No s'ha trobat cap element <forwarded/>"}. {"No hook has processed this command","Cap event ha processat este comandament"}. {"No info about last activity found","No s'ha trobat informació de l'ultima activitat"}. {"No 'item' element found","No s'ha trobat cap element 'item'"}. {"No items found in this query","En aquesta petició no s'ha trobat cap element"}. {"No limit","Sense Llímit"}. {"No module is handling this query","Cap element està manegant esta petició"}. {"No node specified","No s'ha especificat node"}. {"No 'password' found in data form","No s'ha trobat 'password' al formulari de dades"}. {"No 'password' found in this query","No s'ha trobat 'password' en esta petició"}. {"No 'path' found in data form","No s'ha trobat 'path' en el formulari de dades"}. {"No pending subscriptions found","No s'han trobat subscripcions pendents"}. {"No privacy list with this name found","No s'ha trobat cap llista de privacitat amb aquest nom"}. {"No private data found in this query","No s'ha trobat dades privades en esta petició"}. {"No running node found","No s'ha trobat node en marxa"}. {"No services available","No n'hi ha serveis disponibles"}. {"No statistics found for this item","No n'hi ha estadístiques disponibles per a aquest element"}. {"No 'to' attribute found in the invitation","No s'ha trobat l'atribut 'to' en la invitació"}. {"Nobody","Ningú"}. {"Node already exists","El node ja existeix"}. {"Node ID","ID del Node"}. {"Node index not found","Index de node no trobat"}. {"Node not found","Node no trobat"}. {"Node ~p","Node ~p"}. {"Nodeprep has failed","Ha fallat Nodeprep"}. {"Nodes","Nodes"}. {"None","Cap"}. {"Not allowed","No permès"}. {"Not Found","No Trobat"}. {"Not subscribed","No subscrit"}. {"Notify subscribers when items are removed from the node","Notificar subscriptors quan els elements són eliminats del node"}. {"Notify subscribers when the node configuration changes","Notificar subscriptors quan canvia la configuració del node"}. {"Notify subscribers when the node is deleted","Notificar subscriptors quan el node és eliminat"}. {"November","Novembre"}. {"Number of answers required","Número de respostes requerides"}. {"Number of occupants","Número d'ocupants"}. {"Number of Offline Messages","Número de missatges offline"}. {"Number of online users","Número d'usuaris connectats"}. {"Number of registered users","Número d'Usuaris Registrats"}. {"Number of seconds after which to automatically purge items","Número de segons després dels quals es purgaran automàticament elements"}. {"Occupants are allowed to invite others","Els ocupants poden invitar a altres"}. {"Occupants May Change the Subject","Els ocupants poden canviar el Tema"}. {"October","Octubre"}. {"Offline Messages:","Missatges fora de línia:"}. {"Offline Messages","Missatges offline"}. {"OK","Acceptar"}. {"Old Password:","Antiga contrasenya:"}. {"Online Users","Usuaris conectats"}. {"Online Users:","Usuaris en línia:"}. {"Online","Connectat"}. {"Only admins can see this","Només els administradors poden veure esto"}. {"Only collection node owners may associate leaf nodes with the collection","Només els propietaris de la col·lecció de nodes poden associar nodes fulla amb la col·lecció"}. {"Only deliver notifications to available users","Sols enviar notificacions als usuaris disponibles"}. {"Only <enable/> or <disable/> tags are allowed","Només es permeten etiquetes <enable/> o <disable/>"}. {"Only <list/> element is allowed in this query","En esta petició només es permet l'element <list/>"}. {"Only members may query archives of this room","Només membres poden consultar l'arxiu de missatges d'aquesta sala"}. {"Only moderators and participants are allowed to change the subject in this room","Només els moderadors i participants poden canviar el tema d'aquesta sala"}. {"Only moderators are allowed to change the subject in this room","Només els moderadors poden canviar el tema d'aquesta sala"}. {"Only moderators can approve voice requests","Només els moderadors poden aprovar les peticions de veu"}. {"Only occupants are allowed to send messages to the conference","Sols els ocupants poden enviar missatges a la sala"}. {"Only occupants are allowed to send queries to the conference","Sols els ocupants poden enviar sol·licituds a la sala"}. {"Only publishers may publish","Només els publicadors poden publicar"}. {"Only service administrators are allowed to send service messages","Sols els administradors del servei tenen permís per a enviar missatges de servei"}. {"Only those on a whitelist may associate leaf nodes with the collection","Només qui estiga a una llista blanca pot associar nodes fulla amb la col·lecció"}. {"Only those on a whitelist may subscribe and retrieve items","Només qui estiga a una llista blanca pot subscriure's i recuperar elements"}. {"Organization Name","Nom de la organizació"}. {"Organization Unit","Unitat de la organizació"}. {"Outgoing s2s Connections:","Connexions d'eixida s2s:"}. {"Outgoing s2s Connections","Connexions s2s d'eixida"}. {"Owner privileges required","Es requerixen privilegis de propietari de la sala"}. {"Packet relay is denied by service policy","S'ha denegat el reenviament del paquet per política del servei"}. {"Packet","Paquet"}. {"Participant","Participant"}. {"Password Verification","Verificació de la Contrasenya"}. {"Password Verification:","Verificació de la Contrasenya:"}. {"Password","Contrasenya"}. {"Password:","Contrasenya:"}. {"Path to Dir","Ruta al directori"}. {"Path to File","Ruta al fitxer"}. {"Payload type","Tipus de payload"}. {"Pending","Pendent"}. {"Period: ","Període: "}. {"Persist items to storage","Persistir elements al guardar"}. {"Persistent","Persistent"}. {"Ping query is incorrect","La petició de Ping es incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Recorda que aquestes opcions només fan còpia de seguretat de la base de dades Mnesia. Si estàs utilitzant el mòdul d'ODBC també deus de fer una còpia de seguretat de la base de dades de SQL a part."}. {"Please, wait for a while before sending new voice request","Si us plau, espera una mica abans d'enviar una nova petició de veu"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Posseir l'atribut 'ask' no està permès per RFC6121"}. {"Present real Jabber IDs to","Presentar Jabber ID's reals a"}. {"Previous session not found","No s'ha trobat la sessió prèvia"}. {"Previous session PID has been killed","El procés de la sessió prèvia ha sigut matat"}. {"Previous session PID has exited","El procés de la sessió prèvia ha sortit"}. {"Previous session PID is dead","El procés de la sessió prèvia està mort"}. {"Previous session timed out","La sessió prèvia ha caducat"}. {"private, ","privat, "}. {"Public","Public"}. {"Publish model","Model de publicació"}. {"Publish-Subscribe","Publicar-subscriure't"}. {"PubSub subscriber request","Petició de subscriptor PubSub"}. {"Purge all items when the relevant publisher goes offline","Eliminar tots els elements quan el publicant relevant es desconnecti"}. {"Push record not found","No s'ha trobat l'element Push"}. {"Queries to the conference members are not allowed in this room","En aquesta sala no es permeten sol·licituds als membres"}. {"Query to another users is forbidden","Enviar peticions a altres usuaris no està permès"}. {"RAM and disc copy","Còpia en RAM i disc"}. {"RAM copy","Còpia en RAM"}. {"Really delete message of the day?","Segur que vols eliminar el missatge del dia?"}. {"Receive notification from all descendent nodes","Rebre notificació de tots els nodes descendents"}. {"Receive notification from direct child nodes only","Rebre notificació només de nodes fills directes"}. {"Receive notification of new items only","Rebre notificació només de nous elements"}. {"Receive notification of new nodes only","Rebre notificació només de nous nodes"}. {"Recipient is not in the conference room","El receptor no està en la sala de conferència"}. {"Register an XMPP account","Registrar un compte XMPP"}. {"Registered Users","Usuaris registrats"}. {"Registered Users:","Usuaris registrats:"}. {"Register","Registrar"}. {"Remote copy","Còpia remota"}. {"Remove All Offline Messages","Eliminar tots els missatges offline"}. {"Remove User","Eliminar usuari"}. {"Remove","Borrar"}. {"Replaced by new connection","Reemplaçat per una nova connexió"}. {"Request has timed out","La petició ha caducat"}. {"Request is ignored","La petició ha sigut ignorada"}. {"Requested role","Rol sol·licitat"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar el Servei"}. {"Restart","Reiniciar"}. {"Restore Backup from File at ","Restaura còpia de seguretat des del fitxer en "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar una còpia de seguretat binària després de reiniciar el ejabberd (requereix menys memòria:"}. {"Restore binary backup immediately:","Restaurar una còpia de seguretat binària ara mateix:"}. {"Restore plain text backup immediately:","Restaurar una còpia de seguretat en format de text pla ara mateix:"}. {"Restore","Restaurar"}. {"Roles and Affiliations that May Retrieve Member List","Rols i Afiliacions que poden recuperar la llista de membres"}. {"Roles for which Presence is Broadcasted","Rols per als que sí se difon la seua presencia"}. {"Roles that May Send Private Messages","Rols que poden enviar missatges privats"}. {"Room Configuration","Configuració de la sala"}. {"Room creation is denied by service policy","Se t'ha denegat el crear la sala per política del servei"}. {"Room description","Descripció de la sala"}. {"Room Occupants","Ocupants de la sala"}. {"Room terminates","La sala està terminant"}. {"Room title","Títol de la sala"}. {"Roster groups allowed to subscribe","Llista de grups que tenen permés subscriures"}. {"Roster of ~ts","Llista de contactes de ~ts"}. {"Roster size","Mida de la llista"}. {"Roster:","Llista de contactes:"}. {"RPC Call Error","Error de cridada RPC"}. {"Running Nodes","Nodes funcionant"}. {"~s invites you to the room ~s","~s et convida a la sala ~s"}. {"Saturday","Dissabte"}. {"Script check","Comprovar script"}. {"Search from the date","Buscar des de la data"}. {"Search Results for ","Resultats de la búsqueda "}. {"Search the text","Buscar el text"}. {"Search until the date","Buscar fins la data"}. {"Search users in ","Cerca usuaris en "}. {"Select All","Seleccionar Tots"}. {"Send announcement to all online users on all hosts","Enviar anunci a tots els usuaris connectats a tots els hosts"}. {"Send announcement to all online users","Enviar anunci a tots els usuaris connectats"}. {"Send announcement to all users on all hosts","Enviar anunci a tots els usuaris de tots els hosts"}. {"Send announcement to all users","Enviar anunci a tots els usuaris"}. {"September","Setembre"}. {"Server:","Servidor:"}. {"Service list retrieval timed out","L'intent de recuperar la llista de serveis ha caducat"}. {"Session state copying timed out","La copia del estat de la sessió ha caducat"}. {"Set message of the day and send to online users","Configurar el missatge del dia i enviar a tots els usuaris"}. {"Set message of the day on all hosts and send to online users","Escriure missatge del dia en tots els hosts i enviar-ho als usuaris connectats"}. {"Shared Roster Groups","Grups de contactes compartits"}. {"Show Integral Table","Mostrar Taula Integral"}. {"Show Ordinary Table","Mostrar Taula Ordinaria"}. {"Shut Down Service","Apager el Servei"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clients XMPP poden emmagatzemar la teva contrasenya al ordinador, però només hauries de fer això al teu ordinador personal, per raons de seguretat."}. {"Specify the access model","Especificar el model d'accés"}. {"Specify the event message type","Especifica el tipus de missatge d'event"}. {"Specify the publisher model","Especificar el model del publicant"}. {"Stanza ID","ID del paquet"}. {"Statically specify a replyto of the node owner(s)","Especifica estaticament una adreça on respondre al propietari del node"}. {"Statistics of ~p","Estadístiques de ~p"}. {"Statistics","Estadístiques"}. {"Stop","Detindre"}. {"Stopped Nodes","Nodes parats"}. {"Storage Type","Tipus d'emmagatzematge"}. {"Store binary backup:","Guardar una còpia de seguretat binària:"}. {"Store plain text backup:","Guardar una còpia de seguretat en format de text pla:"}. {"Stream management is already enabled","L'administració de la connexió (stream management) ja està activada"}. {"Stream management is not enabled","L'administració de la conexió (stream management) no està activada"}. {"Subject","Tema"}. {"Submit","Enviar"}. {"Submitted","Enviat"}. {"Subscriber Address","Adreça del Subscriptor"}. {"Subscribers may publish","Els subscriptors poden publicar"}. {"Subscription requests must be approved and only subscribers may retrieve items","Les peticiones de subscripció han de ser aprovades i només els subscriptors poden recuperar elements"}. {"Subscriptions are not allowed","Les subscripcions no estan permeses"}. {"Subscription","Subscripció"}. {"Sunday","Diumenge"}. {"Text associated with a picture","Text associat amb una imatge"}. {"Text associated with a sound","Text associat amb un so"}. {"Text associated with a video","Text associat amb un vídeo"}. {"Text associated with speech","Text associat amb una veu"}. {"That nickname is already in use by another occupant","El sobrenom ja l'està utilitzant una altra persona"}. {"That nickname is registered by another person","El sobrenom ja està registrat per una altra persona"}. {"The account already exists","El compte ha existeix"}. {"The account was not unregistered","El compte no ha sigut esborrat"}. {"The body text of the last received message","El contingut del text de l'ultim missatge rebut"}. {"The CAPTCHA is valid.","El CAPTCHA es vàlid."}. {"The CAPTCHA verification has failed","La verificació CAPTCHA ha fallat"}. {"The captcha you entered is wrong","El CAPTCHA que has proporcionat és incorrecte"}. {"The child nodes (leaf or collection) associated with a collection","El nodes fills (fulla o col·leccions) associats amb una col·lecció"}. {"The collections with which a node is affiliated","Les col.leccions amb les que un node està afiliat"}. {"The DateTime at which a leased subscription will end or has ended","La Data i Hora a la que una subscripció prestada terminarà o ha terminat"}. {"The datetime when the node was created","La data i hora a la que un node va ser creat"}. {"The default language of the node","El llenguatge per defecte d'un node"}. {"The feature requested is not supported by the conference","La característica sol·licitada no està suportada per la sala de conferència"}. {"The JID of the node creator","El JID del creador del node"}. {"The JIDs of those to contact with questions","Els JIDs a qui contactar amb preguntes"}. {"The JIDs of those with an affiliation of owner","Els JIDs de qui tenen una afiliació de propietaris"}. {"The JIDs of those with an affiliation of publisher","Els JIDs de qui tenen una afiliació de publicadors"}. {"The list of JIDs that may associate leaf nodes with a collection","La llista de JIDs que poden associar nodes fulla amb una col·lecció"}. {"The maximum number of child nodes that can be associated with a collection","El màxim número de nodes fills que poden associar-se amb una col·lecció"}. {"The minimum number of milliseconds between sending any two notification digests","El número mínim de mil·lisegons entre l'enviament de dos resums de notificacions"}. {"The name of the node","El nom del node"}. {"The node is a collection node","El node es una col·lecció"}. {"The node is a leaf node (default)","El node es un node fulla (per defecte)"}. {"The NodeID of the relevant node","El NodeID del node rellevant"}. {"The number of pending incoming presence subscription requests","El número de peticions rebudes de subscripció de presencia pendents"}. {"The number of subscribers to the node","El número de subscriptors al node"}. {"The number of unread or undelivered messages","El número de missatges no llegits o no enviats"}. {"The password contains unacceptable characters","La contrasenya conté caràcters inacceptables"}. {"The password is too weak","La contrasenya és massa simple"}. {"the password is","la contrasenya és"}. {"The password of your XMPP account was successfully changed.","La contrasenya del teu compte XMPP s'ha canviat correctament."}. {"The password was not changed","La contrasenya no ha sigut canviada"}. {"The passwords are different","Les contrasenyes son diferents"}. {"The presence states for which an entity wants to receive notifications","El estats de presencia per als quals una entitat vol rebre notificacions"}. {"The query is only allowed from local users","La petició està permesa només d'usuaris locals"}. {"The query must not contain <item/> elements","La petició no pot contenir elements <item/>"}. {"The room subject can be modified by participants","El tema de la sala pot modificar-lo els participants"}. {"The sender of the last received message","Qui ha enviat l'ultim missatge rebut"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","El paquet DEU contindre només un element <active/>, un element <default/>, o un element <list/>"}. {"The subscription identifier associated with the subscription request","L'identificador de subscripció associat amb la petició de subscripció"}. {"The type of node data, usually specified by the namespace of the payload (if any)","El tipus de dades al node, usualment especificat pel namespace del payload (si n'hi ha)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de uns transformació XSL que pot ser aplicada als payloads per a generar un element apropiat de contingut de missatge."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","La URL de una transformació XSL que pot ser aplicada al format de payload per a generar un resultat valid de Data Forms, que el client puga mostrar usant un métode generic de Data Forms"}. {"The username is not valid","El nom d'usuari no es vàlid"}. {"There was an error changing the password: ","Hi ha hagut un error canviant la contrasenya: "}. {"There was an error creating the account: ","Hi ha hagut un error creant el compte: "}. {"There was an error deleting the account: ","Hi ha hagut un error esborrant el compte: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Això no distingeix majúscules de minúscules: macbeth es el mateix que MacBeth i Macbeth."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Aquesta pàgina permet crear un compte XMPP en aquest servidor XMPP. El teu JID (Jabber ID; Identificador Jabber) tindrà aquesta forma: usuari@servidor. Si us plau, llegeix amb cura les instruccions per emplenar correctament els camps."}. {"This page allows to unregister an XMPP account in this XMPP server.","Aquesta pàgina permet esborrar un compte XMPP en aquest servidor XMPP."}. {"This room is not anonymous","Aquesta sala no és anònima"}. {"This service can not process the address: ~s","Este servei no pot processar la direcció: ~s"}. {"Thursday","Dijous"}. {"Time delay","Temps de retard"}. {"Timed out waiting for stream resumption","Massa temps esperant que es resumisca la connexió"}. {"Time","Data"}. {"To register, visit ~s","Per a registrar-te, visita ~s"}. {"To ~ts","A ~ts"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","N'hi ha massa Bytestreams actius"}. {"Too many CAPTCHA requests","Massa peticions de CAPTCHA"}. {"Too many child elements","N'hi ha massa subelements"}. {"Too many <item/> elements","N'hi ha massa elements <item/>"}. {"Too many <list/> elements","N'hi ha massa elements <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Massa autenticacions (~p) han fallat des d'aquesta adreça IP (~s). L'adreça serà desbloquejada en ~s UTC"}. {"Too many receiver fields were specified","S'han especificat massa camps de receptors"}. {"Too many unacked stanzas","Massa missatges sense haver reconegut la seva recepció"}. {"Too many users in this conference","N'hi ha massa usuaris en esta sala de conferència"}. {"To","Per a"}. {"Total rooms","Sales totals"}. {"Traffic rate limit is exceeded","El llímit de tràfic ha sigut sobrepassat"}. {"Transactions Aborted:","Transaccions Avortades:"}. {"Transactions Committed:","Transaccions Realitzades:"}. {"Transactions Logged:","Transaccions registrades:"}. {"Transactions Restarted:","Transaccions reiniciades:"}. {"~ts's Offline Messages Queue","~ts's cua de missatges offline"}. {"Tuesday","Dimarts"}. {"Unable to generate a CAPTCHA","No s'ha pogut generar un CAPTCHA"}. {"Unable to register route on existing local domain","No s'ha pogut registrar la ruta al domini local existent"}. {"Unauthorized","No autoritzat"}. {"Unexpected action","Acció inesperada"}. {"Unexpected error condition: ~p","Condició d'error inesperada: ~p"}. {"Unregister an XMPP account","Anul·lar el registre d'un compte XMPP"}. {"Unregister","Anul·lar el registre"}. {"Unselect All","Deseleccionar tots"}. {"Unsupported <index/> element","Element <index/> no soportat"}. {"Unsupported version","Versió no suportada"}. {"Update message of the day (don't send)","Actualitzar el missatge del dia (no enviar)"}. {"Update message of the day on all hosts (don't send)","Actualitza el missatge del dia en tots els hosts (no enviar)"}. {"Update ~p","Actualitzar ~p"}. {"Update plan","Pla d'actualització"}. {"Update script","Script d'actualització"}. {"Update","Actualitzar"}. {"Uptime:","Temps en marxa:"}. {"URL for Archived Discussion Logs","URL dels Arxius de Discussions"}. {"User already exists","El usuari ja existeix"}. {"User JID","JID del usuari"}. {"User (jid)","Usuari (jid)"}. {"User Management","Gestió d'Usuaris"}. {"User removed","Usuari borrat"}. {"User session not found","Sessió d'usuari no trobada"}. {"User session terminated","Sessió d'usuari terminada"}. {"User ~ts","Usuari ~ts"}. {"Username:","Nom d'usuari:"}. {"Users are not allowed to register accounts so quickly","Els usuaris no tenen permís per a crear comptes tan depresa"}. {"Users Last Activity","Última activitat d'usuari"}. {"Users","Usuaris"}. {"User","Usuari"}. {"Validate","Validar"}. {"Value 'get' of 'type' attribute is not allowed","El valor 'get' a l'atribut 'type' no és permès"}. {"Value of '~s' should be boolean","El valor de '~s' deuria ser booleà"}. {"Value of '~s' should be datetime string","El valor de '~s' deuria ser una data"}. {"Value of '~s' should be integer","El valor de '~s' deuria ser un numero enter"}. {"Value 'set' of 'type' attribute is not allowed","El valor 'set' a l'atribut 'type' no és permès"}. {"vCard User Search","vCard recerca d'usuari"}. {"View Queue","Vore Cua"}. {"View Roster","Vore Llista de contactes"}. {"Virtual Hosts","Hosts virtuals"}. {"Visitors are not allowed to change their nicknames in this room","Els visitants no tenen permés canviar el seus Nicknames en esta sala"}. {"Visitors are not allowed to send messages to all occupants","Els visitants no poden enviar missatges a tots els ocupants"}. {"Visitor","Visitant"}. {"Voice request","Petició de veu"}. {"Voice requests are disabled in this conference","Les peticions de veu es troben desactivades en aquesta conferència"}. {"Wednesday","Dimecres"}. {"When a new subscription is processed and whenever a subscriber comes online","Quan es processa una nova subscripció i un subscriptor es connecta"}. {"When a new subscription is processed","Quan es processa una nova subscripció"}. {"When to send the last published item","Quan s'ha enviat l'última publicació"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Si una entitat vol rebre un missatge XMPP amb el format payload"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Si una entitat vol rebre resums (agregacions) de notificacions o totes les notificacions individualment"}. {"Whether an entity wants to receive or disable notifications","Si una entitat vol rebre notificacions o no"}. {"Whether owners or publisher should receive replies to items","Si el propietaris o publicadors deurien de rebre respostes als elements"}. {"Whether the node is a leaf (default) or a collection","Si el node es fulla (per defecte) o es una col·lecció"}. {"Whether to allow subscriptions","Permetre subscripcions"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Si fer totes les subscripcions temporals, basat en la presencia del subscriptor"}. {"Whether to notify owners about new subscribers and unsubscribes","Si notificar als propietaris sobre noves subscripcions i desubscripcions"}. {"Who may associate leaf nodes with a collection","Qui pot associar nodes fulla amb una col·lecció"}. {"Wrong parameters in the web formulary","Paràmetres incorrectes en el formulari web"}. {"Wrong xmlns","El xmlns ès incorrecte"}. {"XMPP Account Registration","Registre de compte XMPP"}. {"XMPP Domains","Dominis XMPP"}. {"XMPP Show Value of Away","Valor 'show' de XMPP: Ausent"}. {"XMPP Show Value of Chat","Valor 'show' de XMPP: Disposat per a xarrar"}. {"XMPP Show Value of DND (Do Not Disturb)","Valor 'show' de XMPP: DND (No Molestar)"}. {"XMPP Show Value of XA (Extended Away)","Valor 'show' de XMPP: XA (Molt Ausent)"}. {"XMPP URI of Associated Publish-Subscribe Node","URI XMPP del Node Associat Publish-Subscribe"}. {"You are being removed from the room because of a system shutdown","Has sigut expulsat de la sala perquè el sistema va a apagar-se"}. {"You are not joined to the channel","No t'has unit al canal"}. {"You can later change your password using an XMPP client.","Podràs canviar la teva contrasenya més endavant utilitzant un client XMPP."}. {"You have been banned from this room","Has sigut bloquejat en aquesta sala"}. {"You have joined too many conferences","Has entrat en massa sales de conferència"}. {"You must fill in field \"Nickname\" in the form","Deus d'omplir el camp \"Nickname\" al formulari"}. {"You need a client that supports x:data and CAPTCHA to register","Necessites un client amb suport x:data i de CAPTCHA para poder registrar-te"}. {"You need a client that supports x:data to register the nickname","Necessites un client amb suport x:data per a poder registrar el sobrenom"}. {"You need an x:data capable client to search","Necessites un client amb suport x:data per a poder buscar"}. {"Your active privacy list has denied the routing of this stanza.","La teva llista de privacitat activa ha denegat l'encaminament d'aquesta stanza."}. {"Your contact offline message queue is full. The message has been discarded.","La teua cua de missatges offline és plena. El missatge ha sigut descartat."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","La teua petició de subscripció i/o missatges a ~s han sigut bloquejats. Per a desbloquejar-los, visita ~s"}. {"Your XMPP account was successfully registered.","El teu compte XMPP ha sigut creat correctament."}. {"Your XMPP account was successfully unregistered.","El teu compte XMPP ha sigut esborrat correctament."}. {"You're not allowed to create nodes","No tens permís per a crear nodes"}. �������������������������ejabberd-21.12/priv/msgs/es.msg���������������������������������������������������������������������0000644�0002322�0002322�00000140311�14154362354�016702� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Añade * al final del campo para buscar subcadenas)"}. {" has set the subject to: "," ha puesto el asunto: "}. {"# participants","# participantes"}. {"A description of the node","Una descripción del nodo"}. {"A friendly name for the node","Un nombre sencillo para el nodo"}. {"A password is required to enter this room","(Añade * al final del campo para buscar subcadenas)"}. {"A Web Page","Una página web"}. {"Accept","Aceptar"}. {"Access denied by service policy","Acceso denegado por la política del servicio"}. {"Access model of authorize","Modelo de acceso de Autorizar"}. {"Access model of open","Modelo de acceso de Abierto"}. {"Access model of presence","Modelo de acceso de Presencia"}. {"Access model of roster","Modelo de acceso de Roster"}. {"Access model of whitelist","Modelo de acceso de Lista Blanca"}. {"Access model","Modelo de Acceso"}. {"Account doesn't exist","La cuenta no existe"}. {"Action on user","Acción en el usuario"}. {"Add Jabber ID","Añadir Jabber ID"}. {"Add New","Añadir nuevo"}. {"Add User","Añadir usuario"}. {"Administration of ","Administración de "}. {"Administration","Administración"}. {"Administrator privileges required","Se necesita privilegios de administrador"}. {"All activity","Toda la actividad"}. {"All Users","Todos los usuarios"}. {"Allow subscription","Permitir la subscripción"}. {"Allow this Jabber ID to subscribe to this pubsub node?","¿Deseas permitir a este Jabber ID que se subscriba a este nodo PubSub?"}. {"Allow this person to register with the room?","¿Permitir a esta persona que se registre en la sala?"}. {"Allow users to change the subject","Permitir a los usuarios cambiar el asunto"}. {"Allow users to query other users","Permitir a los usuarios consultar a otros usuarios"}. {"Allow users to send invites","Permitir a los usuarios enviar invitaciones"}. {"Allow users to send private messages","Permitir a los usuarios enviar mensajes privados"}. {"Allow visitors to change nickname","Permitir a los visitantes cambiarse el apodo"}. {"Allow visitors to send private messages to","Permitir a los visitantes enviar mensajes privados a"}. {"Allow visitors to send status text in presence updates","Permitir a los visitantes enviar texto de estado en las actualizaciones de presencia"}. {"Allow visitors to send voice requests","Permitir a los visitantes enviar peticiones de voz"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un grupo LDAP asociado que define la membresía a la sala; este debería ser un Nombre Distinguido de LDAP, de acuerdo con una definición de grupo específica de la implementación o de esta instalación."}. {"Announcements","Anuncios"}. {"Answer associated with a picture","Respuesta asociada con una imagen"}. {"Answer associated with a video","Respuesta asociada con un video"}. {"Answer associated with speech","Respuesta asociada con un audio"}. {"Answer to a question","Responde a una pregunta"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Cualquiera que esté en el grupo(s) de contactos especificado puede suscribirse y recibir elementos"}. {"Anyone may associate leaf nodes with the collection","Cualquiera puede asociar nodos hoja con la colección"}. {"Anyone may publish","Cualquiera puede publicar"}. {"Anyone may subscribe and retrieve items","Cualquiera puede suscribirse y recibir elementos"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Cualquiera con una suscripción a la presencia de 'ambos' o 'de' puede suscribirse y recibir elementos"}. {"Anyone with Voice","Cualquiera con Voz"}. {"Anyone","Cualquiera"}. {"April","Abril"}. {"Attribute 'channel' is required for this request","El atributo 'channel' es necesario para esta petición"}. {"Attribute 'id' is mandatory for MIX messages","El atributo 'id' es necesario para mensajes MIX"}. {"Attribute 'jid' is not allowed here","El atributo 'jid' no está permitido aqui"}. {"Attribute 'node' is not allowed here","El atributo 'node' no está permitido aqui"}. {"Attribute 'to' of stanza that triggered challenge","Atributo 'to' del paquete que disparó la comprobación"}. {"August","Agosto"}. {"Automatic node creation is not enabled","La creación automática de nodo no está activada"}. {"Backup Management","Gestión de copia de seguridad"}. {"Backup of ~p","Copia de seguridad de ~p"}. {"Backup to File at ","Guardar copia de seguridad en fichero en "}. {"Backup","Guardar copia de seguridad"}. {"Bad format","Mal formato"}. {"Birthday","Cumpleaños"}. {"Both the username and the resource are required","Se requiere tanto el nombre de usuario como el recurso"}. {"Bytestream already activated","Bytestream ya está activado"}. {"Cannot remove active list","No se puede borrar la lista activa"}. {"Cannot remove default list","No se puede borrar la lista por defecto"}. {"CAPTCHA web page","Página web de CAPTCHA"}. {"Challenge ID","ID de la comprobación"}. {"Change Password","Cambiar contraseña"}. {"Change User Password","Cambiar contraseña de usuario"}. {"Changing password is not allowed","No está permitido cambiar la contraseña"}. {"Changing role/affiliation is not allowed","No está permitido cambiar el rol/afiliación"}. {"Channel already exists","El canal ya existe"}. {"Channel does not exist","El canal no existe"}. {"Channels","Canales"}. {"Characters not allowed:","Caracteres no permitidos:"}. {"Chatroom configuration modified","Configuración de la sala modificada"}. {"Chatroom is created","Se ha creado la sala"}. {"Chatroom is destroyed","Se ha destruido la sala"}. {"Chatroom is started","Se ha iniciado la sala"}. {"Chatroom is stopped","Se ha detenido la sala"}. {"Chatrooms","Salas de charla"}. {"Choose a username and password to register with this server","Escoge un nombre de usuario y contraseña para registrarte en este servidor"}. {"Choose storage type of tables","Selecciona tipo de almacenamiento de las tablas"}. {"Choose whether to approve this entity's subscription.","Decidir si aprobar la subscripción de esta entidad."}. {"City","Ciudad"}. {"Client acknowledged more stanzas than sent by server","El cliente ha reconocido más paquetes de los que el servidor ha enviado"}. {"Commands","Comandos"}. {"Conference room does not exist","La sala de conferencias no existe"}. {"Configuration of room ~s","Configuración para la sala ~s"}. {"Configuration","Configuración"}. {"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Direcciones de contacto (normalmente la del dueño o dueños de la sala)"}. {"Country","País"}. {"CPU Time:","Tiempo consumido de CPU:"}. {"Current Discussion Topic","Tema de discusión actual"}. {"Database failure","Error en la base de datos"}. {"Database Tables at ~p","Tablas de la base de datos en ~p"}. {"Database Tables Configuration at ","Configuración de tablas de la base de datos en "}. {"Database","Base de datos"}. {"December","Diciembre"}. {"Default users as participants","Los usuarios son participantes por defecto"}. {"Delete content","Borrar contenido"}. {"Delete message of the day on all hosts","Borrar el mensaje del día en todos los dominios"}. {"Delete message of the day","Borrar mensaje del dia"}. {"Delete Selected","Borrar los seleccionados"}. {"Delete table","Borrar tabla"}. {"Delete User","Borrar usuario"}. {"Deliver event notifications","Entregar notificaciones de eventos"}. {"Deliver payloads with event notifications","Enviar contenidos junto con las notificaciones de eventos"}. {"Description:","Descripción:"}. {"Disc only copy","Copia en disco solamente"}. {"'Displayed groups' not added (they do not exist!): ","'Mostrados' que no han sido añadidos (¡no existen!): "}. {"Displayed:","Mostrados:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","No le digas tu contraseña a nadie, ni siquiera a los administradores del servidor XMPP."}. {"Dump Backup to Text File at ","Exporta copia de seguridad a fichero de texto en "}. {"Dump to Text File","Exportar a fichero de texto"}. {"Duplicated groups are not allowed by RFC6121","Los grupos duplicados no están permitidos por RFC6121"}. {"Dynamically specify a replyto of the item publisher","Especificar dinámicamente como dirección de respuesta al publicador del elemento"}. {"Edit Properties","Editar propiedades"}. {"Either approve or decline the voice request.","Aprueba o rechaza la petición de voz."}. {"ejabberd HTTP Upload service","Servicio HTTP Upload de ejabberd"}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. {"ejabberd Multicast service","Servicio Multicast de ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo de Publicar-Subscribir de ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Módulo SOCKS5 Bytestreams para ejabberd"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. {"Elements","Elementos"}. {"Email Address","Dirección de correo electrónico"}. {"Email","Correo electrónico"}. {"Enable logging","Guardar históricos"}. {"Enable message archiving","Activar el almacenamiento de mensajes"}. {"Enabling push without 'node' attribute is not supported","No está soportado activar Push sin el atributo 'node'"}. {"End User Session","Cerrar sesión de usuario"}. {"Enter nickname you want to register","Introduce el apodo que quieras registrar"}. {"Enter path to backup file","Introduce ruta al fichero de copia de seguridad"}. {"Enter path to jabberd14 spool dir","Introduce la ruta al directorio de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introduce ruta al fichero jabberd14 spool"}. {"Enter path to text file","Introduce ruta al fichero de texto"}. {"Enter the text you see","Teclea el texto que ves"}. {"Erlang XMPP Server","Servidor XMPP en Erlang"}. {"Error","Error"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluir Jabber IDs de las pruebas de CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exportar todas las tablas a un fichero SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar datos de todos los usuarios del servidor a ficheros PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar datos de los usuarios de un dominio a ficheros PIEFXIS (XEP-0227):"}. {"External component failure","Fallo en el componente externo"}. {"External component timeout","Demasiado retraso (timeout) en el componente externo"}. {"Failed to activate bytestream","Falló la activación de bytestream"}. {"Failed to extract JID from your voice request approval","Fallo al extraer el Jabber ID de tu aprobación de petición de voz"}. {"Failed to map delegated namespace to external component","Falló el mapeo de espacio de nombres delegado al componente externo"}. {"Failed to parse HTTP response","Falló la comprensión de la respuesta HTTP"}. {"Failed to process option '~s'","Falló el procesado de la opción '~s'"}. {"Family Name","Apellido"}. {"FAQ Entry","Apunte en la FAQ"}. {"February","Febrero"}. {"File larger than ~w bytes","El fichero es más grande que ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Rellena campos para buscar usuarios XMPP que concuerden"}. {"Friday","Viernes"}. {"From ~ts","De ~ts"}. {"From","De"}. {"Full List of Room Admins","Lista completa de administradores de la sala"}. {"Full List of Room Owners","Lista completa de dueños de la sala"}. {"Full Name","Nombre completo"}. {"Get Number of Online Users","Ver número de usuarios conectados"}. {"Get Number of Registered Users","Ver número de usuarios registrados"}. {"Get Pending","Obtener pendientes"}. {"Get User Last Login Time","Ver fecha de la última conexión de usuario"}. {"Get User Password","Ver contraseña de usuario"}. {"Get User Statistics","Ver estadísticas de usuario"}. {"Given Name","Nombre"}. {"Grant voice to this person?","¿Conceder voz a esta persona?"}. {"Group","Grupo"}. {"Groups that will be displayed to the members","Grupos que se mostrarán a los miembros"}. {"Groups","Grupos"}. {"has been banned","ha sido bloqueado"}. {"has been kicked because of a system shutdown","ha sido expulsado porque el sistema se va a detener"}. {"has been kicked because of an affiliation change","ha sido expulsado por un cambio de su afiliación"}. {"has been kicked because the room has been changed to members-only","ha sido expulsado porque la sala es ahora solo para miembros"}. {"has been kicked","ha sido expulsado"}. {"Host unknown","Dominio desconocido"}. {"Host","Dominio"}. {"HTTP File Upload","Subir fichero por HTTP"}. {"Idle connection","Conexión sin uso"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si no ves la imagen CAPTCHA aquí, visita la página web."}. {"Import Directory","Importar directorio"}. {"Import File","Importar fichero"}. {"Import user data from jabberd14 spool file:","Importar usuario de fichero spool de jabberd14:"}. {"Import User from File at ","Importa usuario desde fichero en "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importar usuarios desde un fichero PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar usuarios del directorio spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuarios desde el directorio en "}. {"Import Users From jabberd14 Spool Files","Importar usuarios de ficheros spool de jabberd-1.4"}. {"Improper domain part of 'from' attribute","Parte de dominio impropia en el atributo 'from'"}. {"Improper message type","Tipo de mensaje incorrecto"}. {"Incoming s2s Connections:","Conexiones S2S entrantes:"}. {"Incorrect CAPTCHA submit","El CAPTCHA proporcionado es incorrecto"}. {"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contraseña incorrecta"}. {"Incorrect value of 'action' attribute","Valor incorrecto del atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorrecto de 'action' en el formulario de datos"}. {"Incorrect value of 'path' in data form","Valor incorrecto de 'path' en el formulario de datos"}. {"Insufficient privilege","Privilegio insuficiente"}. {"Internal server error","Error interno en el servidor"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from' no válido en el mensaje reenviado"}. {"Invalid node name","Nombre de nodo no válido"}. {"Invalid 'previd' value","Valor de 'previd' no válido"}. {"Invitations are not allowed in this conference","Las invitaciones no están permitidas en esta sala"}. {"IP addresses","Direcciones IP"}. {"is now known as","se cambia el nombre a"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","No está permitido enviar mensajes de error a la sala. Este participante (~s) ha enviado un mensaje de error (~s) y fue expulsado de la sala"}. {"It is not allowed to send private messages of type \"groupchat\"","No está permitido enviar mensajes privados del tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir el envio de mensajes privados a la sala"}. {"It is not allowed to send private messages","No está permitido enviar mensajes privados"}. {"Jabber ID","Jabber ID"}. {"January","Enero"}. {"JID normalization denied by service policy","Se ha denegado la normalización del JID por política del servicio"}. {"JID normalization failed","Ha fallado la normalización del JID"}. {"joins the room","entra en la sala"}. {"July","Julio"}. {"June","Junio"}. {"Just created","Recién creada"}. {"Label:","Etiqueta:"}. {"Last Activity","Última actividad"}. {"Last login","Última conexión"}. {"Last message","Último mensaje"}. {"Last month","Último mes"}. {"Last year","Último año"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Los bits menos significativos del hash SHA-256 del texto deberían ser iguales a la etiqueta hexadecimal"}. {"leaves the room","sale de la sala"}. {"List of rooms","Lista de salas"}. {"Logging","Histórico de mensajes"}. {"Low level update script","Script de actualización a bajo nivel"}. {"Make participants list public","La lista de participantes es pública"}. {"Make room CAPTCHA protected","Proteger la sala con CAPTCHA"}. {"Make room members-only","Sala sólo para miembros"}. {"Make room moderated","Sala moderada"}. {"Make room password protected","Proteger la sala con contraseña"}. {"Make room persistent","Sala permanente"}. {"Make room public searchable","Sala públicamente visible"}. {"Malformed username","Nombre de usuario mal formado"}. {"MAM preference modification denied by service policy","Se ha denegado modificar la preferencia MAM por política del servicio"}. {"March","Marzo"}. {"Max # of items to persist","Máximo # de elementos que persisten"}. {"Max payload size in bytes","Máximo tamaño del contenido en bytes"}. {"Maximum file size","Tamaño máximo de fichero"}. {"Maximum Number of History Messages Returned by Room","Máximo número de mensajes del historial devueltos por la sala"}. {"Maximum number of items to persist","Máximo número de elementos que persisten"}. {"Maximum Number of Occupants","Número máximo de ocupantes"}. {"May","Mayo"}. {"Members not added (inexistent vhost!): ","Miembros no añadidos (el vhost no existe): "}. {"Membership is required to enter this room","Necesitas ser miembro de esta sala para poder entrar"}. {"Members:","Miembros:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memoriza tu contraseña, o apúntala en un papel en un lugar seguro. En XMPP no hay un método automatizado para recuperar la contraseña si la olvidas."}. {"Memory","Memoria"}. {"Mere Availability in XMPP (No Show Value)","Disponible en XMPP (sin valor de Mostrado)"}. {"Message body","Cuerpo del mensaje"}. {"Message not found in forwarded payload","Mensaje no encontrado en el contenido reenviado"}. {"Messages from strangers are rejected","Los mensajes de extraños son rechazados"}. {"Messages of type headline","Mensajes de tipo titular"}. {"Messages of type normal","Mensajes de tipo normal"}. {"Middle Name","Segundo nombre"}. {"Minimum interval between voice requests (in seconds)","Intervalo mínimo entre peticiones de voz (en segundos)"}. {"Moderator privileges required","Se necesita privilegios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Solo moderadores"}. {"Modified modules","Módulos modificados"}. {"Module failed to handle the query","El módulo falló al gestionar la petición"}. {"Monday","Lunes"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","No se permiten múltiples elementos <item/> en RFC6121"}. {"Multi-User Chat","Salas de Charla"}. {"Name in the rosters where this group will be displayed","Nombre del grupo con que aparecerá en las listas de contactos"}. {"Name","Nombre"}. {"Name:","Nombre:"}. {"Natural Language for Room Discussions","Idioma natural en las charlas de la sala"}. {"Natural-Language Room Name","Nombre de la sala en el idioma natural de la sala"}. {"Neither 'jid' nor 'nick' attribute found","No se encontraron los atributos 'jid' ni 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","No se encontraron los atributos 'role' ni 'affiliation'"}. {"Never","Nunca"}. {"New Password:","Nueva contraseña:"}. {"Nickname can't be empty","El apodo no puede estar vacío"}. {"Nickname Registration at ","Registro del apodo en "}. {"Nickname ~s does not exist in the room","El apodo ~s no existe en la sala"}. {"Nickname","Apodo"}. {"No address elements found","No se encontraron elementos de dirección ('address')"}. {"No addresses element found","No se encontró elemento de direcciones ('addresses')"}. {"No 'affiliation' attribute found","No se encontró el atributo 'affiliation'"}. {"No available resource found","No se encontró un recurso conectado"}. {"No body provided for announce message","No se ha proporcionado cuerpo de mensaje para el anuncio"}. {"No child elements found","No se encontraron subelementos"}. {"No data form found","No se encontró formulario de datos"}. {"No Data","Sin datos"}. {"No features available","No hay características disponibles"}. {"No <forwarded/> element found","No se ha encontrado elemento <forwarded/>"}. {"No hook has processed this command","Ningún evento ha procesado este comando"}. {"No info about last activity found","No hay información respeto a la última actividad"}. {"No 'item' element found","No se encontró el elemento 'item'"}. {"No items found in this query","No se han encontrado elementos en esta petición"}. {"No limit","Sin límite"}. {"No module is handling this query","Ningún modulo está gestionando esta petición"}. {"No node specified","No se ha especificado ningún nodo"}. {"No 'password' found in data form","No se encontró 'password' en el formulario de datos"}. {"No 'password' found in this query","No se encontró 'password' en esta petición"}. {"No 'path' found in data form","No se encontró 'path' en este formulario de datos"}. {"No pending subscriptions found","No se han encontrado suscripciones pendientes"}. {"No privacy list with this name found","No se ha encontrado una lista de privacidad con este nombre"}. {"No private data found in this query","No se ha encontrado ningún elemento de dato privado en esta petición"}. {"No running node found","No se ha encontrado ningún nodo activo"}. {"No services available","No hay servicios disponibles"}. {"No statistics found for this item","No se han encontrado estadísticas para este elemento"}. {"No 'to' attribute found in the invitation","No se encontró el atributo 'to' en la invitación"}. {"Nobody","Nadie"}. {"Node already exists","El nodo ya existe"}. {"Node ID","Nodo ID"}. {"Node index not found","No se ha encontrado índice de nodo"}. {"Node not found","Nodo no encontrado"}. {"Node ~p","Nodo ~p"}. {"Nodeprep has failed","Ha fallado el procesado del nombre de nodo (nodeprep)"}. {"Nodes","Nodos"}. {"None","Ninguno"}. {"Not allowed","No permitido"}. {"Not Found","No encontrado"}. {"Not subscribed","No suscrito"}. {"Notify subscribers when items are removed from the node","Notificar subscriptores cuando los elementos se borran del nodo"}. {"Notify subscribers when the node configuration changes","Notificar subscriptores cuando cambia la configuración del nodo"}. {"Notify subscribers when the node is deleted","Notificar subscriptores cuando el nodo se borra"}. {"November","Noviembre"}. {"Number of answers required","Número de respuestas necesarias"}. {"Number of occupants","Número de ocupantes"}. {"Number of Offline Messages","Número de mensajes diferidos"}. {"Number of online users","Número de usuarios conectados"}. {"Number of registered users","Número de usuarios registrados"}. {"Number of seconds after which to automatically purge items","Número de segundos después de los cuales se purgarán elementos automáticamente"}. {"Occupants are allowed to invite others","Los ocupantes pueden invitar a otras personas"}. {"Occupants May Change the Subject","Los ocupantes pueden cambiar el Asunto"}. {"October","Octubre"}. {"Offline Messages","Mensajes diferidos"}. {"Offline Messages:","Mensajes diferidos:"}. {"OK","Aceptar"}. {"Old Password:","Contraseña antigua:"}. {"Online Users","Usuarios conectados"}. {"Online Users:","Usuarios conectados:"}. {"Online","Conectado"}. {"Only admins can see this","Solo los administradores pueden ver esto"}. {"Only collection node owners may associate leaf nodes with the collection","Solo los dueños e la colección de nodos pueden asociar nodos hoja a la colección"}. {"Only deliver notifications to available users","Solo enviar notificaciones a los usuarios disponibles"}. {"Only <enable/> or <disable/> tags are allowed","Solo se permiten las etiquetas <enable/> o <disable/>"}. {"Only <list/> element is allowed in this query","Solo se permite el elemento <list/> en esta petición"}. {"Only members may query archives of this room","Solo miembros pueden consultar el archivo de mensajes de la sala"}. {"Only moderators and participants are allowed to change the subject in this room","Solo los moderadores y participantes pueden cambiar el asunto de esta sala"}. {"Only moderators are allowed to change the subject in this room","Solo los moderadores pueden cambiar el asunto de esta sala"}. {"Only moderators can approve voice requests","Solo los moderadores pueden aprobar peticiones de voz"}. {"Only occupants are allowed to send messages to the conference","Solo los ocupantes pueden enviar mensajes a la sala"}. {"Only occupants are allowed to send queries to the conference","Solo los ocupantes pueden enviar solicitudes a la sala"}. {"Only publishers may publish","Solo los publicadores pueden publicar"}. {"Only service administrators are allowed to send service messages","Solo los administradores del servicio tienen permiso para enviar mensajes de servicio"}. {"Only those on a whitelist may associate leaf nodes with the collection","Solo quienes están en una lista blanca pueden asociar nodos hoja a la colección"}. {"Only those on a whitelist may subscribe and retrieve items","Solo quienes están en una lista blanca pueden suscribirse y recibir elementos"}. {"Organization Name","Nombre de la organización"}. {"Organization Unit","Unidad de la organización"}. {"Outgoing s2s Connections","Conexiones S2S salientes"}. {"Outgoing s2s Connections:","Conexiones S2S salientes:"}. {"Owner privileges required","Se requieren privilegios de propietario de la sala"}. {"Packet relay is denied by service policy","Se ha denegado el reenvío del paquete por política del servicio"}. {"Packet","Paquete"}. {"Participant","Participante"}. {"Password Verification","Verificación de la contraseña"}. {"Password Verification:","Verificación de la contraseña:"}. {"Password","Contraseña"}. {"Password:","Contraseña:"}. {"Path to Dir","Ruta al directorio"}. {"Path to File","Ruta al fichero"}. {"Payload type","Tipo de payload"}. {"Pending","Pendiente"}. {"Period: ","Periodo: "}. {"Persist items to storage","Persistir elementos al almacenar"}. {"Persistent","Permanente"}. {"Ping query is incorrect","La petición de Ping es incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ten en cuenta que estas opciones solo harán copia de seguridad de la base de datos Mnesia embebida. Si estás usando ODBC tendrás que hacer también copia de seguridad de tu base de datos SQL."}. {"Please, wait for a while before sending new voice request","Por favor, espera un poco antes de enviar otra petición de voz"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Poseer el atributo 'ask' no está permitido por RFC6121"}. {"Present real Jabber IDs to","Los Jabber ID reales pueden verlos"}. {"Previous session not found","La sesión previa no ha sido encontrada"}. {"Previous session PID has been killed","El proceso de la sesión previa ha sido cerrado"}. {"Previous session PID has exited","El proceso de la sesión previa ha terminado"}. {"Previous session PID is dead","El proceso de la sesión previa está muerto"}. {"Previous session timed out","La sesión previa ha caducado"}. {"private, ","privado, "}. {"Public","Público"}. {"Publish model","Modelo de publicación"}. {"Publish-Subscribe","Servicio de Publicar-Subscribir"}. {"PubSub subscriber request","Petición de subscriptor de PubSub"}. {"Purge all items when the relevant publisher goes offline","Borra todos los elementos cuando el publicador relevante se desconecta"}. {"Push record not found","No se encontró registro Push"}. {"Queries to the conference members are not allowed in this room","En esta sala no se permiten solicitudes a los miembros de la sala"}. {"Query to another users is forbidden","Enviar solicitudes a otros usuarios está prohibido"}. {"RAM and disc copy","Copia en RAM y disco"}. {"RAM copy","Copia en RAM"}. {"Really delete message of the day?","¿Está seguro de quere borrar el mensaje del dia?"}. {"Receive notification from all descendent nodes","Recibir notificaciones de todos los nodos descendientes"}. {"Receive notification from direct child nodes only","Recibir notificaciones solo de los nodos que son hijos directos"}. {"Receive notification of new items only","Recibir notificaciones solo de nuevos elementos"}. {"Receive notification of new nodes only","Recibir notificaciones solo de nuevos nodos"}. {"Recipient is not in the conference room","El receptor no está en la sala de conferencia"}. {"Register an XMPP account","Registrar una cuenta XMPP"}. {"Registered Users","Usuarios registrados"}. {"Registered Users:","Usuarios registrados:"}. {"Register","Registrar"}. {"Remote copy","Copia remota"}. {"Remove All Offline Messages","Borrar todos los mensajes diferidos"}. {"Remove User","Eliminar usuario"}. {"Remove","Borrar"}. {"Replaced by new connection","Reemplazado por una nueva conexión"}. {"Request has timed out","La petición ha caducado"}. {"Request is ignored","La petición ha sido ignorada"}. {"Requested role","Rol solicitado"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar el servicio"}. {"Restart","Reiniciar"}. {"Restore Backup from File at ","Restaura copia de seguridad desde el fichero en "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar copia de seguridad binaria en el siguiente reinicio de ejabberd (requiere menos memoria que si instantánea):"}. {"Restore binary backup immediately:","Restaurar inmediatamente copia de seguridad binaria:"}. {"Restore plain text backup immediately:","Restaurar copias de seguridad de texto plano inmediatamente:"}. {"Restore","Restaurar"}. {"Roles and Affiliations that May Retrieve Member List","Roles y Afiliaciones que pueden obtener la lista de miembros"}. {"Roles for which Presence is Broadcasted","Roles para los que sí se difunde su Presencia"}. {"Roles that May Send Private Messages","Roles que pueden enviar mensajes privados"}. {"Room Configuration","Configuración de la sala"}. {"Room creation is denied by service policy","Se te ha denegado crear la sala por política del servicio"}. {"Room description","Descripción de la sala"}. {"Room Occupants","Ocupantes de la sala"}. {"Room terminates","Cerrando la sala"}. {"Room title","Título de la sala"}. {"Roster groups allowed to subscribe","Grupos de contactos que pueden suscribirse"}. {"Roster of ~ts","Lista de contactos de ~ts"}. {"Roster size","Tamaño de la lista de contactos"}. {"Roster:","Lista de contactos:"}. {"RPC Call Error","Error en la llamada RPC"}. {"Running Nodes","Nodos funcionando"}. {"~s invites you to the room ~s","~s te invita a la sala ~s"}. {"Saturday","Sábado"}. {"Script check","Comprobación de script"}. {"Search from the date","Buscar desde la fecha"}. {"Search Results for ","Buscar resultados por "}. {"Search the text","Buscar el texto"}. {"Search until the date","Buscar hasta la fecha"}. {"Search users in ","Buscar usuarios en "}. {"Select All","Seleccionar todo"}. {"Send announcement to all online users on all hosts","Enviar anuncio a todos los usuarios conectados en todos los dominios"}. {"Send announcement to all online users","Enviar anuncio a todos los usuarios conectados"}. {"Send announcement to all users on all hosts","Enviar anuncio a todos los usuarios en todos los dominios"}. {"Send announcement to all users","Enviar anuncio a todos los usuarios"}. {"September","Septiembre"}. {"Server:","Servidor:"}. {"Service list retrieval timed out","Ha caducado la obtención de la lista de servicio"}. {"Session state copying timed out","El copiado del estado de la sesión ha caducado"}. {"Set message of the day and send to online users","Poner mensaje del dia y enviar a todos los usuarios conectados"}. {"Set message of the day on all hosts and send to online users","Poner mensaje del día en todos los dominios y enviar a los usuarios conectados"}. {"Shared Roster Groups","Grupos Compartidos"}. {"Show Integral Table","Mostrar Tabla Integral"}. {"Show Ordinary Table","Mostrar Tabla Ordinaria"}. {"Shut Down Service","Detener el servicio"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Algunos clientes XMPP pueden guardar tu contraseña en la máquina, pero solo deberías hacer esto en tu propia máquina personal, por razones de seguridad."}. {"Specify the access model","Especifica el modelo de acceso"}. {"Specify the event message type","Especifica el tipo del mensaje de evento"}. {"Specify the publisher model","Especificar el modelo del publicante"}. {"Stanza ID","ID del paquete"}. {"Statically specify a replyto of the node owner(s)","Especificar de forma estática un 'replyto' de dueño(s) del nodo"}. {"Statistics of ~p","Estadísticas de ~p"}. {"Statistics","Estadísticas"}. {"Stop","Detener"}. {"Stopped Nodes","Nodos detenidos"}. {"Storage Type","Tipo de almacenamiento"}. {"Store binary backup:","Guardar copia de seguridad binaria:"}. {"Store plain text backup:","Guardar copia de seguridad en texto plano:"}. {"Stream management is already enabled","Ya está activada la administración de la conexión"}. {"Stream management is not enabled","No está activada la administración de la conexión"}. {"Subject","Asunto"}. {"Submit","Enviar"}. {"Submitted","Enviado"}. {"Subscriber Address","Dirección del subscriptor"}. {"Subscribers may publish","Los suscriptores pueden publicar"}. {"Subscription requests must be approved and only subscribers may retrieve items","Las peticiones de suscripción deben ser aprobadas y solo los suscriptores pueden obtener elementos"}. {"Subscriptions are not allowed","Las subscripciones no están permitidas"}. {"Subscription","Subscripción"}. {"Sunday","Domingo"}. {"Text associated with a picture","Texto asociado con una imagen"}. {"Text associated with a sound","Texto asociado con un sonido"}. {"Text associated with a video","Texto asociado con un vídeo"}. {"Text associated with speech","Texto asociado con una charla"}. {"That nickname is already in use by another occupant","Ese apodo ya está siendo usado por otro ocupante"}. {"That nickname is registered by another person","El apodo ya está registrado por otra persona"}. {"The account already exists","La cuenta ya existe"}. {"The account was not unregistered","La cuenta no fue eliminada"}. {"The body text of the last received message","El contenido de texto del último mensaje recibido"}. {"The CAPTCHA is valid.","El CAPTCHA es válido."}. {"The CAPTCHA verification has failed","La verificación de CAPTCHA ha fallado"}. {"The captcha you entered is wrong","El CAPTCHA que has introducido es erróneo"}. {"The child nodes (leaf or collection) associated with a collection","Los nodos hijos (ya sean hojas o colecciones) asociados con una colección"}. {"The collections with which a node is affiliated","Las colecciones a las que un nodo está afiliado"}. {"The DateTime at which a leased subscription will end or has ended","La FechayHora en la que una suscripción prestada acabará o ha terminado"}. {"The datetime when the node was created","La fechayhora cuando el nodo fue creado"}. {"The default language of the node","El nombre por defecto del nodo"}. {"The feature requested is not supported by the conference","La característica solicitada no está soportada por la sala de conferencia"}. {"The JID of the node creator","El JID del creador del nodo"}. {"The JIDs of those to contact with questions","Los JIDs a quienes contactar con preguntas"}. {"The JIDs of those with an affiliation of owner","Los JIDs de quienes tienen una afiliación de dueños"}. {"The JIDs of those with an affiliation of publisher","Los JIDs de quienes tienen una afiliación de publicadores"}. {"The list of JIDs that may associate leaf nodes with a collection","La lista de JIDs que pueden asociar nodos hijo con una colección"}. {"The maximum number of child nodes that can be associated with a collection","El número máximo de nodos hijo que pueden asociarse a una colección"}. {"The minimum number of milliseconds between sending any two notification digests","El número mínimo de milisegundos entre dos envíos de resumen de notificaciones"}. {"The name of the node","El nombre del nodo"}. {"The node is a collection node","El nodo es una colección"}. {"The node is a leaf node (default)","El nodo es un nodo hoja (por defecto)"}. {"The NodeID of the relevant node","El NodoID del nodo relevante"}. {"The number of pending incoming presence subscription requests","El número de peticiones de suscripción a presencia que están pendientes de llegar"}. {"The number of subscribers to the node","El número de suscriptores al nodo"}. {"The number of unread or undelivered messages","El número de mensajes sin leer o sin entregar"}. {"The password contains unacceptable characters","La contraseña contiene caracteres inaceptables"}. {"The password is too weak","La contraseña es demasiado débil"}. {"the password is","la contraseña es"}. {"The password of your XMPP account was successfully changed.","La contraseña de tu cuenta XMPP se ha cambiado correctamente."}. {"The password was not changed","La contraseña no fue cambiada"}. {"The passwords are different","Las contraseñas son diferentes"}. {"The presence states for which an entity wants to receive notifications","Los estados de presencia para los cuales una entidad quiere recibir notificaciones"}. {"The query is only allowed from local users","La solicitud está permitida solo para usuarios locales"}. {"The query must not contain <item/> elements","La solicitud no debe contener elementos <item/>"}. {"The room subject can be modified by participants","El asunto de la sala puede ser modificado por los participantes"}. {"The sender of the last received message","El emisor del último mensaje recibido"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","El paquete DEBE contener solo un elemento <active/>, un elemento <default/>, o un elemento <list/>"}. {"The subscription identifier associated with the subscription request","El identificador de suscripción asociado con la petición de suscripción"}. {"The type of node data, usually specified by the namespace of the payload (if any)","El tipo de datos del nodo, usualmente especificado por el namespace del payload (en caso de haberlo)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","La URL de una transformación XSL que puede aplicarse a payloads para generar un elemento de contenido del mensaje apropiado."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","La URL de una transformación XSL que puede aplicarse al formato de payload para generar un resultado de Formulario de Datos válido, que el cliente pueda mostrar usando un mecanismo de dibujado genérico de Formulario de Datos"}. {"The username is not valid","El nombre de usuario no es válido"}. {"There was an error changing the password: ","Hubo uno error al cambiar la contaseña: "}. {"There was an error creating the account: ","Hubo uno error al crear la cuenta: "}. {"There was an error deleting the account: ","Hubo un error borrando la cuenta: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","No importa si usas mayúsculas: macbeth es lo mismo que MacBeth y Macbeth."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta página te permite crear una cuenta XMPP este servidor XMPP. Tu JID (Jabber ID) será de la forma: nombredeusuario@servidor. Por favor lee detenidamente las instrucciones para rellenar correctamente los campos."}. {"This page allows to unregister an XMPP account in this XMPP server.","Esta página te permite borrar tu cuenta XMPP en este servidor XMPP."}. {"This room is not anonymous","Sala no anónima"}. {"This service can not process the address: ~s","Este servicio no puede procesar la dirección: ~s"}. {"Thursday","Jueves"}. {"Time delay","Retraso temporal"}. {"Timed out waiting for stream resumption","Ha pasado demasiado tiempo esperando que la conexión se restablezca"}. {"Time","Fecha"}. {"To register, visit ~s","Para registrarte, visita ~s"}. {"To ~ts","A ~ts"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Demasiados bytestreams activos"}. {"Too many CAPTCHA requests","Demasiadas peticiones de CAPTCHA"}. {"Too many child elements","Demasiados subelementos"}. {"Too many <item/> elements","Demasiados elementos <item/>"}. {"Too many <list/> elements","Demasiados elementos <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Demasiadas (~p) autenticaciones fallidas de esta dirección IP (~s). La dirección será desbloqueada en ~s UTC"}. {"Too many receiver fields were specified","Se han especificado demasiados campos de destinatario"}. {"Too many unacked stanzas","Demasiados mensajes sin haber reconocido recibirlos"}. {"Too many users in this conference","Demasiados usuarios en esta sala"}. {"To","Para"}. {"Total rooms","Salas totales"}. {"Traffic rate limit is exceeded","Se ha exedido el límite de tráfico"}. {"Transactions Aborted:","Transacciones abortadas:"}. {"Transactions Committed:","Transacciones finalizadas:"}. {"Transactions Logged:","Transacciones registradas:"}. {"Transactions Restarted:","Transacciones reiniciadas:"}. {"~ts's Offline Messages Queue","Cola de mensajes diferidos de ~ts"}. {"Tuesday","Martes"}. {"Unable to generate a CAPTCHA","No se pudo generar un CAPTCHA"}. {"Unable to register route on existing local domain","No se ha podido registrar la ruta en este dominio local existente"}. {"Unauthorized","No autorizado"}. {"Unexpected action","Acción inesperada"}. {"Unexpected error condition: ~p","Condición de error inesperada: ~p"}. {"Unregister an XMPP account","Borrar una cuenta XMPP"}. {"Unregister","Borrar"}. {"Unselect All","Deseleccionar todo"}. {"Unsupported <index/> element","Elemento <index/> no soportado"}. {"Unsupported version","Versión no soportada"}. {"Update message of the day (don't send)","Actualizar mensaje del dia, pero no enviarlo"}. {"Update message of the day on all hosts (don't send)","Actualizar el mensaje del día en todos los dominos (pero no enviarlo)"}. {"Update ~p","Actualizar ~p"}. {"Update plan","Plan de actualización"}. {"Update script","Script de actualización"}. {"Update","Actualizar"}. {"Uptime:","Tiempo desde el inicio:"}. {"URL for Archived Discussion Logs","URL del registro de discusiones archivadas"}. {"User already exists","El usuario ya existe"}. {"User JID","Jabber ID del usuario"}. {"User (jid)","Usuario (jid)"}. {"User Management","Administración de usuarios"}. {"User removed","Usuario eliminado"}. {"User session not found","Sesión de usuario no encontrada"}. {"User session terminated","Sesión de usuario terminada"}. {"User ~ts","Usuario ~ts"}. {"Username:","Nombre de usuario:"}. {"Users are not allowed to register accounts so quickly","Los usuarios no tienen permitido crear cuentas con tanta rapidez"}. {"Users Last Activity","Última actividad de los usuarios"}. {"Users","Usuarios"}. {"User","Usuario"}. {"Validate","Validar"}. {"Value 'get' of 'type' attribute is not allowed","El valor 'get' del atributo 'type' no está permitido"}. {"Value of '~s' should be boolean","El valor de '~s' debería ser booleano"}. {"Value of '~s' should be datetime string","El valor de '~s' debería ser una fecha"}. {"Value of '~s' should be integer","El valor de '~s' debería ser un entero"}. {"Value 'set' of 'type' attribute is not allowed","El valor 'set' del atributo 'type' no está permitido"}. {"vCard User Search","Búsqueda de vCard de usuarios"}. {"View Queue","Ver Cola"}. {"View Roster","Ver Lista de contactos"}. {"Virtual Hosts","Dominios Virtuales"}. {"Visitors are not allowed to change their nicknames in this room","Los visitantes no tienen permitido cambiar sus apodos en esta sala"}. {"Visitors are not allowed to send messages to all occupants","Los visitantes no pueden enviar mensajes a todos los ocupantes"}. {"Visitor","Visitante"}. {"Voice request","Petición de voz"}. {"Voice requests are disabled in this conference","Las peticiones de voz están desactivadas en esta sala"}. {"Wednesday","Miércoles"}. {"When a new subscription is processed and whenever a subscriber comes online","Cuando se procesa una nueva suscripción y cuando un suscriptor se conecta"}. {"When a new subscription is processed","Cuando se procesa una nueva suscripción"}. {"When to send the last published item","Cuando enviar el último elemento publicado"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Si una entidad quiere recibir un cuerpo de mensaje XMPP adicionalmente al formato de payload"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Si una entidad quiere recibir resúmenes (agregados) de notificaciones o todas las notificaciones individualmente"}. {"Whether an entity wants to receive or disable notifications","Si una entidad quiere recibir o desactivar las notificaciones"}. {"Whether owners or publisher should receive replies to items","Si dueños y publicadores deberían recibir respuestas de los elementos"}. {"Whether the node is a leaf (default) or a collection","Si el nodo es una hoja (por defecto) o una colección"}. {"Whether to allow subscriptions","Permitir subscripciones"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Si hacer que todas las suscripciones sean temporales, basado en la presencia del suscriptor"}. {"Whether to notify owners about new subscribers and unsubscribes","Si notificar a los dueños sobre nuevas suscripciones y desuscripciones"}. {"Who may associate leaf nodes with a collection","Quien puede asociar nodos hoja con una colección"}. {"Wrong parameters in the web formulary","Parámetros incorrectos en el formulario web"}. {"Wrong xmlns","XMLNS incorrecto"}. {"XMPP Account Registration","Registro de Cuenta XMPP"}. {"XMPP Domains","Dominios XMPP"}. {"XMPP Show Value of Away","Valor 'Show' de XMPP: Ausente"}. {"XMPP Show Value of Chat","Valor 'Show' de XMPP: Charlador"}. {"XMPP Show Value of DND (Do Not Disturb)","Valor 'Show' de XMPP: DND (No Molestar)"}. {"XMPP Show Value of XA (Extended Away)","Valor 'Show' de XMPP: XA (Ausente Extendido)"}. {"XMPP URI of Associated Publish-Subscribe Node","URI XMPP del Nodo Asociado de Publicar-Subscribir"}. {"You are being removed from the room because of a system shutdown","Estás siendo expulsado de la sala porque el sistema se va a detener"}. {"You are not joined to the channel","No has entrado en el canal"}. {"You can later change your password using an XMPP client.","Puedes cambiar tu contraseña después, usando un cliente XMPP."}. {"You have been banned from this room","Has sido bloqueado en esta sala"}. {"You have joined too many conferences","Has entrado en demasiadas salas de conferencia"}. {"You must fill in field \"Nickname\" in the form","Debes rellenar el campo \"Apodo\" en el formulario"}. {"You need a client that supports x:data and CAPTCHA to register","Necesitas un cliente con soporte de x:data y CAPTCHA para registrarte"}. {"You need a client that supports x:data to register the nickname","Necesitas un cliente con soporte de x:data para poder registrar el apodo"}. {"You need an x:data capable client to search","Necesitas un cliente con soporte de x:data para poder buscar"}. {"Your active privacy list has denied the routing of this stanza.","Tu lista de privacidad activa ha denegado el envío de este paquete."}. {"Your contact offline message queue is full. The message has been discarded.","Tu cola de mensajes diferidos de contactos está llena. El mensaje se ha descartado."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Tu petición de suscripción y/o mensajes a ~s ha sido bloqueado. Para desbloquear tu petición de suscripción visita ~s"}. {"Your XMPP account was successfully registered.","Tu cuenta XMPP se ha registrado correctamente."}. {"Your XMPP account was successfully unregistered.","Tu cuenta XMPP se ha borrado correctamente."}. {"You're not allowed to create nodes","No tienes permitido crear nodos"}. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/id.msg���������������������������������������������������������������������0000644�0002322�0002322�00000102116�14154362354�016670� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," Isi formulir untuk pencarian pengguna Jabber yang cocok (Tambahkan * ke mengakhiri pengisian untuk menyamakan kata)"}. {" has set the subject to: "," telah menetapkan topik yaitu: "}. {"# participants","# pengguna"}. {"A description of the node","Deskripsi node"}. {"A friendly name for the node","Nama yang dikenal untuk node"}. {"A password is required to enter this room","Diperlukan kata sandi untuk masuk ruangan ini"}. {"A Web Page","Halaman web"}. {"Accept","Diterima"}. {"Access denied by service policy","Akses ditolak oleh kebijakan layanan"}. {"Access model of authorize","Model akses otorisasi"}. {"Access model of open","Model akses terbuka"}. {"Access model of presence","Model akses kehadiran"}. {"Access model of roster","model akses daftar kontak"}. {"Access model of whitelist","Model akses daftar putih"}. {"Access model","Model akses"}. {"Account doesn't exist","Akun tidak ada"}. {"Action on user","Tindakan pada pengguna"}. {"Add Jabber ID","Tambah Jabber ID"}. {"Add New","Tambah Baru"}. {"Add User","Tambah Pengguna"}. {"Administration of ","Administrasi "}. {"Administration","Administrasi"}. {"Administrator privileges required","Hak istimewa Administrator dibutuhkan"}. {"All activity","Semua aktifitas"}. {"All Users","Semua Pengguna"}. {"Allow subscription","Ijinkan berlangganan"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Izinkan ID Jabber ini untuk berlangganan pada node pubsub ini?"}. {"Allow this person to register with the room?","Ijinkan orang ini mendaftar masuk kamar?"}. {"Allow users to change the subject","Perbolehkan pengguna untuk mengganti topik"}. {"Allow users to query other users","Perbolehkan pengguna untuk mengetahui pengguna lain"}. {"Allow users to send invites","Perbolehkan pengguna mengirimkan undangan"}. {"Allow users to send private messages","perbolehkan pengguna mengirimkan pesan ke pengguna lain secara pribadi"}. {"Allow visitors to change nickname","Perbolehkan visitor mengganti nama julukan"}. {"Allow visitors to send private messages to","Izinkan pengunjung mengirimkan pesan privat ke"}. {"Allow visitors to send status text in presence updates","Izinkan pengunjung untuk mengirim teks status terbaru"}. {"Allow visitors to send voice requests","Izinkan pengunjung mengirim permintaan suara"}. {"Announcements","Pengumuman"}. {"Answer associated with a picture","Jawaban yang berhubungan dengan gambar"}. {"Answer associated with a video","Jawaban yang berhubungan dengan video"}. {"Answer associated with speech","Jawaban yang berhubungan dengan ucapan"}. {"Answer to a question","Jawaban pertanyaan"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Siapapun dalam keanggotaan grup tertentu dapat berlangganan dan mengambil item"}. {"Anyone may publish","Siapapun dapat mempublikasi"}. {"Anyone may subscribe and retrieve items","Siapapun dapat berlangganan dan mengambil item"}. {"Anyone with Voice","Siapapun dengan fungsi suara"}. {"Anyone","Siapapun"}. {"April","April"}. {"Attribute 'channel' is required for this request","Atribut 'channel' diperlukan untuk permintaan ini"}. {"Attribute 'id' is mandatory for MIX messages","Atribut 'id' harus ada untuk pesan MIX"}. {"Attribute 'jid' is not allowed here","Atribut 'jid' tidak diijinkan disini"}. {"Attribute 'node' is not allowed here","Atribut 'node' tidak diijinkan disini"}. {"Attribute 'to' of stanza that triggered challenge","Atribut 'to' dari stanza yang memicu respon"}. {"August","Agustus"}. {"Automatic node creation is not enabled","Pembuatan node otomatis tidak diijinkan"}. {"Backup Management","Manajemen Backup"}. {"Backup of ~p","Cadangan dari ~p"}. {"Backup to File at ","Backup ke File di lokasi "}. {"Backup","Backup"}. {"Bad format","Format yang buruk"}. {"Birthday","Hari Lahir"}. {"Both the username and the resource are required","Baik nama pengguna dan sumber daya diperlukan"}. {"Bytestream already activated","Bytestream telah aktif"}. {"Cannot remove active list","Tidak bisa menghapus daftar aktif"}. {"Cannot remove default list","Tidak bisa menghapus daftar standar"}. {"CAPTCHA web page","CAPTCHA laman web"}. {"Challenge ID","ID tantangan"}. {"Change Password","Ubah Kata Sandi"}. {"Change User Password","Ubah User Password"}. {"Changing password is not allowed","Tidak diijinkan mengubah kata sandi"}. {"Changing role/affiliation is not allowed","Tidak diijinkan mengubah peran/afiliasi"}. {"Channel already exists","Channel sudah ada"}. {"Channel does not exist","Channel tidak ada"}. {"Channels","Channel"}. {"Characters not allowed:","Karakter tidak diperbolehkan:"}. {"Chatroom configuration modified","Konfigurasi ruang chat diubah"}. {"Chatroom is created","Ruang chat telah dibuat"}. {"Chatroom is destroyed","Ruang chat dilenyapkan"}. {"Chatroom is started","Ruang chat dimulai"}. {"Chatroom is stopped","Ruang chat dihentikan"}. {"Chatrooms","Ruangan Chat"}. {"Choose a username and password to register with this server","Pilih nama pengguna dan kata sandi untuk mendaftar dengan layanan ini"}. {"Choose storage type of tables","Pilih jenis penyimpanan tabel"}. {"Choose whether to approve this entity's subscription.","Pilih apakah akan menyetujui hubungan pertemanan ini."}. {"City","Kota"}. {"Client acknowledged more stanzas than sent by server","Klien menerima lebih banyak stanza daripada yang dikirim oleh server"}. {"Commands","Perintah"}. {"Conference room does not exist","Ruang Konferensi tidak ada"}. {"Configuration of room ~s","Pengaturan ruangan ~s"}. {"Configuration","Pengaturan"}. {"Connected Resources:","Sumber Daya Terhubung:"}. {"Contact Addresses (normally, room owner or owners)","Alamat Kontak (biasanya, pemilik atau pemilik kamar)"}. {"Country","Negara"}. {"CPU Time:","Waktu CPU:"}. {"Current Discussion Topic","Topik diskusi saat ini"}. {"Database failure","Kegagalan database"}. {"Database Tables at ~p","Tabel Database pada ~p"}. {"Database Tables Configuration at ","Konfigurasi Tabel Database pada "}. {"Database","Database"}. {"December","Desember"}. {"Default users as participants","pengguna pertama kali masuk sebagai participant"}. {"Delete content","Hapus isi"}. {"Delete message of the day on all hosts","Hapus pesan harian pada semua host"}. {"Delete message of the day","Hapus pesan harian"}. {"Delete Selected","Hapus Yang Terpilih"}. {"Delete table","Hapus tabel"}. {"Delete User","Hapus Pengguna"}. {"Deliver event notifications","Memberikan pemberitahuan acara"}. {"Deliver payloads with event notifications","Memberikan muatan dengan pemberitahuan acara"}. {"Description:","Keterangan:"}. {"Disc only copy","Hanya salinan dari disc"}. {"Displayed:","Tampilkan:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Jangan beritahukan kata sandi Anda ke siapapun, bahkan ke administrator layanan XMPP."}. {"Dump Backup to Text File at ","Dump Backup menjadi File Teks di "}. {"Dump to Text File","Dump menjadi File Teks"}. {"Duplicated groups are not allowed by RFC6121","Grup duplikat tidak diperbolehkan oleh RFC6121"}. {"Edit Properties","Ganti Properti"}. {"Either approve or decline the voice request.","Antara terima atau tolak permintaan suara."}. {"ejabberd HTTP Upload service","Layanan HTTP Upload ejabberd"}. {"ejabberd MUC module","Modul MUC ejabberd"}. {"ejabberd Multicast service","Layanan Multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Modul ejabberd Setujui-Pertemanan"}. {"ejabberd SOCKS5 Bytestreams module","modul ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Modul ejabberd vCard"}. {"ejabberd Web Admin","Admin Web ejabberd"}. {"ejabberd","ejabberd"}. {"Elements","Elemen-elemen"}. {"Email Address","Alamat email"}. {"Email","Email"}. {"Enable logging","Aktifkan log"}. {"Enable message archiving","Aktifkan pengarsipan pesan"}. {"Enabling push without 'node' attribute is not supported","Aktivasi push tanpa atribut 'node' tidak didukung"}. {"End User Session","Akhir Sesi Pengguna"}. {"Enter nickname you want to register","Masukkan nama julukan Anda jika ingin mendaftar"}. {"Enter path to backup file","Masukkan path untuk file cadangan"}. {"Enter path to jabberd14 spool dir","Masukkan path ke direktori spool jabberd14"}. {"Enter path to jabberd14 spool file","Masukkan path ke file jabberd14 spool"}. {"Enter path to text file","Masukkan path ke file teks"}. {"Enter the text you see","Masukkan teks yang Anda lihat"}. {"Erlang XMPP Server","Server Erlang XMPP"}. {"Error","Kesalahan"}. {"Exclude Jabber IDs from CAPTCHA challenge","Kecualikan Jabber IDs dari tantangan CAPTCHA"}. {"Export all tables as SQL queries to a file:","Ekspor semua tabel sebagai kueri SQL ke file:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Ekspor data dari semua pengguna pada layanan ke berkas PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Ekspor data pengguna pada sebuah host ke berkas PIEFXIS (XEP-0227):"}. {"External component failure","Kegagalan komponen eksternal"}. {"External component timeout","Komponen eksternal kehabisan waktu"}. {"Failed to activate bytestream","Gagal mengaktifkan bytestream"}. {"Failed to extract JID from your voice request approval","Gagal mendapatkan JID dari permintaan akses suara"}. {"Failed to parse HTTP response","Gagal mengurai respon HTTP"}. {"Failed to process option '~s'","Gagal memproses dengan opsi '~s'"}. {"Family Name","Nama Keluarga (marga)"}. {"FAQ Entry","Entri FAQ"}. {"February","Februari"}. {"File larger than ~w bytes","File lebih besar dari ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Isi kolom untuk mencari pengguna XMPP"}. {"Friday","Jumat"}. {"From ~ts","Dari ~ts"}. {"From","Dari"}. {"Full List of Room Admins","Daftar Lengkap Admin Kamar"}. {"Full List of Room Owners","Daftar Lengkap Pemilik Kamar"}. {"Full Name","Nama Lengkap"}. {"Get Number of Online Users","Dapatkan Jumlah User Yang Online"}. {"Get Number of Registered Users","Dapatkan Jumlah Pengguna Yang Terdaftar"}. {"Get Pending","Lihat yang tertunda"}. {"Get User Last Login Time","Lihat Waktu Login Terakhir Pengguna"}. {"Get User Password","Dapatkan User Password"}. {"Get User Statistics","Dapatkan Statistik Pengguna"}. {"Given Name","Nama"}. {"Grant voice to this person?","Ijinkan akses suara kepadanya?"}. {"Group","Grup"}. {"Groups that will be displayed to the members","Grup yang akan ditampilkan kepada anggota"}. {"Groups","Grup"}. {"has been banned","telah dibanned"}. {"has been kicked because of a system shutdown","telah dikick karena sistem shutdown"}. {"has been kicked because of an affiliation change","telah dikick karena perubahan afiliasi"}. {"has been kicked because the room has been changed to members-only","telah dikick karena ruangan telah diubah menjadi hanya untuk member"}. {"has been kicked","telah dikick"}. {"Host unknown","Host tidak dikenal"}. {"Host","Host"}. {"HTTP File Upload","Unggah Berkas HTTP"}. {"Idle connection","Koneksi menganggur"}. {"If you don't see the CAPTCHA image here, visit the web page.","Jika Anda tidak melihat gambar CAPTCHA disini, silahkan kunjungi halaman web."}. {"Import Directory","Impor Direktori"}. {"Import File","Impor File"}. {"Import user data from jabberd14 spool file:","Impor data pengguna dari sekumpulan berkas jabberd14:"}. {"Import User from File at ","Impor Pengguna dari File pada "}. {"Import users data from a PIEFXIS file (XEP-0227):","impor data-data pengguna dari sebuah PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Импорт пользовательских данных из буферной директории jabberd14:"}. {"Import Users from Dir at ","Impor Pengguna dari Dir di "}. {"Import Users From jabberd14 Spool Files","Impor Pengguna Dari jabberd14 Spool File"}. {"Improper message type","Jenis pesan yang tidak benar"}. {"Incoming s2s Connections:","Koneksi s2s masuk:"}. {"Incorrect CAPTCHA submit","Isian CAPTCHA salah"}. {"Incorrect data form","Formulir data salah"}. {"Incorrect password","Kata sandi salah"}. {"Incorrect value of 'action' attribute","Nilai atribut 'aksi' salah"}. {"Incorrect value of 'action' in data form","Nilai 'tindakan' yang salah dalam formulir data"}. {"Insufficient privilege","Hak tidak mencukupi"}. {"Internal server error","Galat server internal"}. {"Invalid node name","Nama node tidak valid"}. {"IP addresses","Alamat IP"}. {"is now known as","sekarang dikenal sebagai"}. {"It is not allowed to send private messages of type \"groupchat\"","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi jenis \"groupchat \""}. {"It is not allowed to send private messages to the conference","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi ke konferensi"}. {"It is not allowed to send private messages","Hal ini tidak diperbolehkan untuk mengirim pesan pribadi"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","bergabung ke ruangan"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Aktifitas Terakhir"}. {"Last login","Terakhir Login"}. {"Last month","Akhir bulan"}. {"Last year","Akhir tahun"}. {"leaves the room","meninggalkan ruangan"}. {"Low level update script","Perbaruan naskah tingkat rendah"}. {"Make participants list public","Buat daftar participant diketahui oleh public"}. {"Make room CAPTCHA protected","Buat ruangan dilindungi dengan CAPTCHA"}. {"Make room members-only","Buat ruangan hanya untuk member saja"}. {"Make room moderated","Buat ruangan hanya untuk moderator saja"}. {"Make room password protected","Buat ruangan yang dilindungi dengan kata sandi"}. {"Make room persistent","Buat ruangan menjadi permanent"}. {"Make room public searchable","Buat ruangan dapat dicari"}. {"March","Maret"}. {"Max # of items to persist","Max item untuk bertahan"}. {"Max payload size in bytes","Max kapasitas ukuran dalam bytes"}. {"Maximum Number of Occupants","Maksimum Jumlah Penghuni"}. {"May","Mei"}. {"Members:","Anggota:"}. {"Membership is required to enter this room","Hanya Member yang dapat masuk ruangan ini"}. {"Memory","Memori"}. {"Message body","Isi Pesan"}. {"Middle Name","Nama Tengah"}. {"Moderator privileges required","Hak istimewa moderator dibutuhkan"}. {"Modified modules","Modifikasi modul-modul"}. {"Monday","Senin"}. {"Multiple <item/> elements are not allowed by RFC6121","Beberapa elemen <item/> tidak diizinkan oleh RFC6121"}. {"Name","Nama"}. {"Name:","Nama:"}. {"Never","Tidak Pernah"}. {"New Password:","Password Baru:"}. {"Nickname Registration at ","Pendaftaran Julukan pada "}. {"Nickname ~s does not exist in the room","Nama Julukan ~s tidak berada di dalam ruangan"}. {"Nickname","Nama Julukan"}. {"No body provided for announce message","Tidak ada isi pesan yang disediakan untuk mengirimkan pesan"}. {"No Data","Tidak Ada Data"}. {"No <forwarded/> element found","Tidak ada elemen <forwarded/> yang ditemukan"}. {"No limit","Tidak terbatas"}. {"Node ID","ID Node"}. {"Node not found","Node tidak ditemukan"}. {"Nodes","Node-node"}. {"None","Tak satupun"}. {"Not Found","Tidak Ditemukan"}. {"Notify subscribers when items are removed from the node","Beritahu pelanggan ketika item tersebut dikeluarkan dari node"}. {"Notify subscribers when the node configuration changes","Beritahu pelanggan ketika ada perubahan konfigurasi node"}. {"Notify subscribers when the node is deleted","Beritahu pelanggan ketika node dihapus"}. {"November","Nopember"}. {"Number of occupants","Jumlah Penghuni"}. {"Number of online users","Jumlah pengguna online"}. {"Number of registered users","Jumlah pengguna terdaftar"}. {"October","Oktober"}. {"Offline Messages","Pesan Offline"}. {"Offline Messages:","Pesan Offline:"}. {"OK","YA"}. {"Old Password:","Password Lama:"}. {"Online Users:","Pengguna Online:"}. {"Online Users","Pengguna Yang Online"}. {"Online","Online"}. {"Only deliver notifications to available users","Hanya mengirimkan pemberitahuan kepada pengguna yang tersedia"}. {"Only moderators and participants are allowed to change the subject in this room","Hanya moderator dan peserta yang diizinkan untuk mengganti topik pembicaraan di ruangan ini"}. {"Only moderators are allowed to change the subject in this room","Hanya moderator yang diperbolehkan untuk mengubah topik dalam ruangan ini"}. {"Only occupants are allowed to send messages to the conference","Hanya penghuni yang diizinkan untuk mengirim pesan ke konferensi"}. {"Only occupants are allowed to send queries to the conference","Hanya penghuni diizinkan untuk mengirim permintaan ke konferensi"}. {"Only service administrators are allowed to send service messages","Layanan hanya diperuntukan kepada administrator yang diizinkan untuk mengirim layanan pesan"}. {"Organization Name","Nama Organisasi"}. {"Organization Unit","Unit Organisasi"}. {"Outgoing s2s Connections","Koneksi Keluar s2s"}. {"Outgoing s2s Connections:","Koneksi s2s yang keluar:"}. {"Owner privileges required","Hak istimewa owner dibutuhkan"}. {"Packet","Paket"}. {"Participant","Partisipan"}. {"Password Verification:","Verifikasi Kata Sandi:"}. {"Password Verification","Verifikasi Sandi"}. {"Password:","Kata Sandi:"}. {"Password","Sandi"}. {"Path to Dir","Jalur ke Dir"}. {"Path to File","Jalur ke File"}. {"Payload type","Tipe payload"}. {"Pending","Tertunda"}. {"Period: ","Periode: "}. {"Persist items to storage","Pertahankan item ke penyimpanan"}. {"Persistent","Persisten"}. {"Ping query is incorrect","Kueri ping salah"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Harap dicatat bahwa pilihan ini hanya akan membuat cadangan builtin Mnesia database. Jika Anda menggunakan modul ODBC, anda juga perlu untuk membuat cadangan database SQL Anda secara terpisah."}. {"Pong","Pong"}. {"Present real Jabber IDs to","Tampilkan Jabber ID secara lengkap"}. {"Previous session not found","Sesi sebelumnya tidak ditemukan"}. {"Previous session PID has been killed","Sesi PID sebelumnya telah dimatikan"}. {"Previous session PID has exited","Sesi PID sebelumnya telah keluar"}. {"Previous session PID is dead","Sesi PID sebelumnya mati"}. {"Previous session timed out","Sesi sebelumnya habis waktu"}. {"private, ","pribadi, "}. {"Public","Publik"}. {"Publish model","Model penerbitan"}. {"Publish-Subscribe","Setujui-Pertemanan"}. {"PubSub subscriber request","Permintaan pertemanan PubSub"}. {"Purge all items when the relevant publisher goes offline","Bersihkan semua item ketika penerbit yang relevan telah offline"}. {"Queries to the conference members are not allowed in this room","Permintaan untuk para anggota konferensi tidak diperbolehkan di ruangan ini"}. {"Query to another users is forbidden","Kueri ke pengguna lain dilarang"}. {"RAM and disc copy","RAM dan disc salinan"}. {"RAM copy","Salinan RAM"}. {"Really delete message of the day?","Benar-benar ingin menghapus pesan harian?"}. {"Receive notification from all descendent nodes","Terima notifikasi dari semua node turunan"}. {"Receive notification from direct child nodes only","Terima notifikasi dari child node saja"}. {"Receive notification of new items only","Terima notifikasi dari item baru saja"}. {"Receive notification of new nodes only","Terima notifikasi dari node baru saja"}. {"Recipient is not in the conference room","Penerima tidak berada di ruangan konferensi"}. {"Register an XMPP account","Daftarkan sebuah akun XMPP"}. {"Registered Users","Pengguna Terdaftar"}. {"Registered Users:","Pengguna Terdaftar:"}. {"Register","Mendaftar"}. {"Remote copy","Salinan Remote"}. {"Remove All Offline Messages","Hapus Semua Pesan Offline"}. {"Remove User","Hapus Pengguna"}. {"Remove","Menghapus"}. {"Replaced by new connection","Diganti dengan koneksi baru"}. {"Request has timed out","Waktu permintaan telah habis"}. {"Request is ignored","Permintaan diabaikan"}. {"Requested role","Peran yang diminta"}. {"Resources","Sumber daya"}. {"Restart Service","Restart Layanan"}. {"Restart","Jalankan Ulang"}. {"Restore Backup from File at ","Kembalikan Backup dari File pada "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Mengembalikan cadangan yang berpasanagn setelah ejabberd berikutnya dijalankan ulang (memerlukan memori lebih sedikit):"}. {"Restore binary backup immediately:","Segera mengembalikan cadangan yang berpasangan:"}. {"Restore plain text backup immediately:","Segera mengembalikan cadangan teks biasa:"}. {"Restore","Mengembalikan"}. {"Roles that May Send Private Messages","Peran yang Dapat Mengirim Pesan Pribadi"}. {"Room Configuration","Konfigurasi Ruangan"}. {"Room creation is denied by service policy","Pembuatan Ruangan ditolak oleh kebijakan layanan"}. {"Room description","Keterangan ruangan"}. {"Room Occupants","Penghuni Ruangan"}. {"Room terminates","Ruang dihentikan"}. {"Room title","Nama Ruangan"}. {"Roster groups allowed to subscribe","Kelompok kontak yang diizinkan untuk berlangganan"}. {"Roster of ~ts","Daftar ~ts"}. {"Roster size","Ukuran Daftar Kontak"}. {"Roster:","Daftar:"}. {"RPC Call Error","Panggilan Kesalahan RPC"}. {"Running Nodes","Menjalankan Node"}. {"~s invites you to the room ~s","~s mengundang anda masuk kamar ~s"}. {"Saturday","Sabtu"}. {"Script check","Periksa naskah"}. {"Search from the date","Cari dari tanggal"}. {"Search Results for ","Hasil Pencarian untuk "}. {"Search the text","Cari teks"}. {"Search until the date","Cari sampai tanggal"}. {"Search users in ","Pencarian pengguna dalam "}. {"Select All","Pilih Semua"}. {"Send announcement to all online users on all hosts","Kirim pengumuman untuk semua pengguna yang online pada semua host"}. {"Send announcement to all online users","Kirim pengumuman untuk semua pengguna yang online"}. {"Send announcement to all users on all hosts","Kirim pengumuman untuk semua pengguna pada semua host"}. {"Send announcement to all users","Kirim pengumuman untuk semua pengguna"}. {"September","September"}. {"Server:","Layanan:"}. {"Set message of the day and send to online users","Mengatur pesan harian dan mengirimkan ke pengguna yang online"}. {"Set message of the day on all hosts and send to online users","Mengatur pesan harian pada semua host dan kirimkan ke pengguna yang online"}. {"Shared Roster Groups","Berbagi grup kontak"}. {"Show Integral Table","Tampilkan Tabel Terpisah"}. {"Show Ordinary Table","Tampilkan Tabel Normal"}. {"Shut Down Service","Shut Down Layanan"}. {"SOCKS5 Bytestreams","SOCKS5 Bytestreams"}. {"Specify the access model","Tentukan model akses"}. {"Specify the event message type","Tentukan jenis acara pesan"}. {"Specify the publisher model","Tentukan model penerbitan"}. {"Stanza ID","ID Stanza"}. {"Statistics of ~p","statistik dari ~p"}. {"Statistics","Statistik"}. {"Stop","Hentikan"}. {"Stopped Nodes","Menghentikan node"}. {"Storage Type","Jenis Penyimpanan"}. {"Store binary backup:","Penyimpanan cadangan yang berpasangan:"}. {"Store plain text backup:","Simpan cadangan teks biasa:"}. {"Stream management is already enabled","Manajemen stream sudah diaktifkan"}. {"Stream management is not enabled","Manajemen stream tidak diaktifkan"}. {"Subject","Subyek"}. {"Submit","Serahkan"}. {"Submitted","Ulangi masukan"}. {"Subscriber Address","Alamat Pertemanan"}. {"Subscribers may publish","Pelanggan dapat mempublikasikan"}. {"Subscription","Berlangganan"}. {"Subscriptions are not allowed","Langganan tidak diperbolehkan"}. {"Sunday","Minggu"}. {"Text associated with a picture","Teks yang terkait dengan gambar"}. {"Text associated with a sound","Teks yang terkait dengan suara"}. {"Text associated with a video","Teks yang terkait dengan video"}. {"Text associated with speech","Teks yang terkait dengan ucapan"}. {"That nickname is already in use by another occupant","Julukan itu sudah digunakan oleh penghuni lain"}. {"That nickname is registered by another person","Julukan tersebut telah didaftarkan oleh orang lain"}. {"The account was not unregistered","Akun tidak terdaftar"}. {"The CAPTCHA is valid.","Captcha ini benar."}. {"The CAPTCHA verification has failed","Verifikasi CAPTCHA telah gagal"}. {"The captcha you entered is wrong","Isian captcha yang anda masukkan salah"}. {"The collections with which a node is affiliated","Koleksi dengan yang berafiliasi dengan sebuah node"}. {"The JID of the node creator","JID dari pembuat node"}. {"The JIDs of those to contact with questions","JID dari mereka untuk dihubungi dengan pertanyaan"}. {"The JIDs of those with an affiliation of owner","JID dari mereka yang memiliki afiliasi pemilik"}. {"The JIDs of those with an affiliation of publisher","JID dari mereka yang memiliki afiliasi penerbit"}. {"The name of the node","Nama node"}. {"The node is a collection node","Node adalah node koleksi"}. {"The node is a leaf node (default)","Node adalah leaf node (default)"}. {"The NodeID of the relevant node","NodeID dari node yang relevan"}. {"The number of subscribers to the node","Jumlah pendaftar di node"}. {"The number of unread or undelivered messages","Jumlah pesan yang belum dibaca atau tidak terkirim"}. {"The password contains unacceptable characters","Kata sandi mengandung karakter yang tidak dapat diterima"}. {"The password is too weak","Kata sandi terlalu lemah"}. {"the password is","kata sandinya"}. {"The password was not changed","Kata sandi belum berubah"}. {"The passwords are different","Kata sandi berbeda"}. {"The username is not valid","Nama pengguna tidak valid"}. {"There was an error changing the password: ","Ada kesalahan saat merubah kata kunci: "}. {"There was an error creating the account: ","Ada kesalahan saat membuat akun: "}. {"There was an error deleting the account: ","Ada kesalahan saat menghapus akun: "}. {"This room is not anonymous","Ruangan ini tidak dikenal"}. {"This service can not process the address: ~s","Layanan ini tidak dapat memproses alamat: ~s"}. {"Thursday","Kamis"}. {"Time delay","Waktu tunda"}. {"Time","Waktu"}. {"To register, visit ~s","Untuk mendaftar, kunjungi ~s"}. {"To ~ts","Kepada ~ts"}. {"Token TTL","TTL Token"}. {"To","Kepada"}. {"Too many active bytestreams","Terlalu banyak bytestream aktif"}. {"Too many CAPTCHA requests","Terlalu banyak permintaan CAPTCHA"}. {"Too many child elements","Terlalu banyak elemen turunan"}. {"Too many <item/> elements","Terlalu banyak <item/> elemen"}. {"Too many <list/> elements","Terlalu banyak <list/> elemen"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Terlalu banyak (~p) percobaan otentifikasi yang gagal dari alamat IP (~s). Alamat akan di unblok pada ~s UTC"}. {"Too many receiver fields were specified","Terlalu banyak bidang penerima yang ditentukan"}. {"Too many users in this conference","Terlalu banyak pengguna di grup ini"}. {"Total rooms","Total kamar"}. {"Traffic rate limit is exceeded","Batas tingkat lalu lintas terlampaui"}. {"Transactions Aborted:","Transaksi dibatalkan:"}. {"Transactions Committed:","Transaksi yang dilakukan:"}. {"Transactions Logged:","Transaksi yang ditempuh:"}. {"Transactions Restarted:","Transaksi yang dijalankan ulang:"}. {"~ts's Offline Messages Queue","~ts's antrian Pesan Offline"}. {"Tuesday","Selasa"}. {"Unable to generate a CAPTCHA","Tidak dapat menghasilkan CAPTCHA"}. {"Unable to register route on existing local domain","Tidak dapat mendaftarkan rute di domain lokal yang ada"}. {"Unauthorized","Ditolak"}. {"Unexpected action","Aksi yang tidak diharapkan"}. {"Unexpected error condition: ~p","Kondisi kerusakan yang tidak diduga: ~p"}. {"Unregister an XMPP account","Nonaktifkan akun XMPP"}. {"Unregister","Nonaktifkan"}. {"Unselect All","Batalkan semua"}. {"Unsupported <index/> element","Elemen <index/> tidak didukung"}. {"Unsupported version","Versi tidak didukung"}. {"Update message of the day (don't send)","Rubah pesan harian (tidak dikirim)"}. {"Update message of the day on all hosts (don't send)","Rubah pesan harian pada semua host (tidak dikirim)"}. {"Update plan","Rencana Perubahan"}. {"Update ~p","Memperbaharui ~p"}. {"Update script","Perbarui naskah"}. {"Update","Memperbarui"}. {"Uptime:","Sampai saat:"}. {"User already exists","Pengguna sudah ada"}. {"User (jid)","Pengguna (jid)"}. {"User JID","Pengguna JID"}. {"User Management","Manajemen Pengguna"}. {"User removed","Pengguna dipindahkan"}. {"User session not found","Sesi pengguna tidak ditemukan"}. {"User session terminated","Sesi pengguna dihentikan"}. {"User ~ts","Pengguna ~ts"}. {"Username:","Nama Pengguna:"}. {"User","Pengguna"}. {"Users are not allowed to register accounts so quickly","Pengguna tidak diperkenankan untuk mendaftar akun begitu cepat"}. {"Users Last Activity","Aktifitas terakhir para pengguna"}. {"Users","Pengguna"}. {"Validate","Mengesahkan"}. {"Value 'get' of 'type' attribute is not allowed","Nilai 'get' dari 'type' atribut tidak diperbolehkan"}. {"Value of '~s' should be boolean","Nilai '~ s' harus boolean"}. {"Value of '~s' should be datetime string","Nilai '~s' harus string datetime"}. {"Value of '~s' should be integer","Nilai '~ s' harus integer"}. {"Value 'set' of 'type' attribute is not allowed","Nilai 'set' dari 'type' atribut tidak diperbolehkan"}. {"vCard User Search","vCard Pencarian Pengguna"}. {"View Queue","Lihat antrian"}. {"View Roster","Lihat daftar kontak"}. {"Virtual Hosts","Host Virtual"}. {"Visitors are not allowed to change their nicknames in this room","Tamu tidak diperbolehkan untuk mengubah nama panggilan di ruangan ini"}. {"Visitors are not allowed to send messages to all occupants","Tamu tidak diperbolehkan untuk mengirim pesan ke semua penghuni"}. {"Visitor","Tamu"}. {"Voice request","Permintaan suara"}. {"Voice requests are disabled in this conference","Permintaan suara dinonaktifkan dalam konferensi ini"}. {"Wednesday","Rabu"}. {"When a new subscription is processed and whenever a subscriber comes online","Saat langganan baru diproses dan tiap kali pelanggan online"}. {"When a new subscription is processed","Saat langganan baru diproses"}. {"When to send the last published item","Ketika untuk mengirim item terakhir yang dipublikasikan"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Apakah entitas ingin menerima isi pesan XMPP selain format payload"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Apakah entitas ingin menerima ringkasan(agregasi) pemberitahuan atau semua pemberitahuan satu per satu"}. {"Whether an entity wants to receive or disable notifications","Apakah entitas ingin menerima atau menonaktifkan pemberitahuan"}. {"Whether owners or publisher should receive replies to items","Apakah pemilik atau penerbit harus menerima balasan dari item"}. {"Whether the node is a leaf (default) or a collection","Apakah node adalah leaf (default) atau koleksi"}. {"Whether to allow subscriptions","Apakah diperbolehkan untuk berlangganan"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Apakah akan menjadikan semua langganan sementara, berdasarkan keberadaan pelanggan"}. {"Whether to notify owners about new subscribers and unsubscribes","Apakah akan memberi tahu pemilik tentang pelanggan baru dan berhenti berlangganan"}. {"Who may associate leaf nodes with a collection","Siapa yang dapat mengaitkan leaf node dengan koleksi"}. {"Wrong parameters in the web formulary","Parameter yang salah di formula web"}. {"Wrong xmlns","xmlns salah"}. {"XMPP Account Registration","Pendaftaran Akun XMPP"}. {"XMPP Domains","Domain XMPP"}. {"XMPP Show Value of Away","XMPP menunjukkan status Away"}. {"XMPP Show Value of Chat","XMPP menunjukkan status Chat"}. {"XMPP Show Value of DND (Do Not Disturb)","XMPP menunjukkan status DND (Do Not Disturb)"}. {"XMPP Show Value of XA (Extended Away)","XMPP menunjukkan status XA (Extended Away)"}. {"XMPP URI of Associated Publish-Subscribe Node","XMPP URI dari node Associated Publish-Subscribe"}. {"You are being removed from the room because of a system shutdown","Anda sedang dikeluarkan dari kamar karena sistem shutdown"}. {"You are not joined to the channel","Anda tidak bergabung ke channel"}. {"You can later change your password using an XMPP client.","Anda dapat mengubah kata sandi menggunakan aplikasi XMPP."}. {"You have been banned from this room","Anda telah diblokir dari ruangan ini"}. {"You have joined too many conferences","Anda telah mengikuti terlalu banyak grup"}. {"You must fill in field \"Nickname\" in the form","Anda harus mengisi kolom \"Panggilan\" dalam formulir"}. {"You need a client that supports x:data and CAPTCHA to register","Anda memerlukan klien yang mendukung x:data dan CAPTCHA untuk mendaftar"}. {"You need a client that supports x:data to register the nickname","Anda memerlukan klien yang mendukung x:data untuk mendaftar julukan"}. {"You need an x:data capable client to search","Anda memerlukan x:data klien untuk melakukan pencarian"}. {"Your active privacy list has denied the routing of this stanza.","Daftar privasi aktif Anda telah menolak routing stanza ini."}. {"Your contact offline message queue is full. The message has been discarded.","Kontak offline Anda pada antrian pesan sudah penuh. Pesan telah dibuang."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Pesan Anda untuk ~s sedang diblokir. Untuk membuka blokir tersebut, kunjungi ~s"}. {"Your XMPP account was successfully registered.","Akun XMPP Anda berhasil didaftarkan."}. {"Your XMPP account was successfully unregistered.","Akun XMPP Anda berhasil dihapus."}. {"You're not allowed to create nodes","Anda tidak diizinkan membuat node"}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/tr.msg���������������������������������������������������������������������0000644�0002322�0002322�00000050566�14154362354�016734� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," konuyu değiştirdi: "}. {"A friendly name for the node","Düğüm için dostane bir isim"}. {"A password is required to enter this room","Bu odaya girmek için parola gerekiyor"}. {"Access denied by service policy","Servis politikası gereği erişim engellendi"}. {"Action on user","Kullanıcıya uygulanacak eylem"}. {"Add Jabber ID","Jabber ID'si Ekle"}. {"Add New","Yeni Ekle"}. {"Add User","Kullanıcı Ekle"}. {"Administration of ","Yönetim : "}. {"Administration","Yönetim"}. {"Administrator privileges required","Yönetim yetkileri gerekli"}. {"All activity","Tüm aktivite"}. {"All Users","Tüm Kullanıcılar"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Bu Jabber ID bu pubsub düğümüne üye olmasına izin verilsin mi?"}. {"Allow users to change the subject","Kullanıcıların konu değiştirmesine izin ver"}. {"Allow users to query other users","Kullanıcıların diğer kullanıcıları sorgulamalarına izin ver"}. {"Allow users to send invites","Kullanıcıların davetiye göndermelerine izin ver"}. {"Allow users to send private messages","Kullanıcıların özel mesaj göndermelerine izin ver"}. {"Allow visitors to change nickname","Ziyaretçilerin takma isim değiştirmelerine izin ver"}. {"Allow visitors to send private messages to","Ziyaretçilerin özel mesaj göndermelerine izin ver"}. {"Allow visitors to send status text in presence updates","Ziyaretçilerin varlık (presence) güncellemelerinde durum metni göndermelerine izin ver"}. {"Allow visitors to send voice requests","Ziyaretçilerin ses isteğine göndermelerine izin ver"}. {"Announcements","Duyurular"}. {"April","Nisan"}. {"August","Ağustos"}. {"Backup Management","Yedek Yönetimi"}. {"Backup to File at ","Dosyaya Yedekle : "}. {"Backup","Yedekle"}. {"Bad format","Kötü biçem"}. {"Birthday","Doğumgünü"}. {"CAPTCHA web page","CAPTCHA web sayfası"}. {"Change Password","Parola Değiştir"}. {"Change User Password","Kullanıcı Parolasını Değiştir"}. {"Characters not allowed:","İzin verilmeyen karakterler:"}. {"Chatroom configuration modified","Sohbet odası ayarı değiştirildi"}. {"Chatroom is created","Sohbet odası oluşturuldu"}. {"Chatroom is destroyed","Sohbet odası kaldırıldı"}. {"Chatroom is started","Sohbet odası başlatıldı"}. {"Chatroom is stopped","Sohbet odası durduruldu"}. {"Chatrooms","Sohbet Odaları"}. {"Choose a username and password to register with this server","Bu sunucuya kayıt olmak için bir kullanıcı ismi ve parola seçiniz"}. {"Choose storage type of tables","Tabloların veri depolama tipini seçiniz"}. {"Choose whether to approve this entity's subscription.","Bu varlığın üyeliğini onaylayıp onaylamamayı seçiniz."}. {"City","İl"}. {"Commands","Komutlar"}. {"Conference room does not exist","Konferans odası bulunamadı"}. {"Configuration of room ~s","~s odasının ayarları"}. {"Configuration","Ayarlar"}. {"Connected Resources:","Bağlı Kaynaklar:"}. {"Country","Ülke"}. {"CPU Time:","İşlemci Zamanı:"}. {"Database Tables Configuration at ","Veritabanı Tablo Ayarları : "}. {"Database","Veritabanı"}. {"December","Aralık"}. {"Default users as participants","Kullanıcılar öntanımlı olarak katılımcı olsun"}. {"Delete message of the day on all hosts","Tüm sunuculardaki günün mesajını sil"}. {"Delete message of the day","Günün mesajını sil"}. {"Delete Selected","Seçilenleri Sil"}. {"Delete User","Kullanıcıyı Sil"}. {"Deliver event notifications","Olay uyarıları gönderilsin"}. {"Deliver payloads with event notifications","Yükleri (payload) olay uyarıları ile beraber gönder"}. {"Description:","Tanım:"}. {"Disc only copy","Sadece disk kopyala"}. {"Dump Backup to Text File at ","Metin Dosyasına Döküm Alarak Yedekle : "}. {"Dump to Text File","Metin Dosyasına Döküm Al"}. {"Edit Properties","Özellikleri Düzenle"}. {"Either approve or decline the voice request.","Ses isteğini kabul edin ya da reddedin"}. {"ejabberd MUC module","ejabberd MUC modülü"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modülü"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modülü"}. {"ejabberd vCard module","ejabberd vCard modülü"}. {"ejabberd Web Admin","ejabberd Web Yöneticisi"}. {"Elements","Elementler"}. {"Email","E-posta"}. {"Enable logging","Kayıt tutma özelliğini aç"}. {"End User Session","Kullanıcı Oturumunu Kapat"}. {"Enter nickname you want to register","Kaydettirmek istediğiniz takma ismi giriniz"}. {"Enter path to backup file","Yedek dosyasının yolunu giriniz"}. {"Enter path to jabberd14 spool dir","jabberd14 spool dosyası için yol giriniz"}. {"Enter path to jabberd14 spool file","jabberd14 spool dosyası için yol giriniz"}. {"Enter path to text file","Metin dosyasının yolunu giriniz"}. {"Enter the text you see","Gördüğünüz metni giriniz"}. {"Error","Hata"}. {"Exclude Jabber IDs from CAPTCHA challenge","CAPTCHA doğrulamasını şu Jabber ID'ler için yapma"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Sunucudaki tüm kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa aktar:"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Bir sunucudaki kullanıcıların verisini PIEFXIS dosyalarına (XEP-0227) dışa aktar:"}. {"Failed to extract JID from your voice request approval","Ses isteği onayınızdan JID bilginize ulaşılamadı"}. {"Family Name","Soyisim"}. {"February","Şubat"}. {"Friday","Cuma"}. {"From","Kimden"}. {"Full Name","Tam İsim"}. {"Get Number of Online Users","Bağlı Kullanıcı Sayısını Al"}. {"Get Number of Registered Users","Kayıtlı Kullanıcı Sayısını Al"}. {"Get User Last Login Time","Kullanıcı Son Giriş Zamanınlarını Al"}. {"Get User Password","Kullanıcı Parolasını Al"}. {"Get User Statistics","Kullanıcı İstatistiklerini Al"}. {"Grant voice to this person?","Bu kişiye ses verelim mi?"}. {"Groups","Gruplar"}. {"has been banned","odaya girmesi yasaklandı"}. {"has been kicked because of a system shutdown","sistem kapandığından dolayı atıldı"}. {"has been kicked because of an affiliation change","ilişki değişikliğinden dolayı atıldı"}. {"has been kicked because the room has been changed to members-only","oda üyelere-özel hale getirildiğinden dolayı atıldı"}. {"has been kicked","odadan atıldı"}. {"Host","Sunucu"}. {"If you don't see the CAPTCHA image here, visit the web page.","Eğer burada CAPTCHA resmini göremiyorsanız, web sayfasını ziyaret edin."}. {"Import Directory","Dizini İçe Aktar"}. {"Import File","Dosyayı İçe Aktar"}. {"Import user data from jabberd14 spool file:","Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçe Aktar:"}. {"Import User from File at ","Dosyadan Kullanıcıları İçe Aktar : "}. {"Import users data from a PIEFXIS file (XEP-0227):","Kullanıcıları bir PIEFXIS dosyasından (XEP-0227) içe aktar:"}. {"Import users data from jabberd14 spool directory:","Jabberd 1.4 Spool Dizininden Kullanıcıları İçe Aktar:"}. {"Import Users from Dir at ","Dizinden Kullanıcıları İçe Aktar : "}. {"Import Users From jabberd14 Spool Files","Jabberd 1.4 Spool Dosyalarından Kullanıcıları İçeri Aktar"}. {"Improper message type","Uygunsuz mesaj tipi"}. {"Incorrect password","Yanlış parola"}. {"IP addresses","IP adresleri"}. {"is now known as","isim değiştirdi :"}. {"It is not allowed to send private messages of type \"groupchat\"","\"groupchat\" tipinde özel mesajlar gönderilmesine izin verilmiyor"}. {"It is not allowed to send private messages to the conference","Konferansa özel mesajlar gönderilmesine izin verilmiyor"}. {"It is not allowed to send private messages","Özel mesaj gönderilmesine izin verilmiyor"}. {"Jabber ID","Jabber ID"}. {"January","Ocak"}. {"joins the room","odaya katıldı"}. {"July","Temmuz"}. {"June","Haziran"}. {"Last Activity","Son Aktivite"}. {"Last login","Son giriş"}. {"Last month","Geçen ay"}. {"Last year","Geçen yıl"}. {"leaves the room","odadan ayrıldı"}. {"Low level update script","Düşük seviye güncelleme betiği"}. {"Make participants list public","Katılımcı listesini herkese açık hale getir"}. {"Make room CAPTCHA protected","Odayı insan doğrulaması (captcha) korumalı hale getir"}. {"Make room members-only","Odayı sadece üyelere açık hale getir"}. {"Make room moderated","Odayı moderasyonlu hale getir"}. {"Make room password protected","Odayı parola korumalı hale getir"}. {"Make room persistent","Odayı kalıcı hale getir"}. {"Make room public searchable","Odayı herkes tarafından aranabilir hale getir"}. {"March","Mart"}. {"Max # of items to persist","Kalıcı hale getirilecek en fazla öğe sayısı"}. {"Max payload size in bytes","En fazla yük (payload) boyutu (bayt olarak)"}. {"Maximum Number of Occupants","Odada En Fazla Bulunabilecek Kişi Sayısı"}. {"May","Mayıs"}. {"Membership is required to enter this room","Bu odaya girmek için üyelik gerekiyor"}. {"Members:","Üyeler:"}. {"Memory","Bellek"}. {"Message body","Mesajın gövdesi"}. {"Middle Name","Ortanca İsim"}. {"Minimum interval between voice requests (in seconds)","Ses istekleri arasında olabilecek en az aralık (saniye olarak)"}. {"Moderator privileges required","Moderatör yetkileri gerekli"}. {"Modified modules","Değişen modüller"}. {"Monday","Pazartesi"}. {"Name","İsim"}. {"Name:","İsim:"}. {"Never","Asla"}. {"New Password:","Yeni Parola:"}. {"Nickname Registration at ","Takma İsim Kaydı : "}. {"Nickname ~s does not exist in the room","~s takma ismi odada yok"}. {"Nickname","Takma isim"}. {"No body provided for announce message","Duyuru mesajının gövdesi yok"}. {"No Data","Veri Yok"}. {"No limit","Sınırsız"}. {"Node ID","Düğüm ID"}. {"Node not found","Düğüm bulunamadı"}. {"Nodes","Düğümler"}. {"None","Hiçbiri"}. {"Not Found","Bulunamadı"}. {"Notify subscribers when items are removed from the node","Düğümden öğeler kaldırıldığında üyeleri uyar"}. {"Notify subscribers when the node configuration changes","Düğüm ayarları değiştiğinde üyeleri uyar"}. {"Notify subscribers when the node is deleted","Bir düğüm silindiğinde üyeleri uyar"}. {"November","Kasım"}. {"Number of occupants","Oda sakini sayısı"}. {"Number of online users","Bağlı kullanıcı sayısı"}. {"Number of registered users","Kayıtlı kullanıcı sayısı"}. {"October","Ekim"}. {"Offline Messages","Çevirim-dışı Mesajlar"}. {"Offline Messages:","Çevirim-dışı Mesajlar:"}. {"OK","Tamam"}. {"Old Password:","Eski Parola:"}. {"Online Users","Bağlı Kullanıcılar"}. {"Online Users:","Bağlı Kullanıcılar:"}. {"Online","Bağlı"}. {"Only deliver notifications to available users","Uyarıları sadece durumu uygun kullanıcılara ulaştır"}. {"Only moderators and participants are allowed to change the subject in this room","Sadece moderatörlerin ve katılımcıların bu odanın konusunu değiştirmesine izin veriliyor"}. {"Only moderators are allowed to change the subject in this room","Sadece moderatörlerin bu odanın konusunu değiştirmesine izin veriliyor"}. {"Only moderators can approve voice requests","Yalnız moderatörler ses isteklerini onaylayabilir"}. {"Only occupants are allowed to send messages to the conference","Sadece oda sakinlerinin konferansa mesaj göndermesine izin veriliyor"}. {"Only occupants are allowed to send queries to the conference","Sadece oda sakinlerinin konferansa sorgu göndermesine izin veriliyor"}. {"Only service administrators are allowed to send service messages","Sadece servis yöneticileri servis mesajı gönderebilirler"}. {"Organization Name","Kurum İsmi"}. {"Organization Unit","Kurumun İlgili Birimi"}. {"Outgoing s2s Connections","Giden s2s Bağlantıları"}. {"Outgoing s2s Connections:","Giden s2s Bağlantıları:"}. {"Owner privileges required","Sahip yetkileri gerekli"}. {"Packet","Paket"}. {"Password Verification","Parola Doğrulaması"}. {"Password Verification:","Parola Doğrulaması:"}. {"Password","Parola"}. {"Password:","Parola:"}. {"Path to Dir","Dizinin Yolu"}. {"Path to File","Dosyanın Yolu"}. {"Pending","Sıra Bekleyen"}. {"Period: ","Periyot:"}. {"Persist items to storage","Öğeleri depoda kalıcı hale getir"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Bu seçeneklerin sadece gömülü Mnesia veritabanını yedekleyeceğine dikkat edin. Eğer ODBC modülünü kullanıyorsanız, SQL veritabanınızı da ayrıca yedeklemeniz gerekiyor."}. {"Please, wait for a while before sending new voice request","Lütfen yeni bir ses isteği göndermeden önce biraz bekleyin"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Gerçek Jabber ID'lerini göster :"}. {"private, ","özel"}. {"Publish-Subscribe","Yayınla-Üye Ol"}. {"PubSub subscriber request","PubSub üye isteği"}. {"Purge all items when the relevant publisher goes offline","İlgili yayıncı çevirimdışı olunca tüm onunla ilgili olanları sil"}. {"Queries to the conference members are not allowed in this room","Bu odada konferans üyelerine sorgu yapılmasına izin verilmiyor"}. {"RAM and disc copy","RAM ve disk kopyala"}. {"RAM copy","RAM kopyala"}. {"Really delete message of the day?","Günün mesajını silmek istediğinize emin misiniz?"}. {"Recipient is not in the conference room","Alıcı konferans odasında değil"}. {"Registered Users","Kayıtlı Kullanıcılar"}. {"Registered Users:","Kayıtlı Kullanıcılar:"}. {"Register","Kayıt Ol"}. {"Remote copy","Uzak kopyala"}. {"Remove All Offline Messages","Tüm Çevirim-dışı Mesajları Kaldır"}. {"Remove User","Kullanıcıyı Kaldır"}. {"Remove","Kaldır"}. {"Replaced by new connection","Eski bağlantı yenisi ile değiştirildi"}. {"Resources","Kaynaklar"}. {"Restart Service","Servisi Tekrar Başlat"}. {"Restart","Tekrar Başlat"}. {"Restore Backup from File at ","Dosyadaki Yedekten Geri Al : "}. {"Restore binary backup after next ejabberd restart (requires less memory):","ejabberd'nin bir sonraki tekrar başlatılışında ikili yedekten geri al (daha az bellek gerektirir)"}. {"Restore binary backup immediately:","Hemen ikili yedekten geri al:"}. {"Restore plain text backup immediately:","Hemen düz metin yedekten geri al"}. {"Restore","Yedekten Geri Al"}. {"Room Configuration","Oda Ayarları"}. {"Room creation is denied by service policy","Odanın oluşturulması servis politikası gereği reddedildi"}. {"Room description","Oda tanımı"}. {"Room Occupants","Oda Sakini Sayısı"}. {"Room title","Oda başlığı"}. {"Roster groups allowed to subscribe","Üye olunmasına izin verilen kontak listesi grupları"}. {"Roster size","İsim listesi boyutu"}. {"RPC Call Error","RPC Çağrı Hatası"}. {"Running Nodes","Çalışan Düğümler"}. {"Saturday","Cumartesi"}. {"Script check","Betik kontrolü"}. {"Search Results for ","Arama sonuçları : "}. {"Search users in ","Kullanıcılarda arama yap : "}. {"Send announcement to all online users on all hosts","Duyuruyu tüm sunuculardaki tüm bağlı kullanıcılara yolla"}. {"Send announcement to all online users","Duyuruyu tüm bağlı kullanıcılara yolla"}. {"Send announcement to all users on all hosts","Tüm sunuculardaki tüm kullanıcılara duyuru yolla"}. {"Send announcement to all users","Duyuruyu tüm kullanıcılara yolla"}. {"September","Eylül"}. {"Server:","Sunucu:"}. {"Set message of the day and send to online users","Günün mesajını belirle"}. {"Set message of the day on all hosts and send to online users","Tüm sunucularda günün mesajını belirle ve bağlı tüm kullanıcılara gönder"}. {"Shared Roster Groups","Paylaşımlı Kontak Listesi Grupları"}. {"Show Integral Table","Önemli Tabloyu Göster"}. {"Show Ordinary Table","Sıradan Tabloyu Göster"}. {"Shut Down Service","Servisi Kapat"}. {"Specify the access model","Erişim modelini belirtiniz"}. {"Specify the event message type","Olay mesaj tipini belirtiniz"}. {"Specify the publisher model","Yayıncı modelini belirtiniz"}. {"Statistics of ~p","~p istatistikleri"}. {"Statistics","İstatistikler"}. {"Stop","Durdur"}. {"Stopped Nodes","Durdurulmuş Düğümler"}. {"Storage Type","Depolama Tipi"}. {"Store binary backup:","İkili yedeği sakla:"}. {"Store plain text backup:","Düz metin yedeği sakla:"}. {"Subject","Konu"}. {"Submit","Gönder"}. {"Submitted","Gönderilenler"}. {"Subscriber Address","Üye Olanın Adresi"}. {"Subscription","Üyelik"}. {"Sunday","Pazar"}. {"That nickname is already in use by another occupant","Takma isim odanın başka bir sakini tarafından halihazırda kullanımda"}. {"That nickname is registered by another person","O takma isim başka biri tarafından kaydettirilmiş"}. {"The CAPTCHA is valid.","İnsan doğrulaması (captcha) geçerli."}. {"The CAPTCHA verification has failed","CAPTCHA doğrulaması başarısız oldu"}. {"The collections with which a node is affiliated","Bir düğüm ile bağlantılı koleksiyonlar"}. {"The password is too weak","Parola çok zayıf"}. {"the password is","parola :"}. {"There was an error creating the account: ","Hesap oluşturulurken bir hata oluştu:"}. {"There was an error deleting the account: ","Hesabın silinmesi sırasında bir hata oluştu:"}. {"This room is not anonymous","Bu oda anonim değil"}. {"Thursday","Perşembe"}. {"Time delay","Zaman gecikmesi"}. {"Time","Zaman"}. {"To","Kime"}. {"Too many CAPTCHA requests","Çok fazla CAPTCHA isteği"}. {"Traffic rate limit is exceeded","Trafik oran sınırı aşıldı"}. {"Transactions Aborted:","İptal Edilen Hareketler (Transactions):"}. {"Transactions Committed:","Tamamlanan Hareketler (Transactions Committed):"}. {"Transactions Logged:","Kaydı Tutulan Hareketler (Transactions):"}. {"Transactions Restarted:","Tekrar Başlatılan Hareketler (Transactions):"}. {"Tuesday","Salı"}. {"Unable to generate a CAPTCHA","İnsan doğrulaması (CAPTCHA) oluşturulamadı"}. {"Unauthorized","Yetkisiz"}. {"Unregister","Kaydı Sil"}. {"Update message of the day (don't send)","Günün mesajını güncelle (gönderme)"}. {"Update message of the day on all hosts (don't send)","Tüm sunuculardaki günün mesajını güncelle (gönderme)"}. {"Update plan","Planı güncelle"}. {"Update script","Betiği Güncelle"}. {"Update","GÜncelle"}. {"Uptime:","Hizmet Süresi:"}. {"User JID","Kullanıcı JID"}. {"User Management","Kullanıcı Yönetimi"}. {"User","Kullanıcı"}. {"Username:","Kullanıcı adı:"}. {"Users are not allowed to register accounts so quickly","Kullanıcıların bu kadar hızlı hesap açmalarına izin verilmiyor"}. {"Users Last Activity","Kullanıcıların Son Aktiviteleri"}. {"Users","Kullanıcılar"}. {"Validate","Geçerli"}. {"vCard User Search","vCard Kullanıcı Araması"}. {"Virtual Hosts","Sanal Sunucuları"}. {"Visitors are not allowed to change their nicknames in this room","Bu odada ziyaretçilerin takma isimlerini değiştirmesine izin verilmiyor"}. {"Visitors are not allowed to send messages to all occupants","Ziyaretçilerin odadaki tüm sakinlere mesaj göndermesine izin verilmiyor"}. {"Voice requests are disabled in this conference","Bu konferansta ses istekleri etkisizleştirilmiş durumda."}. {"Voice request","Ses isteği"}. {"Wednesday","Çarşamba"}. {"When to send the last published item","Son yayınlanan öğe ne zaman gönderilsin"}. {"Whether to allow subscriptions","Üyeliklere izin verilsin mi"}. {"You have been banned from this room","Bu odaya girmeniz yasaklandı"}. {"You must fill in field \"Nickname\" in the form","Formda \"Takma isim\" alanını doldurmanız gerekiyor"}. {"You need a client that supports x:data and CAPTCHA to register","Takma isminizi kaydettirmek için x:data ve CAPTCHA destekleyen bir istemciye gereksinimiz var"}. {"You need a client that supports x:data to register the nickname","Takma isminizi kaydettirmek için x:data destekleyen bir istemciye gereksinimiz var"}. {"You need an x:data capable client to search","Arama yapabilmek için x:data becerisine sahip bir istemciye gereksinimiz var"}. {"Your active privacy list has denied the routing of this stanza.","Etkin mahremiyet listeniz bu bölümün yönlendirilmesini engelledi."}. {"Your contact offline message queue is full. The message has been discarded.","Çevirim-dışı mesaj kuyruğunuz dolu. Mesajını dikkate alınmadı."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","~s kullanıcısına mesajlarınız engelleniyor. Durumu düzeltmek için ~s adresini ziyaret ediniz."}. ������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/pt.msg���������������������������������������������������������������������0000644�0002322�0002322�00000140023�14154362354�016716� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Adicione * no final do campo para combinar com a substring)"}. {" has set the subject to: "," colocou o tópico: "}. {"# participants","# participantes"}. {"A description of the node","Uma descrição do nó"}. {"A friendly name for the node","Um nome familiar para o nó"}. {"A password is required to enter this room","Se necessita palavra-passe para entrar nesta sala"}. {"A Web Page","Uma página da web"}. {"Accept","Aceito"}. {"Access denied by service policy","Acesso negado pela política de serviço"}. {"Access model of authorize","Modelo de acesso da autorização"}. {"Access model of open","Modelo para acesso aberto"}. {"Access model of presence","Modelo para acesso presença"}. {"Access model of roster","Modelo para acesso lista"}. {"Access model of whitelist","Modelo de acesso da lista branca"}. {"Access model","Modelo de acesso"}. {"Account doesn't exist","A conta não existe"}. {"Action on user","Acção no utilizador"}. {"Add Jabber ID","Adicionar ID jabber"}. {"Add New","Adicionar novo"}. {"Add User","Adicionar utilizador"}. {"Administration of ","Administração de "}. {"Administration","Administração"}. {"Administrator privileges required","São necessários privilégios de administrador"}. {"All activity","Todas atividades"}. {"All Users","Todos os utilizadores"}. {"Allow subscription","Permitir a assinatura"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autorizar este Jabber ID para a inscrição neste tópico pubsub?"}. {"Allow this person to register with the room?","Permita que esta pessoa se registe na sala?"}. {"Allow users to change the subject","Permitir a utilizadores modificar o assunto"}. {"Allow users to query other users","Permitir a utilizadores pesquisar informações sobre os demais"}. {"Allow users to send invites","Permitir a utilizadores envio de convites"}. {"Allow users to send private messages","Permitir a utilizadores enviarem mensagens privadas"}. {"Allow visitors to change nickname","Permitir mudança de apelido aos visitantes"}. {"Allow visitors to send private messages to","Permitir visitantes enviar mensagem privada para"}. {"Allow visitors to send status text in presence updates","Permitir atualizações de estado aos visitantes"}. {"Allow visitors to send voice requests","Permitir aos visitantes o envio de requisições de voz"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Um grupo LDAP associado que define a adesão à sala; este deve ser um Nome Distinto LDAP de acordo com uma definição específica da implementação ou da implantação específica de um grupo."}. {"Announcements","Anúncios"}. {"Answer associated with a picture","Resposta associada com uma foto"}. {"Answer associated with a video","Resposta associada com um vídeo"}. {"Answer associated with speech","Resposta associada com a fala"}. {"Answer to a question","Resposta para uma pergunta"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualquer pessoa do(s) grupo(s) informado(s) podem se inscrever e recuperar itens"}. {"Anyone may associate leaf nodes with the collection","Qualquer pessoa pode associar nós das páginas à coleção"}. {"Anyone may publish","Qualquer pessoa pode publicar"}. {"Anyone may subscribe and retrieve items","Qualquer pessoa pode se inscrever e recuperar os itens"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualquer pessoa com uma assinatura presente dos dois ou de ambos pode se inscrever e recuperar os itens"}. {"Anyone with Voice","Qualquer pessoa com voz"}. {"Anyone","Qualquer pessoa"}. {"April","Abril"}. {"Attribute 'channel' is required for this request","O atributo 'canal' é necessário para esta solicitação"}. {"Attribute 'id' is mandatory for MIX messages","O atributo 'id' é obrigatório para mensagens MIX"}. {"Attribute 'jid' is not allowed here","O atributo 'jid' não é permitido aqui"}. {"Attribute 'node' is not allowed here","O Atributo 'nó' não é permitido aqui"}. {"Attribute 'to' of stanza that triggered challenge","O atributo 'para' da estrofe que desencadeou o desafio"}. {"August","Agosto"}. {"Automatic node creation is not enabled","Criação automatizada de nós está desativada"}. {"Backup Management","Gestão de cópias de segurança"}. {"Backup of ~p","Backup de ~p"}. {"Backup to File at ","Guardar cópia de segurança para ficheiro em "}. {"Backup","Guardar cópia de segurança"}. {"Bad format","Formato incorreto"}. {"Birthday","Data de nascimento"}. {"Both the username and the resource are required","Nome de utilizador e recurso são necessários"}. {"Bytestream already activated","Bytestream já foi ativado"}. {"Cannot remove active list","Não é possível remover uma lista ativa"}. {"Cannot remove default list","Não é possível remover uma lista padrão"}. {"CAPTCHA web page","CAPTCHA web page"}. {"Challenge ID","ID do desafio"}. {"Change Password","Mudar palavra-chave"}. {"Change User Password","Alterar Palavra-passe do Utilizador"}. {"Changing password is not allowed","Não é permitida a alteração da palavra-passe"}. {"Changing role/affiliation is not allowed","Não é permitida a alteração da função/afiliação"}. {"Channel already exists","O canal já existe"}. {"Channel does not exist","O canal não existe"}. {"Channels","Canais"}. {"Characters not allowed:","Caracteres não aceitos:"}. {"Chatroom configuration modified","Configuração da sala de bate-papo modificada"}. {"Chatroom is created","A sala de chat está criada"}. {"Chatroom is destroyed","A sala de chat está destruída"}. {"Chatroom is started","A sala de chat está iniciada"}. {"Chatroom is stopped","A sala de chat está parada"}. {"Chatrooms","Salas de Chat"}. {"Choose a username and password to register with this server","Escolha um nome de utilizador e palavra-chave para se registar neste servidor"}. {"Choose storage type of tables","Seleccione o tipo de armazenagem das tabelas"}. {"Choose whether to approve this entity's subscription.","Aprovar esta assinatura."}. {"City","Cidade"}. {"Client acknowledged more stanzas than sent by server","O cliente reconheceu mais estrofes do que as enviadas pelo servidor"}. {"Commands","Comandos"}. {"Conference room does not exist","A sala não existe"}. {"Configuration of room ~s","Configuração para ~s"}. {"Configuration","Configuração"}. {"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Endereços de contato (normalmente, o proprietário ou os proprietários da sala)"}. {"Country","País"}. {"CPU Time:","Tempo da CPU:"}. {"Current Discussion Topic","Assunto em discussão"}. {"Database failure","Falha no banco de dados"}. {"Database Tables at ~p","Tabelas da Base de dados em ~p"}. {"Database Tables Configuration at ","Configuração de Tabelas de Base de dados em "}. {"Database","Base de dados"}. {"December","Dezembro"}. {"Default users as participants","Utilizadores padrões como participantes"}. {"Delete content","Apagar o conteúdo"}. {"Delete message of the day on all hosts","Apagar a mensagem do dia em todos os hosts"}. {"Delete message of the day","Apagar mensagem do dia"}. {"Delete Selected","Eliminar os seleccionados"}. {"Delete table","Apagar a tabela"}. {"Delete User","Deletar Utilizador"}. {"Deliver event notifications","Entregar as notificações de evento"}. {"Deliver payloads with event notifications","Enviar payloads junto com as notificações de eventos"}. {"Description:","Descrição:"}. {"Disc only copy","Cópia apenas em disco"}. {"'Displayed groups' not added (they do not exist!): ","Os 'Grupos exibidos' não foi adicionado (eles não existem!): "}. {"Displayed:","Exibido:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Não revele a sua palavra-passe a ninguém, nem mesmo para o administrador deste servidor XMPP."}. {"Dump Backup to Text File at ","Exporta cópia de segurança para ficheiro de texto em "}. {"Dump to Text File","Exportar para ficheiro de texto"}. {"Duplicated groups are not allowed by RFC6121","Os grupos duplicados não são permitidos pela RFC6121"}. {"Dynamically specify a replyto of the item publisher","Definir de forma dinâmica uma resposta da editora do item"}. {"Edit Properties","Editar propriedades"}. {"Either approve or decline the voice request.","Deve aprovar/desaprovar a requisição de voz."}. {"ejabberd HTTP Upload service","serviço HTTP de upload ejabberd"}. {"ejabberd MUC module","Módulo MUC de ejabberd"}. {"ejabberd Multicast service","Serviço multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo para Publicar Tópicos do ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Modulo ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Módulo vCard de ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. {"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. {"Enable logging","Permitir criação de logs"}. {"Enable message archiving","Ativar arquivamento de mensagens"}. {"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. {"End User Session","Terminar Sessão do Utilizador"}. {"Enter nickname you want to register","Introduza a alcunha que quer registar"}. {"Enter path to backup file","Introduza o caminho do ficheiro de cópia de segurança"}. {"Enter path to jabberd14 spool dir","Introduza o caminho para o directório de spools do jabberd14"}. {"Enter path to jabberd14 spool file","Introduza o caminho para o ficheiro de spool do jabberd14"}. {"Enter path to text file","Introduza caminho para o ficheiro de texto"}. {"Enter the text you see","Insira o texto que vê"}. {"Erlang XMPP Server","Servidor XMPP Erlang"}. {"Error","Erro"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluir IDs Jabber de serem submetidos ao CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exportar todas as tabelas como SQL para um ficheiro:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar todos os dados de todos os utilizadores no servidor, para ficheiros de formato PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dados dos utilizadores num host, para ficheiros de PIEFXIS (XEP-0227):"}. {"External component failure","Falha de componente externo"}. {"External component timeout","Tempo esgotado à espera de componente externo"}. {"Failed to activate bytestream","Falha ao ativar bytestream"}. {"Failed to extract JID from your voice request approval","Não foi possível extrair o JID (Jabber ID) da requisição de voz"}. {"Failed to map delegated namespace to external component","Falha ao mapear namespace delegado ao componente externo"}. {"Failed to parse HTTP response","Falha ao analisar resposta HTTP"}. {"Failed to process option '~s'","Falha ao processar opção '~s'"}. {"Family Name","Apelido"}. {"FAQ Entry","Registo das perguntas frequentes"}. {"February","Fevereiro"}. {"File larger than ~w bytes","Ficheiro é maior que ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Preencha campos para procurar por quaisquer utilizadores XMPP"}. {"Friday","Sexta"}. {"From ~ts","De ~s"}. {"From","De"}. {"Full List of Room Admins","Lista completa dos administradores das salas"}. {"Full List of Room Owners","Lista completa dos proprietários das salas"}. {"Full Name","Nome completo"}. {"Get Number of Online Users","Obter quantidade de utilizadores online"}. {"Get Number of Registered Users","Obter quantidade de utilizadores registados"}. {"Get Pending","Obter os pendentes"}. {"Get User Last Login Time","Obter a data do último login"}. {"Get User Password","Obter palavra-passe do utilizador"}. {"Get User Statistics","Obter estatísticas do utilizador"}. {"Given Name","Sobrenome"}. {"Grant voice to this person?","Dar voz a esta pessoa?"}. {"Group","Grupo"}. {"Groups that will be displayed to the members","Os grupos que serão exibidos para os membros"}. {"Groups","Grupos"}. {"has been banned","foi banido"}. {"has been kicked because of a system shutdown","foi desconectado porque o sistema foi desligado"}. {"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. {"has been kicked","foi removido"}. {"Host unknown","Máquina desconhecida"}. {"Host","Máquina"}. {"HTTP File Upload","Upload de ficheiros por HTTP"}. {"Idle connection","Conexão inativa"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se não conseguir ver o CAPTCHA aqui, visite a web page."}. {"Import Directory","Importar directório"}. {"Import File","Importar ficheiro"}. {"Import user data from jabberd14 spool file:","Importar dados dos utilizadores de uma fila jabberd14:"}. {"Import User from File at ","Importar utilizador a partir do ficheiro em "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importe os utilizadores de um ficheiro PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar dados dos utilizadores de um diretório-fila jabberd14:"}. {"Import Users from Dir at ","Importar utilizadores a partir do directório em "}. {"Import Users From jabberd14 Spool Files","Importar utilizadores de ficheiros de jabberd14 (spool files)"}. {"Improper domain part of 'from' attribute","Atributo 'from' contém domínio incorreto"}. {"Improper message type","Tipo de mensagem incorrecto"}. {"Incoming s2s Connections:","Conexões s2s de Entrada:"}. {"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. {"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Palavra-chave incorrecta"}. {"Incorrect value of 'action' attribute","Valor incorreto do atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorreto de 'action' no formulário de dados"}. {"Incorrect value of 'path' in data form","Valor incorreto de 'path' no formulário de dados"}. {"Insufficient privilege","Privilégio insuficiente"}. {"Internal server error","Erro interno do servidor"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from' inválido na mensagem reenviada"}. {"Invalid node name","Nome do nó inválido"}. {"Invalid 'previd' value","Valor 'previd' inválido"}. {"Invitations are not allowed in this conference","Os convites não são permitidos nesta conferência"}. {"IP addresses","Endereços IP"}. {"is now known as","é agora conhecido como"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Não é permitido o envio de mensagens de erro para a sala. O membro (~s) enviou uma mensagem de erro (~s) e foi expulso da sala"}. {"It is not allowed to send private messages of type \"groupchat\"","Não é permitido enviar mensagens privadas do tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir o envio de mensagens privadas para a sala"}. {"It is not allowed to send private messages","Não é permitido enviar mensagens privadas"}. {"Jabber ID","ID Jabber"}. {"January","Janeiro"}. {"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. {"JID normalization failed","A normalização JID falhou"}. {"joins the room","Entrar na sala"}. {"July","Julho"}. {"June","Junho"}. {"Just created","Acabou de ser criado"}. {"Label:","Rótulo:"}. {"Last Activity","Última actividade"}. {"Last login","Último login"}. {"Last message","Última mensagem"}. {"Last month","Último mês"}. {"Last year","Último ano"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. {"leaves the room","Sair da sala"}. {"List of rooms","Lista de salas"}. {"Logging","Registando no log"}. {"Low level update script","Script de atualização low level"}. {"Make participants list public","Tornar pública a lista de participantes"}. {"Make room CAPTCHA protected","Tornar protegida a palavra-passe da sala"}. {"Make room members-only","Tornar sala apenas para membros"}. {"Make room moderated","Tornar a sala moderada"}. {"Make room password protected","Tornar sala protegida à palavra-passe"}. {"Make room persistent","Tornar sala persistente"}. {"Make room public searchable","Tornar sala pública possível de ser encontrada"}. {"Malformed username","Nome de utilizador inválido"}. {"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. {"March","Março"}. {"Max # of items to persist","Máximo # de elementos que persistem"}. {"Max payload size in bytes","Máximo tamanho do payload em bytes"}. {"Maximum file size","Tamanho máximo do ficheiro"}. {"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. {"Maximum number of items to persist","Quantidade máxima dos itens para manter"}. {"Maximum Number of Occupants","Quantidate máxima de participantes"}. {"May","Maio"}. {"Members not added (inexistent vhost!): ","Membros que não foram adicionados (o vhost não existe!): "}. {"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. {"Members:","Membros:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memorize a sua palavra-passe ou anote-a num papel guardado num local seguro. No XMPP, não há uma maneira automatizada de recuperar a sua palavra-passe caso a esqueça."}. {"Memory","Memória"}. {"Mere Availability in XMPP (No Show Value)","Mera disponibilidade no XMPP (Sem valor para ser exibido)"}. {"Message body","Corpo da mensagem"}. {"Message not found in forwarded payload","Mensagem não encontrada em conteúdo encaminhado"}. {"Messages from strangers are rejected","As mensagens vindas de estranhos são rejeitadas"}. {"Messages of type headline","Mensagens do tipo do título"}. {"Messages of type normal","Mensagens do tipo normal"}. {"Middle Name","Segundo nome"}. {"Minimum interval between voice requests (in seconds)","O intervalo mínimo entre requisições de voz (em segundos)"}. {"Moderator privileges required","São necessários privilégios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Somente moderadores"}. {"Modified modules","Módulos atualizados"}. {"Module failed to handle the query","Módulo falhou ao processar a consulta"}. {"Monday","Segunda"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","Vários elementos <item/> não são permitidos pela RFC6121"}. {"Multi-User Chat","Chat multi-utilizador"}. {"Name in the rosters where this group will be displayed","O nome nas listas onde este grupo será exibido"}. {"Name","Nome"}. {"Name:","Nome:"}. {"Natural Language for Room Discussions","Idioma nativo para as discussões na sala"}. {"Natural-Language Room Name","Nome da sala no idioma nativo"}. {"Neither 'jid' nor 'nick' attribute found","Nem o atributo 'jid' nem 'nick' foram encontrados"}. {"Neither 'role' nor 'affiliation' attribute found","Nem o atributo 'role' nem 'affiliation' foram encontrados"}. {"Never","Nunca"}. {"New Password:","Nova Palavra-passe:"}. {"Nickname can't be empty","O apelido não pode ser vazio"}. {"Nickname Registration at ","Registo da alcunha em "}. {"Nickname ~s does not exist in the room","A alcunha ~s não existe na sala"}. {"Nickname","Alcunha"}. {"No address elements found","Nenhum elemento endereço foi encontrado"}. {"No addresses element found","Nenhum elemento endereços foi encontrado"}. {"No 'affiliation' attribute found","Atributo 'affiliation' não foi encontrado"}. {"No available resource found","Nenhum recurso disponível foi encontrado"}. {"No body provided for announce message","Nenhum corpo de texto fornecido para anunciar mensagem"}. {"No child elements found","Nenhum elemento filho foi encontrado"}. {"No data form found","Nenhum formulário de dados foi encontrado"}. {"No Data","Nenhum dado"}. {"No features available","Nenhuma funcionalidade disponível"}. {"No <forwarded/> element found","Nenhum elemento <forwarded/> foi encontrado"}. {"No hook has processed this command","Nenhum hook processou este comando"}. {"No info about last activity found","Não foi encontrada informação sobre última atividade"}. {"No 'item' element found","O elemento 'item' não foi encontrado"}. {"No items found in this query","Nenhum item encontrado nesta consulta"}. {"No limit","Ilimitado"}. {"No module is handling this query","Nenhum módulo está processando esta consulta"}. {"No node specified","Nenhum nó especificado"}. {"No 'password' found in data form","'password' não foi encontrado em formulário de dados"}. {"No 'password' found in this query","Nenhuma 'palavra-passe' foi encontrado nesta consulta"}. {"No 'path' found in data form","'path' não foi encontrado em formulário de dados"}. {"No pending subscriptions found","Não foram encontradas subscrições"}. {"No privacy list with this name found","Nenhuma lista de privacidade encontrada com este nome"}. {"No private data found in this query","Nenhum dado privado encontrado nesta consulta"}. {"No running node found","Nenhum nó em execução foi encontrado"}. {"No services available","Não há serviços disponíveis"}. {"No statistics found for this item","Não foram encontradas estatísticas para este item"}. {"No 'to' attribute found in the invitation","Atributo 'to' não foi encontrado no convite"}. {"Nobody","Ninguém"}. {"Node already exists","Nó já existe"}. {"Node ID","ID do Tópico"}. {"Node index not found","O índice do nó não foi encontrado"}. {"Node not found","Nodo não encontrado"}. {"Node ~p","Nó ~p"}. {"Node","Nó"}. {"Nodeprep has failed","Processo de identificação de nó falhou (nodeprep)"}. {"Nodes","Nodos"}. {"None","Nenhum"}. {"Not allowed","Não é permitido"}. {"Not Found","Não encontrado"}. {"Not subscribed","Não subscrito"}. {"Notify subscribers when items are removed from the node","Notificar assinantes quando itens forem eliminados do nó"}. {"Notify subscribers when the node configuration changes","Notificar assinantes a configuração do nó mudar"}. {"Notify subscribers when the node is deleted","Notificar assinantes quando o nó for eliminado se elimine"}. {"November","Novembro"}. {"Number of answers required","Quantidade de respostas necessárias"}. {"Number of occupants","Quantidade de participantes"}. {"Number of Offline Messages","Quantidade das mensagens offline"}. {"Number of online users","Quantidade de utilizadores online"}. {"Number of registered users","Quantidade de utilizadores registados"}. {"Number of seconds after which to automatically purge items","Quantidade de segundos para excluir os itens automaticamente"}. {"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. {"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. {"October","Outubro"}. {"Offline Messages","Mensagens offline"}. {"Offline Messages:","Mensagens offline:"}. {"OK","OK"}. {"Old Password:","Palavra-passe Antiga:"}. {"Online Users","Utilizadores ligados"}. {"Online Users:","Utilizadores online:"}. {"Online","Ligado"}. {"Only admins can see this","Apenas administradores podem ver isso"}. {"Only collection node owners may associate leaf nodes with the collection","Apenas um grupo dos proprietários dos nós podem associar as páginas na coleção"}. {"Only deliver notifications to available users","Somente enviar notificações aos utilizadores disponíveis"}. {"Only <enable/> or <disable/> tags are allowed","Apenas tags <enable/> ou <disable/> são permitidas"}. {"Only <list/> element is allowed in this query","Apenas elemento <list/> é permitido nesta consulta"}. {"Only members may query archives of this room","Somente os membros podem procurar nos arquivos desta sala"}. {"Only moderators and participants are allowed to change the subject in this room","Somente os moderadores e os participamentes podem alterar o assunto desta sala"}. {"Only moderators are allowed to change the subject in this room","Somente os moderadores podem alterar o assunto desta sala"}. {"Only moderators can approve voice requests","Somente moderadores podem aprovar requisições de voz"}. {"Only occupants are allowed to send messages to the conference","Só os ocupantes podem enviar mensagens para a sala"}. {"Only occupants are allowed to send queries to the conference","Só os ocupantes podem enviar consultas para a sala"}. {"Only publishers may publish","Apenas os editores podem publicar"}. {"Only service administrators are allowed to send service messages","Só os administradores do serviço têm permissão para enviar mensagens de serviço"}. {"Only those on a whitelist may associate leaf nodes with the collection","Apenas aqueles presentes numa lista branca podem associar páginas na coleção"}. {"Only those on a whitelist may subscribe and retrieve items","Apenas aqueles presentes numa lista branca podem se inscrever e recuperar os itens"}. {"Organization Name","Nome da organização"}. {"Organization Unit","Unidade da organização"}. {"Outgoing s2s Connections","Conexões s2s de Saída"}. {"Outgoing s2s Connections:","Saída das conexões s2s:"}. {"Owner privileges required","São necessários privilégios de dono"}. {"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. {"Packet","Pacote"}. {"Participant","Participante"}. {"Password Verification:","Verificação da Palavra-passe:"}. {"Password Verification","Verificação de Palavra-passe"}. {"Password","Palavra-chave"}. {"Password:","Palavra-chave:"}. {"Path to Dir","Caminho para o directório"}. {"Path to File","Caminho do ficheiro"}. {"Payload type","Tipo da carga útil"}. {"Pending","Pendente"}. {"Period: ","Período: "}. {"Persist items to storage","Persistir elementos ao armazenar"}. {"Persistent","Persistente"}. {"Ping query is incorrect","A consulta ping está incorreta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Observe que tais opções farão backup apenas da base de dados Mnesia. Caso esteja a utilizar o modulo ODBC, precisará fazer backup da sua base de dados SQL separadamente."}. {"Please, wait for a while before sending new voice request","Por favor, espere antes de enviar uma nova requisição de voz"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Possuir o atributo 'ask' não é permitido pela RFC6121"}. {"Present real Jabber IDs to","Tornar o Jabber ID real visível por"}. {"Previous session not found","A sessão anterior não foi encontrada"}. {"Previous session PID has been killed","O PID da sessão anterior foi excluído"}. {"Previous session PID has exited","O PID da sessão anterior foi encerrado"}. {"Previous session PID is dead","O PID da sessão anterior está morto"}. {"Previous session timed out","A sessão anterior expirou"}. {"private, ","privado, "}. {"Public","Público"}. {"Publish model","Publicar o modelo"}. {"Publish-Subscribe","Publicação de Tópico"}. {"PubSub subscriber request","PubSub requisição de assinante"}. {"Purge all items when the relevant publisher goes offline","Descartar todos os itens quando o publicante principal estiver offline"}. {"Push record not found","O registo push não foi encontrado"}. {"Queries to the conference members are not allowed in this room","Nesta sala não são permitidas consultas aos seus membros"}. {"Query to another users is forbidden","Consultar a outro utilizador é proibido"}. {"RAM and disc copy","Cópia em RAM e em disco"}. {"RAM copy","Cópia em RAM"}. {"Really delete message of the day?","Deletar realmente a mensagem do dia?"}. {"Receive notification from all descendent nodes","Receba a notificação de todos os nós descendentes"}. {"Receive notification from direct child nodes only","Receba apenas as notificações dos nós relacionados"}. {"Receive notification of new items only","Receba apenas as notificações dos itens novos"}. {"Receive notification of new nodes only","Receba apenas as notificações dos nós novos"}. {"Recipient is not in the conference room","O destinatário não está na sala"}. {"Register an XMPP account","Registe uma conta XMPP"}. {"Registered Users","Utilizadores registados"}. {"Registered Users:","Utilizadores registados:"}. {"Register","Registar"}. {"Remote copy","Cópia remota"}. {"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Eliminar utilizador"}. {"Remove","Remover"}. {"Replaced by new connection","Substituído por nova conexão"}. {"Request has timed out","O pedido expirou"}. {"Request is ignored","O pedido foi ignorado"}. {"Requested role","Função solicitada"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar Serviço"}. {"Restart","Reiniciar"}. {"Restore Backup from File at ","Restaura cópia de segurança a partir do ficheiro em "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar backup binário após reinicialização do ejabberd (requer menos memória):"}. {"Restore binary backup immediately:","Restaurar imediatamente o backup binário:"}. {"Restore plain text backup immediately:","Restaurar backup formato texto imediatamente:"}. {"Restore","Restaurar"}. {"Roles and Affiliations that May Retrieve Member List","As funções e as afiliações que podem recuperar a lista dos membros"}. {"Roles for which Presence is Broadcasted","Para quem a presença será notificada"}. {"Roles that May Send Private Messages","Atribuições que talvez possam enviar mensagens privadas"}. {"Room Configuration","Configuração de salas"}. {"Room creation is denied by service policy","Sala não pode ser criada devido à política do serviço"}. {"Room description","Descrição da Sala"}. {"Room Occupants","Quantidade de participantes"}. {"Room terminates","Terminação da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Listar grupos autorizados"}. {"Roster of ~ts","Lista de ~ts"}. {"Roster size","Tamanho da Lista"}. {"Roster:","Lista:"}. {"RPC Call Error","Erro de chamada RPC"}. {"Running Nodes","Nodos a correr"}. {"~s invites you to the room ~s","~s convidaram-o à sala ~s"}. {"Saturday","Sábado"}. {"Script check","Verificação de Script"}. {"Search from the date","Pesquise a partir da data"}. {"Search Results for ","Resultados de pesquisa para "}. {"Search the text","Pesquise o texto"}. {"Search until the date","Pesquise até a data"}. {"Search users in ","Procurar utilizadores em "}. {"Select All","Selecione tudo"}. {"Send announcement to all online users on all hosts","Enviar anúncio a todos utilizadores online em todas as máquinas"}. {"Send announcement to all online users","Enviar anúncio a todos os utilizadorns online"}. {"Send announcement to all users on all hosts","Enviar aviso para todos os utilizadores em todos os hosts"}. {"Send announcement to all users","Enviar anúncio a todos os utilizadores"}. {"September","Setembro"}. {"Server:","Servidor:"}. {"Service list retrieval timed out","A recuperação da lista dos serviços expirou"}. {"Session state copying timed out","A cópia do estado da sessão expirou"}. {"Set message of the day and send to online users","Definir mensagem do dia e enviar a todos utilizadores online"}. {"Set message of the day on all hosts and send to online users","Definir mensagem do dia em todos os hosts e enviar para os utilizadores online"}. {"Shared Roster Groups","Grupos Shared Roster"}. {"Show Integral Table","Mostrar Tabela Integral"}. {"Show Ordinary Table","Mostrar Tabela Ordinária"}. {"Shut Down Service","Parar Serviço"}. {"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clientes XMPP podem armazenar a sua palavra-passe no seu computador, só faça isso no seu computador particular por questões de segurança."}. {"Specify the access model","Especificar os modelos de acesso"}. {"Specify the event message type","Especificar o tipo de mensagem para o evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. {"Stanza ID","ID da estrofe"}. {"Statically specify a replyto of the node owner(s)","Defina uma resposta fixa do(s) proprietário(s) do nó"}. {"Statistics of ~p","Estatísticas de ~p"}. {"Statistics","Estatísticas"}. {"Stop","Parar"}. {"Stopped Nodes","Nodos parados"}. {"Storage Type","Tipo de armazenagem"}. {"Store binary backup:","Armazenar backup binário:"}. {"Store plain text backup:","Armazenar backup em texto:"}. {"Stream management is already enabled","A gestão do fluxo já está ativada"}. {"Stream management is not enabled","A gestão do fluxo não está ativada"}. {"Subject","Assunto"}. {"Submit","Enviar"}. {"Submitted","Submetido"}. {"Subscriber Address","Endereço dos Assinantes"}. {"Subscribers may publish","Os assinantes podem publicar"}. {"Subscription requests must be approved and only subscribers may retrieve items","Os pedidos de assinatura devem ser aprovados e apenas os assinantes podem recuperar os itens"}. {"Subscriptions are not allowed","Subscrições não estão permitidas"}. {"Subscription","Subscrição"}. {"Sunday","Domingo"}. {"Text associated with a picture","Um texto associado a uma imagem"}. {"Text associated with a sound","Um texto associado a um som"}. {"Text associated with a video","Um texto associado a um vídeo"}. {"Text associated with speech","Um texto associado à fala"}. {"That nickname is already in use by another occupant","O apelido (nick) já está a ser utilizado"}. {"That nickname is registered by another person","O apelido já está registado por outra pessoa"}. {"The account already exists","A conta já existe"}. {"The account was not unregistered","A conta não estava não registada"}. {"The body text of the last received message","O corpo do texto da última mensagem que foi recebida"}. {"The CAPTCHA is valid.","O CAPTCHA é inválido."}. {"The CAPTCHA verification has failed","A verificação do CAPTCHA falhou"}. {"The captcha you entered is wrong","O captcha que digitou está errado"}. {"The child nodes (leaf or collection) associated with a collection","Os nós relacionados (página ou coleção) associados com uma coleção"}. {"The collections with which a node is affiliated","As coleções com as quais o nó está relacionado"}. {"The DateTime at which a leased subscription will end or has ended","A data e a hora que uma assinatura alugada terminará ou terá terminado"}. {"The datetime when the node was created","A data em que o nó foi criado"}. {"The default language of the node","O idioma padrão do nó"}. {"The feature requested is not supported by the conference","A funcionalidade solicitada não é suportada pela sala de conferência"}. {"The JID of the node creator","O JID do criador do nó"}. {"The JIDs of those to contact with questions","Os JIDs daqueles para entrar em contato com perguntas"}. {"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. {"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. {"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós numa coleção"}. {"The maximum number of child nodes that can be associated with a collection","A quantidade máxima dos nós relacionados que podem ser associados com uma coleção"}. {"The minimum number of milliseconds between sending any two notification digests","A quantidade mínima de milissegundos entre o envio do resumo das duas notificações"}. {"The name of the node","O nome do nó"}. {"The node is a collection node","O nó é um nó da coleção"}. {"The node is a leaf node (default)","O nó é uma página do nó (padrão)"}. {"The NodeID of the relevant node","O NodeID do nó relevante"}. {"The number of pending incoming presence subscription requests","A quantidade pendente dos pedidos da presença da assinatura"}. {"The number of subscribers to the node","A quantidade dos assinantes para o nó"}. {"The number of unread or undelivered messages","A quantidade das mensagens que não foram lidas ou não foram entregues"}. {"The password contains unacceptable characters","A palavra-passe contém caracteres proibidos"}. {"The password is too weak","Palavra-passe considerada muito fraca"}. {"the password is","a palavra-passe é"}. {"The password of your XMPP account was successfully changed.","A palavra-passe da sua conta XMPP foi alterada com sucesso."}. {"The password was not changed","A palavra-passe não foi alterada"}. {"The passwords are different","As palavras-passe não batem"}. {"The presence states for which an entity wants to receive notifications","As condições da presença para os quais uma entidade queira receber as notificações"}. {"The query is only allowed from local users","Esta consulta só é permitida a partir de utilizadores locais"}. {"The query must not contain <item/> elements","A consulta não pode conter elementos <item/>"}. {"The room subject can be modified by participants","O tema da sala pode ser alterada pelos próprios participantes"}. {"The sender of the last received message","O remetente da última mensagem que foi recebida"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","A instância DEVE conter apenas um elemento <active/>, um elemento <default/>, ou um elemento <list/>"}. {"The subscription identifier associated with the subscription request","O identificador da assinatura associado à solicitação da assinatura"}. {"The type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. {"The username is not valid","O nome do utilizador não é válido"}. {"There was an error changing the password: ","Houve um erro ao alterar a palavra-passe: "}. {"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. {"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","O tamanho da caixa não importa: macbeth é o mesmo que MacBeth e Macbeth."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta pagina permite a criação de novas contas XMPP neste servidor. O seu JID (Identificador Jabber) será da seguinte maneira: utilizador@servidor. Por favor, leia cuidadosamente as instruções para preencher todos os campos corretamente."}. {"This page allows to unregister an XMPP account in this XMPP server.","Esta página permite a exclusão de uma conta XMPP neste servidor."}. {"This room is not anonymous","Essa sala não é anônima"}. {"This service can not process the address: ~s","Este serviço não pode processar o endereço: ~s"}. {"Thursday","Quinta"}. {"Time delay","Intervalo (Tempo)"}. {"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. {"Time","Data"}. {"To register, visit ~s","Para registar, visite ~s"}. {"To ~ts","Para ~s"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Quantidade excessiva de bytestreams ativos"}. {"Too many CAPTCHA requests","Quantidade excessiva de requisições para o CAPTCHA"}. {"Too many child elements","Quantidade excessiva de elementos filho"}. {"Too many <item/> elements","Quantidade excessiva de elementos <item/>"}. {"Too many <list/> elements","Quantidade excessiva de elementos <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Tentativas excessivas (~p) com falha de autenticação (~s). O endereço será desbloqueado às ~s UTC"}. {"Too many receiver fields were specified","Foram definidos receptores demais nos campos"}. {"Too many unacked stanzas","Quantidade excessiva de instâncias sem confirmação"}. {"Too many users in this conference","Há uma quantidade excessiva de utilizadores nesta conferência"}. {"To","Para"}. {"Total rooms","Salas no total"}. {"Traffic rate limit is exceeded","Limite de banda excedido"}. {"Transactions Aborted:","Transações abortadas:"}. {"Transactions Committed:","Transações salvas:"}. {"Transactions Logged:","Transações de log:"}. {"Transactions Restarted:","Transações reiniciadas:"}. {"~ts's Offline Messages Queue","~s's Fila de Mensagens Offline"}. {"Tuesday","Terça"}. {"Unable to generate a CAPTCHA","Impossível gerar um CAPTCHA"}. {"Unable to register route on existing local domain","Não foi possível registar rota no domínio local existente"}. {"Unauthorized","Não Autorizado"}. {"Unexpected action","Ação inesperada"}. {"Unexpected error condition: ~p","Condição de erro inesperada: ~p"}. {"Unregister an XMPP account","Excluir uma conta XMPP"}. {"Unregister","Deletar registo"}. {"Unselect All","Desmarcar todos"}. {"Unsupported <index/> element","Elemento <index/> não suportado"}. {"Unsupported version","Versão sem suporte"}. {"Update message of the day (don't send)","Atualizar mensagem do dia (não enviar)"}. {"Update message of the day on all hosts (don't send)","Atualizar a mensagem do dia em todos os host (não enviar)"}. {"Update ~p","Atualizar ~p"}. {"Update plan","Plano de atualização"}. {"Update script","Script de atualização"}. {"Update","Actualizar"}. {"Uptime:","Tempo de atividade:"}. {"URL for Archived Discussion Logs","A URL para o arquivamento dos registos da discussão"}. {"User already exists","Utilizador já existe"}. {"User (jid)","Utilizador (jid)"}. {"User JID","Utilizador JID"}. {"User Management","Gestão de utilizadores"}. {"User removed","O utilizador foi removido"}. {"User session not found","A sessão do utilizador não foi encontrada"}. {"User session terminated","Sessão de utilizador terminada"}. {"User ~ts","Utilizador ~s"}. {"Username:","Utilizador:"}. {"Users are not allowed to register accounts so quickly","Utilizadores não estão autorizados a registar contas imediatamente"}. {"Users Last Activity","Últimas atividades dos utilizadores"}. {"Users","Utilizadores"}. {"User","Utilizador"}. {"Validate","Validar"}. {"Value 'get' of 'type' attribute is not allowed","Valor 'get' não permitido para atributo 'type'"}. {"Value of '~s' should be boolean","Value de '~s' deveria ser um booleano"}. {"Value of '~s' should be datetime string","Valor de '~s' deveria ser data e hora"}. {"Value of '~s' should be integer","Valor de '~s' deveria ser um inteiro"}. {"Value 'set' of 'type' attribute is not allowed","Valor 'set' não permitido para atributo 'type'"}. {"vCard User Search","Busca de Utilizador vCard"}. {"View Queue","Exibir a fila"}. {"View Roster","Ver a lista"}. {"Virtual Hosts","Hosts virtuais"}. {"Visitors are not allowed to change their nicknames in this room","Nesta sala, os visitantes não podem mudar os apelidos deles"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes não podem enviar mensagens para todos os ocupantes"}. {"Visitor","Visitante"}. {"Voice request","Requisição de voz"}. {"Voice requests are disabled in this conference","Requisições de voz estão desativadas nesta sala de conferência"}. {"Wednesday","Quarta"}. {"When a new subscription is processed and whenever a subscriber comes online","Quando uma nova assinatura é processada e sempre que um assinante fica online"}. {"When a new subscription is processed","Quando uma nova assinatura é processada"}. {"When to send the last published item","Quando enviar o último tópico publicado"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Caso uma entidade queira receber o corpo de uma mensagem XMPP além do formato de carga útil"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Caso uma entidade queira receber os resumos (as agregações) das notificações ou todas as notificações individualmente"}. {"Whether an entity wants to receive or disable notifications","Caso uma entidade queira receber ou desativar as notificações"}. {"Whether owners or publisher should receive replies to items","Caso os proprietários ou a editora devam receber as respostas nos itens"}. {"Whether the node is a leaf (default) or a collection","Caso o nó seja uma folha (padrão) ou uma coleção"}. {"Whether to allow subscriptions","Permitir subscrições"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Caso todas as assinaturas devam ser temporárias, com base na presença do assinante"}. {"Whether to notify owners about new subscribers and unsubscribes","Caso deva notificar os proprietários sobre os novos assinantes e aqueles que cancelaram a assinatura"}. {"Who may associate leaf nodes with a collection","Quem pode associar as folhas dos nós numa coleção"}. {"Wrong parameters in the web formulary","O formulário web está com os parâmetros errados"}. {"Wrong xmlns","Xmlns errado"}. {"XMPP Account Registration","Registo da Conta XMPP"}. {"XMPP Domains","Domínios XMPP"}. {"XMPP Show Value of Away","XMPP Exiba o valor da ausência"}. {"XMPP Show Value of Chat","XMPP Exiba o valor do chat"}. {"XMPP Show Value of DND (Do Not Disturb)","XMPP Exiba o valor do DND (Não Perturbe)"}. {"XMPP Show Value of XA (Extended Away)","XMPP Exiba o valor do XA (Ausência Estendida)"}. {"XMPP URI of Associated Publish-Subscribe Node","XMPP URI da publicação do nó associado da assinatura"}. {"You are being removed from the room because of a system shutdown","Está a ser removido da sala devido ao desligamento do sistema"}. {"You are not joined to the channel","Não está inscrito no canal"}. {"You can later change your password using an XMPP client.","Pode alterar a sua palavra-passe mais tarde usando um cliente XMPP."}. {"You have been banned from this room","Foi banido desta sala"}. {"You have joined too many conferences","Entrou em demais salas de conferência"}. {"You must fill in field \"Nickname\" in the form","Deve completar o campo \"Apelido\" no formulário"}. {"You need a client that supports x:data and CAPTCHA to register","Precisa de um cliente com suporte de x:data para poder registar o apelido"}. {"You need a client that supports x:data to register the nickname","Precisa de um cliente com suporte a x:data para registar o seu apelido"}. {"You need an x:data capable client to search","É necessário um cliente com suporte de x:data para poder procurar"}. {"Your active privacy list has denied the routing of this stanza.","A sua lista de privacidade ativa negou o roteamento desta instância."}. {"Your contact offline message queue is full. The message has been discarded.","A fila de contatos offline esta cheia. A sua mensagem foi descartada."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","As suas mensagens para ~s estão bloqueadas. Para desbloqueá-las, visite: ~s"}. {"Your XMPP account was successfully registered.","A sua conta XMPP foi registada com sucesso."}. {"Your XMPP account was successfully unregistered.","A sua conta XMPP foi excluída com sucesso."}. {"You're not allowed to create nodes","Não tem autorização para criar nós"}. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/ru.msg���������������������������������������������������������������������0000644�0002322�0002322�00000126654�14154362354�016737� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Добавьте * в конец поля для поиска подстроки)"}. {" has set the subject to: "," установил(а) тему: "}. {"A friendly name for the node","Легко запоминаемое имя для узла"}. {"A password is required to enter this room","Чтобы войти в эту конференцию, нужен пароль"}. {"Accept","Принять"}. {"Access denied by service policy","Доступ запрещён политикой службы"}. {"Account doesn't exist","Учётная запись не существует"}. {"Action on user","Действие над пользователем"}. {"Add Jabber ID","Добавить Jabber ID"}. {"Add New","Добавить"}. {"Add User","Добавить пользователя"}. {"Administration of ","Администрирование "}. {"Administration","Администрирование"}. {"Administrator privileges required","Требуются права администратора"}. {"All activity","Вся статистика"}. {"All Users","Все пользователи"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Разрешить этому Jabber ID подписаться на данный узел?"}. {"Allow users to change the subject","Разрешить пользователям изменять тему"}. {"Allow users to query other users","Разрешить iq-запросы к пользователям"}. {"Allow users to send invites","Разрешить пользователям посылать приглашения"}. {"Allow users to send private messages","Разрешить приватные сообщения"}. {"Allow visitors to change nickname","Разрешить посетителям изменять псевдоним"}. {"Allow visitors to send private messages to","Разрешить посетителям посылать приватные сообщения"}. {"Allow visitors to send status text in presence updates","Разрешить посетителям вставлять текcт статуса в сообщения о присутствии"}. {"Allow visitors to send voice requests","Разрешить посетителям запрашивать право голоса"}. {"Announcements","Объявления"}. {"April","апреля"}. {"Attribute 'channel' is required for this request","Атрибут 'channel' является обязательным для этого запроса"}. {"Attribute 'id' is mandatory for MIX messages","Атрибут 'id' является обязательным для MIX сообщений"}. {"Attribute 'jid' is not allowed here","Атрибут 'jid' здесь недопустим"}. {"Attribute 'node' is not allowed here","Атрибут 'node' здесь недопустим"}. {"August","августа"}. {"Automatic node creation is not enabled","Автоматическое создание узлов недоступно"}. {"Backup Management","Управление резервным копированием"}. {"Backup of ~p","Резервное копирование ~p"}. {"Backup to File at ","Резервное копирование в файл на "}. {"Backup","Резервное копирование"}. {"Bad format","Неправильный формат"}. {"Birthday","День рождения"}. {"Both the username and the resource are required","Требуются имя пользователя и ресурс"}. {"Bytestream already activated","Поток данных уже активирован"}. {"Cannot remove active list","Невозможно удалить активный список"}. {"Cannot remove default list","Невозможно удалить список по умолчанию"}. {"CAPTCHA web page","Ссылка на капчу"}. {"Change Password","Сменить пароль"}. {"Change User Password","Изменить пароль пользователя"}. {"Changing password is not allowed","Изменение пароля не разрешено"}. {"Changing role/affiliation is not allowed","Изменение роли/ранга не разрешено"}. {"Channel already exists","Канал уже существует"}. {"Channel does not exist","Канал не существует"}. {"Channels","Каналы"}. {"Characters not allowed:","Недопустимые символы:"}. {"Chatroom configuration modified","Конфигурация комнаты изменилась"}. {"Chatroom is created","Комната создана"}. {"Chatroom is destroyed","Комната уничтожена"}. {"Chatroom is started","Комната запущена"}. {"Chatroom is stopped","Комната остановлена"}. {"Chatrooms","Комнаты"}. {"Choose a username and password to register with this server","Выберите имя пользователя и пароль для регистрации на этом сервере"}. {"Choose storage type of tables","Выберите тип хранения таблиц"}. {"Choose whether to approve this entity's subscription.","Решите: предоставить ли подписку этому объекту."}. {"City","Город"}. {"Client acknowledged more stanzas than sent by server","Клиент подтвердил больше сообщений чем было отправлено сервером"}. {"Commands","Команды"}. {"Conference room does not exist","Конференция не существует"}. {"Configuration of room ~s","Конфигурация комнаты ~s"}. {"Configuration","Конфигурация"}. {"Connected Resources:","Подключённые ресурсы:"}. {"Country","Страна"}. {"CPU Time:","Процессорное время:"}. {"Database failure","Ошибка базы данных"}. {"Database Tables at ~p","Таблицы базы данных на ~p"}. {"Database Tables Configuration at ","Конфигурация таблиц базы данных на "}. {"Database","База данных"}. {"December","декабря"}. {"Default users as participants","Сделать пользователей участниками по умолчанию"}. {"Delete content","Удалить содержимое"}. {"Delete message of the day on all hosts","Удалить сообщение дня со всех виртуальных серверов"}. {"Delete message of the day","Удалить сообщение дня"}. {"Delete Selected","Удалить выделенные"}. {"Delete table","Удалить таблицу"}. {"Delete User","Удалить пользователя"}. {"Deliver event notifications","Доставлять уведомления о событиях"}. {"Deliver payloads with event notifications","Доставлять вместе с уведомлениями o публикациях сами публикации"}. {"Description:","Описание:"}. {"Disc only copy","только диск"}. {"Dump Backup to Text File at ","Копирование в текстовый файл на "}. {"Dump to Text File","Копирование в текстовый файл"}. {"Edit Properties","Изменить параметры"}. {"Either approve or decline the voice request.","Подтвердите или отклоните право голоса."}. {"ejabberd MUC module","ejabberd MUC модуль"}. {"ejabberd Multicast service","ejabberd Multicast сервис"}. {"ejabberd Publish-Subscribe module","Модуль ejabberd Публикации-Подписки"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams модуль"}. {"ejabberd vCard module","ejabberd vCard модуль"}. {"ejabberd Web Admin","Web-интерфейс администрирования ejabberd"}. {"ejabberd","ejabberd"}. {"Elements","Элементы"}. {"Email","Электронная почта"}. {"Enable logging","Включить журналирование"}. {"Enable message archiving","Включить хранение сообщений"}. {"Enabling push without 'node' attribute is not supported","Включение push-режима без атрибута 'node' не поддерживается"}. {"End User Session","Завершить сеанс пользователя"}. {"Enter nickname you want to register","Введите псевдоним, который Вы хотели бы зарегистрировать"}. {"Enter path to backup file","Введите путь к резервному файлу"}. {"Enter path to jabberd14 spool dir","Введите путь к директории спула jabberd14"}. {"Enter path to jabberd14 spool file","Введите путь к файлу из спула jabberd14"}. {"Enter path to text file","Введите путь к текстовому файлу"}. {"Enter the text you see","Введите увиденный текст"}. {"Error","Ошибка"}. {"Exclude Jabber IDs from CAPTCHA challenge","Исключить показ капчи для списка Jabber ID"}. {"Export all tables as SQL queries to a file:","Экспортировать все таблицы в виде SQL запросов в файл:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Экспорт данных всех пользователей сервера в файлы формата PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Экспорт пользовательских данных домена в файлы формата PIEFXIS (XEP-0227):"}. {"External component failure","Ошибка внешнего сервиса"}. {"External component timeout","Таймаут внешнего сервиса"}. {"Failed to activate bytestream","Ошибка при активировании потока данных"}. {"Failed to extract JID from your voice request approval","Ошибка обработки JID из вашего запроса на право голоса"}. {"Failed to map delegated namespace to external component","Не получилось найти внешний сервис, делегирующий это пространство имён"}. {"Failed to parse HTTP response","Ошибка разбора HTTP ответа"}. {"Failed to process option '~s'","Ошибка обработки опции '~s'"}. {"Family Name","Фамилия"}. {"February","февраля"}. {"File larger than ~w bytes","Файл больше ~w байт"}. {"Friday","Пятница"}. {"From","От кого"}. {"Full Name","Полное имя"}. {"Get Number of Online Users","Получить количество подключённых пользователей"}. {"Get Number of Registered Users","Получить количество зарегистрированных пользователей"}. {"Get Pending","Получить отложенные"}. {"Get User Last Login Time","Получить время последнего подключения пользователя"}. {"Get User Password","Получить пароль пользователя"}. {"Get User Statistics","Получить статистику по пользователю"}. {"Given Name","Имя"}. {"Grant voice to this person?","Предоставить голос?"}. {"Groups","Группы"}. {"Group","Группа"}. {"has been banned","запретили входить в комнату"}. {"has been kicked because of a system shutdown","выгнали из комнаты из-за останова системы"}. {"has been kicked because of an affiliation change","выгнали из комнаты вследствие смены ранга"}. {"has been kicked because the room has been changed to members-only","выгнали из комнаты потому что она стала только для членов"}. {"has been kicked","выгнали из комнаты"}. {"Host unknown","Неизвестное имя сервера"}. {"Host","Хост"}. {"HTTP File Upload","Передача файлов по HTTP"}. {"Idle connection","Неиспользуемое соединение"}. {"If you don't see the CAPTCHA image here, visit the web page.","Если вы не видите изображение капчи, перейдите по ссылке."}. {"Import Directory","Импорт из директории"}. {"Import File","Импорт из файла"}. {"Import user data from jabberd14 spool file:","Импорт пользовательских данных из буферного файла jabberd14:"}. {"Import User from File at ","Импорт пользователя из файла на "}. {"Import users data from a PIEFXIS file (XEP-0227):","Импорт пользовательских данных из файла формата PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Импорт пользовательских данных из буферной директории jabberd14:"}. {"Import Users from Dir at ","Импорт пользователей из директории на "}. {"Import Users From jabberd14 Spool Files","Импорт пользователей из спула jabberd14"}. {"Improper domain part of 'from' attribute","Неправильная доменная часть атрибута 'from'"}. {"Improper message type","Неправильный тип сообщения"}. {"Incoming s2s Connections:","Входящие s2s соединения:"}. {"Incorrect CAPTCHA submit","Неверный ввод капчи"}. {"Incorrect data form","Некорректная форма данных"}. {"Incorrect password","Неправильный пароль"}. {"Incorrect value of 'action' attribute","Некорректное значение атрибута 'action'"}. {"Incorrect value of 'action' in data form","Некорректное значение 'action' в форме данных"}. {"Incorrect value of 'path' in data form","Некорректное значение 'path' в форме данных"}. {"Insufficient privilege","Недостаточно прав"}. {"Internal server error","Внутренняя ошибка сервера"}. {"Invalid 'from' attribute in forwarded message","Некорректный атрибут 'from' в пересланном сообщении"}. {"Invalid node name","Недопустимое имя узла"}. {"Invalid 'previd' value","Недопустимое значение 'previd'"}. {"Invitations are not allowed in this conference","Рассылка приглашений отключена в этой конференции"}. {"IP addresses","IP адреса"}. {"is now known as","изменил(а) имя на"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Запрещено посылать сообщения об ошибках в эту комнату. Участник (~s) послал сообщение об ошибке (~s) и был выкинут из комнаты"}. {"It is not allowed to send private messages of type \"groupchat\"","Нельзя посылать частные сообщения типа \"groupchat\""}. {"It is not allowed to send private messages to the conference","Не разрешается посылать частные сообщения прямо в конференцию"}. {"It is not allowed to send private messages","Запрещено посылать приватные сообщения"}. {"Jabber ID","Jabber ID"}. {"January","января"}. {"joins the room","вошёл(а) в комнату"}. {"July","июля"}. {"June","июня"}. {"Last Activity","Последнее подключение"}. {"Last login","Время последнего подключения"}. {"Last month","За последний месяц"}. {"Last year","За последний год"}. {"leaves the room","вышел(а) из комнаты"}. {"List of rooms","Список комнат"}. {"Low level update script","Низкоуровневый сценарий обновления"}. {"Make participants list public","Сделать список участников видимым всем"}. {"Make room CAPTCHA protected","Сделать комнату защищённой капчей"}. {"Make room members-only","Комната только для зарегистрированных участников"}. {"Make room moderated","Сделать комнату модерируемой"}. {"Make room password protected","Сделать комнату защищённой паролем"}. {"Make room persistent","Сделать комнату постоянной"}. {"Make room public searchable","Сделать комнату видимой всем"}. {"Malformed username","Недопустимое имя пользователя"}. {"MAM preference modification denied by service policy","Изменение настроек архива сообщений запрещено политикой службы"}. {"March","марта"}. {"Max # of items to persist","Максимальное число сохраняемых публикаций"}. {"Max payload size in bytes","Максимальный размер полезной нагрузки в байтах"}. {"Maximum Number of Occupants","Максимальное количество участников"}. {"May","мая"}. {"Membership is required to enter this room","В эту конференцию могут входить только её члены"}. {"Members:","Члены:"}. {"Memory","Память"}. {"Message body","Тело сообщения"}. {"Message not found in forwarded payload","Сообщение не найдено в пересылаемом вложении"}. {"Messages from strangers are rejected","Сообщения от незнакомцев запрещены"}. {"Middle Name","Отчество"}. {"Minimum interval between voice requests (in seconds)","Минимальный интервал между запросами на право голоса"}. {"Moderator privileges required","Требуются права модератора"}. {"Modified modules","Изменённые модули"}. {"Module failed to handle the query","Ошибка модуля при обработке запроса"}. {"Monday","Понедельник"}. {"Multicast","Мультикаст"}. {"Multi-User Chat","Конференция"}. {"Name","Название"}. {"Name:","Название:"}. {"Neither 'jid' nor 'nick' attribute found","Не найден атрибут 'jid' или 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Не найден атрибут 'role' или 'affiliation'"}. {"Never","Никогда"}. {"New Password:","Новый пароль:"}. {"Nickname can't be empty","Псевдоним не может быть пустым значением"}. {"Nickname Registration at ","Регистрация псевдонима на "}. {"Nickname ~s does not exist in the room","Псевдоним ~s в комнате отсутствует"}. {"Nickname","Псевдоним"}. {"No address elements found","Не найден элемент <address/>"}. {"No addresses element found","Не найден элемент <addresses/>"}. {"No 'affiliation' attribute found","Не найден атрибут 'affiliation'"}. {"No available resource found","Нет доступных ресурсов"}. {"No body provided for announce message","Тело объявления не должно быть пустым"}. {"No child elements found","Нет дочерних элементов"}. {"No data form found","Форма данных не найдена"}. {"No Data","Нет данных"}. {"No features available","Свойства недоступны"}. {"No <forwarded/> element found","Не найден элемент <forwarded/>"}. {"No hook has processed this command","Ни один из хуков не выполнил эту команду"}. {"No info about last activity found","Не найдено информации о последней активности"}. {"No 'item' element found","Элемент 'item' не найден"}. {"No items found in this query","Не найдены элементы в этом запросе"}. {"No limit","Не ограничено"}. {"No module is handling this query","Нет модуля который бы обработал этот запрос"}. {"No node specified","Узел не указан"}. {"No 'password' found in data form","Не найден 'password' в форме данных"}. {"No 'password' found in this query","Не найден 'password' в этом запросе"}. {"No 'path' found in data form","Не найден 'path' в форме данных"}. {"No pending subscriptions found","Не найдено ожидающих решения подписок"}. {"No privacy list with this name found","Списка приватности с таким именем не найдено"}. {"No private data found in this query","Приватные данные не найдены в этом запросе"}. {"No running node found","Нет работающих узлов"}. {"No services available","Нет доступных сервисов"}. {"No statistics found for this item","Не найдено статистики для этого элемента"}. {"No 'to' attribute found in the invitation","Не найден атрибут 'to' в этом приглашении"}. {"Node already exists","Узел уже существует"}. {"Node ID","ID узла"}. {"Node index not found","Индекс узла не найден"}. {"Node not found","Узел не найден"}. {"Node ~p","Узел ~p"}. {"Nodeprep has failed","Ошибка применения профиля Nodeprep"}. {"Nodes","Узлы"}. {"None","Нет"}. {"Not allowed","Недопустимо"}. {"Not Found","Не Найдено"}. {"Not subscribed","Нет подписки"}. {"Notify subscribers when items are removed from the node","Уведомлять подписчиков об удалении публикаций из сборника"}. {"Notify subscribers when the node configuration changes","Уведомлять подписчиков об изменении конфигурации сборника"}. {"Notify subscribers when the node is deleted","Уведомлять подписчиков об удалении сборника"}. {"November","ноября"}. {"Number of occupants","Число присутствующих"}. {"Number of online users","Количество подключённых пользователей"}. {"Number of registered users","Количество зарегистрированных пользователей"}. {"October","октября"}. {"Offline Messages","Офлайновые сообщения"}. {"Offline Messages:","Офлайновые сообщения:"}. {"OK","Продолжить"}. {"Old Password:","Старый пароль:"}. {"Online Users","Подключённые пользователи"}. {"Online Users:","Подключённые пользователи:"}. {"Online","Подключён"}. {"Only deliver notifications to available users","Доставлять уведомления только доступным пользователям"}. {"Only <enable/> or <disable/> tags are allowed","Допустимы только тэги <enable/> или <disable/>"}. {"Only <list/> element is allowed in this query","Только элемент <list/> допустим в этом запросе"}. {"Only members may query archives of this room","Только члены могут запрашивать архивы этой комнаты"}. {"Only moderators and participants are allowed to change the subject in this room","Только модераторы и участники могут изменять тему в этой комнате"}. {"Only moderators are allowed to change the subject in this room","Только модераторы могут изменять тему в этой комнате"}. {"Only moderators can approve voice requests","Только модераторы могут утверждать запросы на право голоса"}. {"Only occupants are allowed to send messages to the conference","Только присутствующим разрешается посылать сообщения в конференцию"}. {"Only occupants are allowed to send queries to the conference","Только присутствующим разрешается посылать запросы в конференцию"}. {"Only service administrators are allowed to send service messages","Только администратор службы может посылать служебные сообщения"}. {"Organization Name","Название организации"}. {"Organization Unit","Отдел организации"}. {"Outgoing s2s Connections:","Исходящие s2s-серверы:"}. {"Outgoing s2s Connections","Исходящие s2s-соединения"}. {"Owner privileges required","Требуются права владельца"}. {"Packet relay is denied by service policy","Пересылка пакетов запрещена политикой службы"}. {"Packet","Пакет"}. {"Password Verification","Проверка пароля"}. {"Password Verification:","Проверка пароля:"}. {"Password","Пароль"}. {"Password:","Пароль:"}. {"Path to Dir","Путь к директории"}. {"Path to File","Путь к файлу"}. {"Pending","Ожидание"}. {"Period: ","Период"}. {"Persist items to storage","Сохранять публикации в хранилище"}. {"Ping query is incorrect","Некорректный пинг-запрос"}. {"Ping","Пинг"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Заметьте, что здесь производится резервное копирование только встроенной базы данных Mnesia. Если Вы также используете другое хранилище данных (например с помощью модуля ODBC), то его резервное копирование следует осуществлять отдельно."}. {"Please, wait for a while before sending new voice request","Пожалуйста, подождите перед тем как подать новый запрос на право голоса"}. {"Pong","Понг"}. {"Present real Jabber IDs to","Сделать реальные Jabber ID участников видимыми"}. {"Previous session not found","Предыдущая сессия не найдена"}. {"Previous session PID has been killed","Предыдущая сессия была убита"}. {"Previous session PID has exited","Процесс предыдущей сессии завершён"}. {"Previous session PID is dead","Предыдущая сессия мертва"}. {"Previous session timed out","Предыдущая сессия не отвечает"}. {"private, ","приватная, "}. {"Publish-Subscribe","Публикация-Подписка"}. {"PubSub subscriber request","Запрос подписчика PubSub"}. {"Purge all items when the relevant publisher goes offline","Очищать все записи автора публикации когда он отключается"}. {"Push record not found","Push-запись не найдена"}. {"Queries to the conference members are not allowed in this room","Запросы к пользователям в этой конференции запрещены"}. {"Query to another users is forbidden","Запрос к другим пользователям запрещён"}. {"RAM and disc copy","ОЗУ и диск"}. {"RAM copy","ОЗУ"}. {"Really delete message of the day?","Действительно удалить сообщение дня?"}. {"Recipient is not in the conference room","Адресата нет в конференции"}. {"Registered Users","Зарегистрированные пользователи"}. {"Registered Users:","Зарегистрированные пользователи:"}. {"Register","Зарегистрировать"}. {"Remote copy","не хранится локально"}. {"Remove All Offline Messages","Удалить все офлайновые сообщения"}. {"Remove User","Удалить пользователя"}. {"Remove","Удалить"}. {"Replaced by new connection","Заменено новым соединением"}. {"Request has timed out","Истекло время ожидания запроса"}. {"Resources","Ресурсы"}. {"Restart Service","Перезапустить службу"}. {"Restart","Перезапустить"}. {"Restore Backup from File at ","Восстановление из резервной копии на "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Восстановить из бинарной резервной копии при следующем запуске (требует меньше памяти):"}. {"Restore binary backup immediately:","Восстановить из бинарной резервной копии немедленно:"}. {"Restore plain text backup immediately:","Восстановить из текстовой резервной копии немедленно:"}. {"Restore","Восстановление из резервной копии"}. {"Room Configuration","Конфигурация комнаты"}. {"Room creation is denied by service policy","Cоздавать конференцию запрещено политикой службы"}. {"Room description","Описание комнаты"}. {"Room Occupants","Участники комнаты"}. {"Room terminates","Комната остановлена"}. {"Room title","Название комнаты"}. {"Roster groups allowed to subscribe","Группы списка контактов, которым разрешена подписка"}. {"Roster size","Размер списка контактов"}. {"RPC Call Error","Ошибка вызова RPC"}. {"Running Nodes","Работающие узлы"}. {"Saturday","Суббота"}. {"Script check","Проверка сценария"}. {"Search Results for ","Результаты поиска в "}. {"Search users in ","Поиск пользователей в "}. {"Select All","Выбрать всё"}. {"Send announcement to all online users on all hosts","Разослать объявление всем подключённым пользователям на всех виртуальных серверах"}. {"Send announcement to all online users","Разослать объявление всем подключённым пользователям"}. {"Send announcement to all users on all hosts","Разослать объявление всем пользователям на всех виртуальных серверах"}. {"Send announcement to all users","Разослать объявление всем пользователям"}. {"September","сентября"}. {"Server:","Сервер:"}. {"Session state copying timed out","Таймаут копирования состояния сессии"}. {"Set message of the day and send to online users","Установить сообщение дня и разослать его подключённым пользователям"}. {"Set message of the day on all hosts and send to online users","Установить сообщение дня на всех виртуальных серверах и разослать его подключённым пользователям"}. {"Shared Roster Groups","Группы общих контактов"}. {"Show Integral Table","Показать интегральную таблицу"}. {"Show Ordinary Table","Показать обычную таблицу"}. {"Shut Down Service","Остановить службу"}. {"SOCKS5 Bytestreams","Передача файлов через SOCKS5"}. {"Specify the access model","Укажите механизм управления доступом"}. {"Specify the event message type","Укажите тип сообщения о событии"}. {"Specify the publisher model","Условия публикации"}. {"Statistics of ~p","статистика узла ~p"}. {"Statistics","Статистика"}. {"Stopped Nodes","Остановленные узлы"}. {"Stop","Остановить"}. {"Storage Type","Тип таблицы"}. {"Store binary backup:","Сохранить бинарную резервную копию:"}. {"Store plain text backup:","Сохранить текстовую резервную копию:"}. {"Stream management is already enabled","Управление потоком уже активировано"}. {"Stream management is not enabled","Управление потоком не активировано"}. {"Subject","Тема"}. {"Submitted","Отправлено"}. {"Submit","Отправить"}. {"Subscriber Address","Адрес подписчика"}. {"Subscriptions are not allowed","Подписки недопустимы"}. {"Subscription","Подписка"}. {"Sunday","Воскресенье"}. {"That nickname is already in use by another occupant","Этот псевдоним уже занят другим участником"}. {"That nickname is registered by another person","Этот псевдоним зарегистрирован кем-то другим"}. {"The account already exists","Учётная запись уже существует"}. {"The CAPTCHA is valid.","Проверка капчи прошла успешно."}. {"The CAPTCHA verification has failed","Проверка капчи не пройдена"}. {"The captcha you entered is wrong","Неправильно введённое значение капчи"}. {"The collections with which a node is affiliated","Имя коллекции, в которую входит узел"}. {"The feature requested is not supported by the conference","Запрашиваемое свойство не поддерживается этой конференцией"}. {"The password contains unacceptable characters","Пароль содержит недопустимые символы"}. {"The password is too weak","Слишком слабый пароль"}. {"the password is","пароль:"}. {"The password was not changed","Пароль не был изменён"}. {"The passwords are different","Пароли не совпадают"}. {"The query is only allowed from local users","Запрос доступен только для локальных пользователей"}. {"The query must not contain <item/> elements","Запрос не должен содержать элементов <item/>"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Строфа может содержать только один элемент <active/>, один элемент <default/> или один элемент <list/>"}. {"The username is not valid","Недопустимое имя пользователя"}. {"There was an error creating the account: ","Ошибка при создании аккаунта:"}. {"There was an error deleting the account: ","Ошибка при удалении аккаунта:"}. {"This room is not anonymous","Эта комната не анонимная"}. {"This service can not process the address: ~s","Сервер не может обработать адрес: ~s"}. {"Thursday","Четверг"}. {"Time delay","По истечение"}. {"Timed out waiting for stream resumption","Истекло время ожидания возобновления потока"}. {"Time","Время"}. {"To register, visit ~s","Для регистрации посетите ~s"}. {"Token TTL","Токен TTL"}. {"Too many active bytestreams","Слишком много активных потоков данных"}. {"Too many CAPTCHA requests","Слишком много запросов капчи"}. {"Too many child elements","Слишком много дочерних элементов"}. {"Too many <item/> elements","Слишком много элементов <item/>"}. {"Too many <list/> elements","Слишком много элементов <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Слишком много (~p) неудачных попыток аутентификации с этого IP-адреса (~s). Адрес будет разблокирован в ~s UTC"}. {"Too many receiver fields were specified","Указано слишком много получателей"}. {"Too many unacked stanzas","Слишком много неподтверждённых пакетов"}. {"Too many users in this conference","Слишком много пользователей в этой конференции"}. {"Total rooms","Все комнаты"}. {"To","Кому"}. {"Traffic rate limit is exceeded","Превышен лимит скорости посылки информации"}. {"Transactions Aborted:","Транзакции отмененные:"}. {"Transactions Committed:","Транзакции завершенные:"}. {"Transactions Logged:","Транзакции запротоколированные:"}. {"Transactions Restarted:","Транзакции перезапущенные:"}. {"Tuesday","Вторник"}. {"Unable to generate a CAPTCHA","Не получилось создать капчу"}. {"Unable to register route on existing local domain","Нельзя регистрировать маршруты на существующие локальные домены"}. {"Unauthorized","Не авторизован"}. {"Unexpected action","Неожиданное действие"}. {"Unexpected error condition: ~p","Неожиданная ошибка: ~p"}. {"Unregister","Удалить"}. {"Unselect All","Снять всё выделение"}. {"Unsupported <index/> element","Элемент <index/> не поддерживается"}. {"Unsupported version","Неподдерживаемая версия"}. {"Update message of the day (don't send)","Обновить сообщение дня (не рассылать)"}. {"Update message of the day on all hosts (don't send)","Обновить сообщение дня на всех виртуальных серверах (не рассылать)"}. {"Update plan","План обновления"}. {"Update ~p","Обновление ~p"}. {"Update script","Сценарий обновления"}. {"Update","Обновить"}. {"Uptime:","Время работы:"}. {"User already exists","Пользователь уже существует"}. {"User JID","JID пользователя"}. {"User (jid)","Пользователь (XMPP адрес)"}. {"User Management","Управление пользователями"}. {"User removed","Пользователь удалён"}. {"User session not found","Сессия пользователя не найдена"}. {"User session terminated","Сессия пользователя завершена"}. {"Username:","Имя пользователя:"}. {"Users are not allowed to register accounts so quickly","Пользователи не могут регистрировать учётные записи так быстро"}. {"Users Last Activity","Статистика последнего подключения пользователей"}. {"Users","Пользователи"}. {"User","Пользователь"}. {"Validate","Утвердить"}. {"Value 'get' of 'type' attribute is not allowed","Значение 'get' атрибута 'type' недопустимо"}. {"Value of '~s' should be boolean","Значение '~s' должно быть булевым"}. {"Value of '~s' should be datetime string","Значение '~s' должно быть датой"}. {"Value of '~s' should be integer","Значение '~s' должно быть целочисленным"}. {"Value 'set' of 'type' attribute is not allowed","Значение 'set' атрибута 'type' недопустимо"}. {"vCard User Search","Поиск пользователей по vCard"}. {"Virtual Hosts","Виртуальные хосты"}. {"Visitors are not allowed to change their nicknames in this room","Посетителям запрещено изменять свои псевдонимы в этой комнате"}. {"Visitors are not allowed to send messages to all occupants","Посетителям не разрешается посылать сообщения всем присутствующим"}. {"Voice requests are disabled in this conference","Запросы на право голоса отключены в этой конференции"}. {"Voice request","Запрос на право голоса"}. {"Wednesday","Среда"}. {"When to send the last published item","Когда посылать последний опубликованный элемент"}. {"Whether to allow subscriptions","Разрешить подписку"}. {"Wrong parameters in the web formulary","Недопустимые параметры веб-формы"}. {"Wrong xmlns","Неправильный xmlns"}. {"You are being removed from the room because of a system shutdown","Вы покинули комнату из-за останова системы"}. {"You are not joined to the channel","Вы не присоединены к каналу"}. {"You have been banned from this room","Вам запрещено входить в эту конференцию"}. {"You have joined too many conferences","Вы присоединены к слишком большому количеству конференций"}. {"You must fill in field \"Nickname\" in the form","Вы должны заполнить поле \"Псевдоним\" в форме"}. {"You need a client that supports x:data and CAPTCHA to register","Чтобы зарегистрироваться, требуется x:data-совместимый клиент"}. {"You need a client that supports x:data to register the nickname","Чтобы зарегистрировать псевдоним, требуется x:data-совместимый клиент"}. {"You need an x:data capable client to search","Чтобы воспользоваться поиском, требуется x:data-совместимый клиент"}. {"Your active privacy list has denied the routing of this stanza.","Маршрутизация этой строфы запрещена вашим активным списком приватности."}. {"Your contact offline message queue is full. The message has been discarded.","Очередь недоставленных сообщений Вашего адресата переполнена. Сообщение не было сохранено."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ваши запросы на добавление в контакт-лист, а также сообщения к ~s блокируются. Для снятия блокировки перейдите по ссылке ~s"}. {"You're not allowed to create nodes","Вам не разрешается создавать узлы"}. ������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/no.msg���������������������������������������������������������������������0000644�0002322�0002322�00000055166�14154362354�016724� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," har satt emnet til: "}. {"A friendly name for the node","Et vennlig navn for noden"}. {"A password is required to enter this room","Et passord kreves for tilgang til samtalerommet"}. {"Accept","Godta"}. {"Access denied by service policy","Tilgang nektes på grunn av en tjenesteregel"}. {"Access model of presence","Tilgangsmodell for tilstedeværelse"}. {"Access model of roster","Tilgangsmodell for kontaktliste"}. {"Action on user","Handling på bruker"}. {"Add Jabber ID","Legg til Jabber-ID"}. {"Add New","Legg til ny"}. {"Add User","Legg til bruker"}. {"Administration of ","Administrasjon av "}. {"Administration","Administrasjon"}. {"Administrator privileges required","Administratorprivilegier kreves"}. {"All activity","All aktivitet"}. {"All Users","Alle brukere"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Tillat denne Jabber-ID-en å abonnere på denne pubsub -noden?"}. {"Allow users to change the subject","Tillat brukere å endre emnet"}. {"Allow users to query other users","Tillat brukere å sende forespørsler til andre brukere"}. {"Allow users to send invites","Tillat brukere å sende invitasjoner"}. {"Allow users to send private messages","Tillat brukere å sende private meldinger"}. {"Allow visitors to change nickname","Tillat besøkende å endre kallenavn"}. {"Allow visitors to send private messages to","Tillat brukere å sende private meldinger til"}. {"Allow visitors to send status text in presence updates","Tillat besøkende å sende statustekst i tilstedeværelsesoppdateringer"}. {"Allow visitors to send voice requests","Tillat brukere å sende lydforespørsler"}. {"Announcements","Kunngjøringer"}. {"Answer to a question","Svar på spørsmål"}. {"April","april"}. {"August","august"}. {"Backup Management","Håndtering av sikkerehetskopiering"}. {"Backup of ~p","Sikkerhetskopi av ~p"}. {"Backup to File at ","Sikkerhetskopier til fil på "}. {"Backup","Sikkerhetskopiering"}. {"Bad format","Feilaktig format"}. {"Birthday","Geburtsdag"}. {"Both the username and the resource are required","Både brukernavn og ressurs kreves"}. {"Cannot remove active list","Kan ikke fjerne aktiv liste"}. {"Cannot remove default list","Kan ikke fjerne forvalgt liste"}. {"CAPTCHA web page","CAPTCHA-nettside"}. {"Challenge ID","Utfordrings-ID"}. {"Change Password","Endre passord"}. {"Change User Password","Endre brukerpassord"}. {"Channel already exists","Kanalen finnes allerede"}. {"Channels","Kanaler"}. {"Characters not allowed:","Ikke godtatte tegn:"}. {"Chatroom configuration modified","Samtalerommets oppsett er endret"}. {"Chatroom is created","Samtalerom opprettet"}. {"Chatroom is destroyed","Samtalerom er fjernet"}. {"Chatroom is started","Samtalerom startet"}. {"Chatroom is stopped","Samtalerom stoppet"}. {"Chatrooms","Samtalerom"}. {"Choose a username and password to register with this server","Velg et brukernavn og passord for å registrere deg på denne tjeneren"}. {"Choose storage type of tables","Velg lagringstype for tabeller"}. {"City","By"}. {"Commands","Kommandoer"}. {"Conference room does not exist","Konferanserommet finnes ikke"}. {"Configuration of room ~s","Oppsett for rom ~s"}. {"Configuration","Oppsett"}. {"Connected Resources:","Tilkoblede ressurser:"}. {"Country","Land"}. {"CPU Time:","Prosessortid:"}. {"Current Discussion Topic","Nåværende diskusjonstema"}. {"Database Tables at ~p","Databasetabeller på ~p"}. {"Database Tables Configuration at ","Database-tabelloppsett på "}. {"Database","Database"}. {"December","desember"}. {"Default users as participants","Standard brukere som deltakere"}. {"Delete message of the day","Slett melding for dagen"}. {"Delete Selected","Slett valgte"}. {"Delete User","Slett bruker"}. {"Deliver event notifications","Lever begivenhetskunngjøringer"}. {"Deliver payloads with event notifications","Send innhold sammen med hendelsesmerknader"}. {"Description:","Beskrivelse:"}. {"Disc only copy","Kun diskkopi"}. {"'Displayed groups' not added (they do not exist!): ","«Viste grupper» ikke lagt til (de finnes ikke!): "}. {"Dump Backup to Text File at ","Dump sikkerhetskopi til tekstfil på "}. {"Dump to Text File","Dump til tekstfil"}. {"Edit Properties","Rediger egenskaper"}. {"Either approve or decline the voice request.","Enten godkjenn eller forby lydforespørselen."}. {"ejabberd HTTP Upload service","ejabberd-HTTP-opplastingstjeneste"}. {"ejabberd MUC module","ejabberd-MUC-modul"}. {"ejabberd Multicast service","ejabberd-multikastingstjeneste"}. {"ejabberd Publish-Subscribe module","ejabberd-Publish-Subscribe-modul"}. {"Elements","Elementer"}. {"Email","E-post"}. {"Enable logging","Skru på loggføring"}. {"Enable message archiving","Skru på meldingsarkivering"}. {"Enabling push without 'node' attribute is not supported","Å skru på dytting uten «node»-attributt støttes ikke"}. {"End User Session","Avslutt brukerøkt"}. {"Enter nickname you want to register","Skriv inn kallenavnet du ønsker å registrere"}. {"Enter path to backup file","Skriv inn sti til sikkerhetskopifilen"}. {"Enter path to jabberd14 spool dir","Skriv inn sti til jabberd14 spoolkatalog"}. {"Enter path to text file","Skriv inn sti til tekstfil"}. {"Enter the text you see","Skriv inn teksten du ser"}. {"Error","Feil"}. {"Exclude Jabber IDs from CAPTCHA challenge","Ekskluder Jabber-ID-er fra CAPTCHA-utfordring"}. {"Export all tables as SQL queries to a file:","Eksporter alle tabeller som SQL-spørringer til en fil:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere på en tjener til PIEFXIS-filer (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksporter data om alle brukere på en vert til PIEFXIS-filer (XEP-0227):"}. {"External component failure","Feil med ekstern komponent"}. {"External component timeout","Tidsavbrudd for ekstern komponent"}. {"Family Name","Etternavn"}. {"FAQ Entry","O-S-S -oppføring"}. {"February","februar"}. {"File larger than ~w bytes","Fil større enn ~w byte"}. {"Friday","fredag"}. {"From","Fra"}. {"Full List of Room Admins","Full liste over romadministratorer"}. {"Full List of Room Owners","Full liste over romeiere"}. {"Full Name","Fullt navn"}. {"Get Number of Online Users","Vis antall tilkoblede brukere"}. {"Get Number of Registered Users","Vis antall registrerte brukere"}. {"Get User Last Login Time","Vis brukers siste innloggingstidspunkt"}. {"Get User Password","Hent brukers passord"}. {"Get User Statistics","Vis brukerstatistikk"}. {"Group","Gruppe"}. {"Groups","Grupper"}. {"has been banned","har blitt bannlyst"}. {"has been kicked because of a system shutdown","har blitt kastet ut på grunn av systemavstenging"}. {"has been kicked because of an affiliation change","har blitt kastet ut på grunn av en tilknytningsendring"}. {"has been kicked","har blitt kastet ut"}. {"Host unknown","Ukjent vert"}. {"Host","Vert"}. {"HTTP File Upload","HTTP-filopplasting"}. {"If you don't see the CAPTCHA image here, visit the web page.","Dersom du ikke ser et CAPTCHA-bilde her, besøk nettsiden."}. {"Import Directory","Importer mappe"}. {"Import File","Importer file"}. {"Import User from File at ","Importer bruker fra fil på "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importer data fra bruker fra en PIEFXIS-fil (XEP-0227):"}. {"Import Users from Dir at ","Importer brukere fra mappe på "}. {"Improper message type","Feilaktig meldingstype"}. {"Incorrect password","Feil passord"}. {"Insufficient privilege","Mangler tilstrekkelige rettigheter"}. {"Internal server error","Intern tjenerfeil"}. {"Invalid 'from' attribute in forwarded message","Ugyldig «fra»-attributt i videresendt melding"}. {"IP addresses","IP-adresser"}. {"is now known as","er nå kjent som"}. {"It is not allowed to send private messages to the conference","Det er ikke tillatt å sende private meldinger til konferansen"}. {"It is not allowed to send private messages","Det er ikke tillatt å sende private meldinger"}. {"Jabber ID","Jabber-ID"}. {"January","januar"}. {"JID normalization failed","JID-normalisering mislyktes"}. {"joins the room","tar del i rommet"}. {"July","juli"}. {"June","juni"}. {"Just created","Akkurat opprettet"}. {"Label:","Etikett:"}. {"Last Activity","Siste aktivitet"}. {"Last login","Siste innlogging"}. {"Last month","Siste måned"}. {"Last year","Siste år"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","De minst viktige bit-ene av SHA-256-sjekksummen for tekst skal tilsvare heksadesimal etikett"}. {"leaves the room","forlater rommet"}. {"List of rooms","Romliste"}. {"Logging","Loggføring"}. {"Low level update script","Lavnivå-oppdateringsskript"}. {"Make participants list public","Gjør deltakerlisten offentlig"}. {"Make room members-only","Gjør rommet tilgjengelig kun for medlemmer"}. {"Make room password protected","Passordbeskytt rommet"}. {"Make room persistent","Gjør rommet vedvarende"}. {"Make room public searchable","Gjør rommet offentlig søkbart"}. {"March","mars"}. {"Max # of items to persist","Høyeste # elementer som skal lagres"}. {"Maximum file size","Maksimal filstørrelse"}. {"Maximum Number of History Messages Returned by Room","Maksimalt antall historikkmeldinger tilbudt av rommet"}. {"May","mai"}. {"Membership is required to enter this room","Medlemskap kreves for tilgang til dette rommet"}. {"Members:","Medlemmer:"}. {"Memory","Minne"}. {"Message body","Meldingskropp"}. {"Message not found in forwarded payload","Fant ikke melding i videresendt nyttelast"}. {"Messages from strangers are rejected","Meldinger fra ukjente avvises"}. {"Messages of type headline","Meldinger av typen overskrift"}. {"Messages of type normal","Meldinger av normal type"}. {"Middle Name","Mellomnavn"}. {"Minimum interval between voice requests (in seconds)","Minimumsintervall mellom lydforespørsler (i sekunder)"}. {"Modified modules","Endrede moduler"}. {"Monday","mandag"}. {"Multicast","Multikasting"}. {"Multi-User Chat","Multibrukersludring"}. {"Name in the rosters where this group will be displayed","Navn i kontaktlistene der denne gruppen vises"}. {"Name","Navn"}. {"Name:","Navn:"}. {"Never","Aldri"}. {"New Password:","Nytt passord:"}. {"Nickname can't be empty","Kallenavn kan ikke stå tomt"}. {"Nickname Registration at ","Registrer kallenavn på "}. {"Nickname","Kallenavn"}. {"No available resource found","Fant ingen tilgjengelig ressurs"}. {"No body provided for announce message","Ingen meldingskropp angitt for kunngjøringsmelding"}. {"No Data","Ingen data"}. {"No features available","Ingen tilgjengelige funksjoner"}. {"No <forwarded/> element found","Fant ikke noe <forwarded/>-element"}. {"No limit","Ingen grense"}. {"Node already exists","Node finnes allerede"}. {"Node ID","Node-ID"}. {"Node not found","Noden finnes ikke"}. {"Nodes","Noder"}. {"None","Ingen"}. {"Not Found","Finnes ikke"}. {"Notify subscribers when the node configuration changes","Informer abonnenter når nodeoppsettet endres"}. {"Notify subscribers when the node is deleted","Informer abonnenter når noden slettes"}. {"November","november"}. {"Number of occupants","Antall deltakere"}. {"Number of online users","Antall tilkoblede brukere"}. {"Number of registered users","Antall registrerte brukere"}. {"October","oktober"}. {"Offline Messages","Frakoblede meldinger"}. {"Offline Messages:","Frakoblede meldinger:"}. {"OK","OK"}. {"Old Password:","Gammelt passord:"}. {"Online Users","Tilkoblede brukere"}. {"Online Users:","Tilkoblede brukere:"}. {"Online","Tilkoblet"}. {"Only admins can see this","Kun administratorer kan se dette"}. {"Only deliver notifications to available users","Kun send kunngjøringer til tilgjengelige brukere"}. {"Only occupants are allowed to send messages to the conference","Bare deltakere får sende normale meldinger til konferansen"}. {"Only occupants are allowed to send queries to the conference","Kun deltakere tillates å sende forespørsler til konferansen"}. {"Only publishers may publish","Kun publiserere kan publisere"}. {"Only service administrators are allowed to send service messages","Bare tjenesteadministratorer tillates å sende tjenestemeldinger"}. {"Organization Name","Organisasjonsnavn"}. {"Organization Unit","Organisasjonsenhet"}. {"Outgoing s2s Connections","Utgående s2s-koblinger"}. {"Outgoing s2s Connections:","Utgående s2s-koblinger"}. {"Owner privileges required","Eierprivilegier kreves"}. {"Packet","Pakke"}. {"Participant","Deltager"}. {"Password Verification","Passordbekreftelse"}. {"Password Verification:","Passordbekreftelse:"}. {"Password","Passord"}. {"Password:","Passord:"}. {"Path to Dir","Sti til mappe"}. {"Path to File","Sti til fil"}. {"Pending","Ventende"}. {"Period: ","Periode: "}. {"Persist items to storage","Vedvarende elementer til lagring"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Merk at disse valgene kun vil sikkerhetskopiere den innebygde Mnesia-databasen. Dersom du bruker ODBC-modulen må du også ta sikkerhetskopi av din SQL-database."}. {"Previous session PID has exited","Forrige økt-ID har avsluttet"}. {"Previous session PID is dead","Forrige økt-PID er død"}. {"Previous session timed out","Tidsavbrudd for forrige økt"}. {"private, ","privat, "}. {"Public","Offentlig"}. {"PubSub subscriber request","PubSub-abonementsforespørsel"}. {"Purge all items when the relevant publisher goes offline","Rydd alle elementer når den aktuelle utgiveren logger av"}. {"Queries to the conference members are not allowed in this room","Forespørsler til konferansemedlemmerer tillates ikke i dette rommet"}. {"Query to another users is forbidden","Spørringer til andre brukere er forbudt"}. {"RAM and disc copy","Minne og diskkopi"}. {"RAM copy","Minnekopi"}. {"Really delete message of the day?","Vil du virkelig slette melding for dagen?"}. {"Recipient is not in the conference room","Mottakeren er ikke i konferanserommet"}. {"Registered Users","Registrerte brukere"}. {"Registered Users:","Registrerte brukere:"}. {"Register","Registrer"}. {"Remove All Offline Messages","Fjern Alle frakoblede meldinger"}. {"Remove User","Fjern bruker"}. {"Remove","Fjern"}. {"Replaced by new connection","Erstattet av en ny tilkobling"}. {"Request has timed out","Tidsavbrudd for forespørsel"}. {"Request is ignored","Forespørsel ignorert"}. {"Requested role","Forespurt rolle"}. {"Resources","Ressurser"}. {"Restart Service","Omstart av tjeneste"}. {"Restart","Start på ny"}. {"Restore Backup from File at ","Gjenopprett fra sikkerhetskopifil på "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Gjenopprett binær sikkerhetskopi etter neste ejabberd-omstart (krever mindre minne):"}. {"Restore binary backup immediately:","Gjenopprett binær sikkerhetskopi umiddelbart:"}. {"Restore plain text backup immediately:","Gjenopprette rentekst sikkerhetskopi umiddelbart:"}. {"Restore","Gjenopprett"}. {"Room Configuration","Rom-oppsett"}. {"Room creation is denied by service policy","Oppretting av rom nektes av en tjensteregel"}. {"Room description","Rom-beskrivelse"}. {"Room Occupants","Samtaleromsdeltagere"}. {"Room title","Rom-tittel"}. {"Roster groups allowed to subscribe","Kontaktlistegrupper som tillates å abonnere"}. {"Roster size","Kontaktlistestørrelse"}. {"Running Nodes","Kjørende noder"}. {"Saturday","lørdag"}. {"Script check","Skript-sjekk"}. {"Search Results for ","Søkeresultater for "}. {"Search users in ","Søk etter brukere i "}. {"Select All","Velg alt"}. {"Send announcement to all online users on all hosts","Send kunngjøring til alle tilkoblede brukere på alle verter"}. {"Send announcement to all online users","Send kunngjøring alle tilkoblede brukere"}. {"Send announcement to all users on all hosts","Send kunngjøring til alle brukere på alle verter"}. {"Send announcement to all users","Send kunngjøring til alle brukere"}. {"September","september"}. {"Server:","Tjener:"}. {"Set message of the day and send to online users","Angi melding for dagen og send til tilkoblede brukere"}. {"Shared Roster Groups","Delte kontaktgrupper"}. {"Show Ordinary Table","Vis ordinær tabell"}. {"Shut Down Service","Avslutt tjeneste"}. {"Specify the access model","Spesifiser tilgangsmodellen"}. {"Specify the event message type","Spesifiser hendelsesbeskjedtypen"}. {"Specify the publisher model","Angi publiseringsmodell"}. {"Statistics of ~p","Statistikk for ~p"}. {"Statistics","Statistikk"}. {"Stopped Nodes","Stoppede noder"}. {"Stop","Stopp"}. {"Storage Type","Lagringstype"}. {"Store binary backup:","Lagre binær sikkerhetskopi:"}. {"Store plain text backup:","Lagre klartekst-sikkerhetskopi:"}. {"Subject","Emne"}. {"Submit","Send"}. {"Submitted","Innsendt"}. {"Subscriber Address","Abonnementsadresse"}. {"Subscribers may publish","Abonnenter kan publisere"}. {"Subscription","Abonnement"}. {"Subscriptions are not allowed","Abonnementer tillates ikke"}. {"Sunday","søndag"}. {"Text associated with a picture","Tekst tilknyttet et bilde"}. {"Text associated with a sound","Tekst tilknyttet en lyd"}. {"Text associated with a video","Tekst tilknyttet en video"}. {"Text associated with speech","Tekst tilknyttet tale"}. {"That nickname is already in use by another occupant","Det kallenavnet er allerede i bruk av en annen deltaker"}. {"That nickname is registered by another person","Det kallenavnet er registrert av en annen person"}. {"The account already exists","Kontoen finnes allerede"}. {"The account was not unregistered","Kontoen ble ikke avregistrert"}. {"The body text of the last received message","Brødteksten i sist mottatte melding"}. {"The CAPTCHA is valid.","CAPTCHA-en er gyldig."}. {"The CAPTCHA verification has failed","CAPTCHA-godkjenning mislyktes"}. {"The captcha you entered is wrong","CAPTCHA-en du skrev inn er feil"}. {"The number of unread or undelivered messages","Antallet uleste eller uleverte meldinger"}. {"The password contains unacceptable characters","Passordet inneholder ulovlige tegn"}. {"The password is too weak","Passordet er for svakt"}. {"the password is","passordet er"}. {"The presence states for which an entity wants to receive notifications","Tilstedeværelsestilstandene en eksistens ønsker å motta merknader for"}. {"The query is only allowed from local users","Spørringen tillates kun fra lokale brukere"}. {"The query must not contain <item/> elements","Spørringen kan ikke inneholde <item/>-elementer"}. {"The room subject can be modified by participants","Romemnet kan endres av dets deltagere"}. {"The sender of the last received message","Avsender for sist mottatte melding"}. {"The username is not valid","Brukernavnet er ikke gyldig"}. {"There was an error creating the account: ","En feil inntraff under oppretting av kontoen: "}. {"There was an error deleting the account: ","En feil inntraff under sletting av kontoen: "}. {"This room is not anonymous","Dette rommet er ikke anonymt"}. {"Thursday","torsdag"}. {"Time delay","Tidsforsinkelse"}. {"Time","Tid"}. {"To register, visit ~s","Besøk ~s for registrering"}. {"Too many CAPTCHA requests","For mange CAPTCHA-forespørsler"}. {"Too many <item/> elements","For mange <item/>-elementer"}. {"Too many <list/> elements","For mange <list/>-elementer"}. {"To","Til"}. {"Traffic rate limit is exceeded","Grense for tillatt trafikkmengde overskredet"}. {"Transactions Aborted:","Avbrutte transaksjoner:"}. {"Transactions Committed:","Sendte transaksjoner:"}. {"Transactions Logged:","Loggede transaksjoner:"}. {"Transactions Restarted:","Omstartede transaksjoner:"}. {"Tuesday","tirsdag"}. {"Unable to generate a CAPTCHA","Kunne ikke generere CAPTCHA"}. {"Unauthorized","Uautorisert"}. {"Unregister","Avregistrer"}. {"Unsupported <index/> element","Ustøttet <index/>-element"}. {"Unsupported version","Ustøttet versjon"}. {"Update message of the day (don't send)","Oppdater melding for dagen (ikke send)"}. {"Update message of the day on all hosts (don't send)","Oppdater melding for dagen på alle verter (ikke send)"}. {"Update plan","Oppdateringplan"}. {"Update ~p","Oppdater ~p"}. {"Update script","Oppdateringsskript"}. {"Update","Oppdater"}. {"Uptime:","Oppetid:"}. {"User already exists","Brukeren finnes allerede"}. {"User Management","Brukerhåndtering"}. {"User removed","Fjernet bruker"}. {"User ~ts","Bruker ~ts"}. {"User","Bruker"}. {"Username:","Brukernavn:"}. {"Users are not allowed to register accounts so quickly","Brukere har ikke lov til registrere kontoer så fort"}. {"Users Last Activity","Brukers siste aktivitet"}. {"Users","Brukere"}. {"Validate","Bekrefte gyldighet"}. {"vCard User Search","vCard-brukersøk"}. {"View Queue","Vis kø"}. {"Virtual Hosts","Virtuelle maskiner"}. {"Visitor","Besøker"}. {"Visitors are not allowed to change their nicknames in this room","Besøkende får ikke lov å endre kallenavn i dette rommet"}. {"Visitors are not allowed to send messages to all occupants","Besøkende får ikke sende meldinger til alle deltakere"}. {"Voice requests are disabled in this conference","Stemmeforespørsler er blokkert i denne konferansen"}. {"Voice request","Stemmeforespørsel"}. {"Wednesday","onsdag"}. {"When to send the last published item","Når skal siste publiserte artikkel sendes"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Hvorvidt en eksistens ønsker å motta en XMPP-meldingsbrødtekst i tillegg til nyttedata-formatet"}. {"Whether an entity wants to receive or disable notifications","Hvorvidt en eksistens ønsker å motta eller skru av merknader"}. {"Whether owners or publisher should receive replies to items","Hvorvidt elere eller publisererer skal motta svar på elementer"}. {"Whether to allow subscriptions","Hvorvidt abonnementer skal tillates"}. {"XMPP Domains","XMPP-domener"}. {"You have been banned from this room","Du har blitt bannlyst fra dette rommet."}. {"You have joined too many conferences","Du har tilknyttet deg for mange konferanser"}. {"You must fill in field \"Nickname\" in the form","Du må fylle inn feltet «Kallenavn» i skjemaet"}. {"You need a client that supports x:data and CAPTCHA to register","Du trenger en klient som støtter x:data og CAPTCHA for registrering"}. {"You need a client that supports x:data to register the nickname","Du trenger en klient som støtter x:data for å registrere kallenavnet"}. {"You need an x:data capable client to search","Du trenger en klient som støtter x:data for å søke"}. {"Your contact offline message queue is full. The message has been discarded.","Kontaktens frakoblede meldingskø er full. Meldingen har blitt kassert."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Din abonnementsforespørsel og/eller meldinger til ~s har blitt blokkert. For å avblokkere din abonnementsforespørsel, besøk ~s"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/ja.msg���������������������������������������������������������������������0000644�0002322�0002322�00000076015�14154362354�016676� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (* を最後に付けると部分文字列にマッチします)"}. {" has set the subject to: "," は件名を設定しました: "}. {"A description of the node","ノードの説明"}. {"A friendly name for the node","ノードのフレンドリネーム"}. {"A password is required to enter this room","このチャットルームに入るにはパスワードが必要です"}. {"A Web Page","ウェブページ"}. {"Accept","許可"}. {"Access denied by service policy","サービスポリシーによってアクセスが禁止されました"}. {"Access model","アクセスモデル"}. {"Account doesn't exist","アカウントは存在しません"}. {"Action on user","ユーザー操作"}. {"Add Jabber ID","Jabber ID を追加"}. {"Add New","新規追加"}. {"Add User","ユーザーを追加"}. {"Administration of ","管理: "}. {"Administration","管理"}. {"Administrator privileges required","管理者権限が必要です"}. {"All activity","すべて"}. {"All Users","全ユーザー"}. {"Allow subscription","購読を認可"}. {"Allow this Jabber ID to subscribe to this pubsub node?","この Jabber ID に、この pubsubノードの購読を許可しますか ?"}. {"Allow users to change the subject","ユーザーによる件名の変更を許可"}. {"Allow users to query other users","ユーザーによる他のユーザーへのクエリーを許可"}. {"Allow users to send invites","ユーザーによる招待を許可"}. {"Allow users to send private messages","ユーザーによるプライベートメッセージの送信を許可"}. {"Allow visitors to change nickname","傍聴者のニックネームの変更を許可"}. {"Allow visitors to send private messages to","傍聴者によるプライベートメッセージの送信を次の相手に許可"}. {"Allow visitors to send status text in presence updates","傍聴者によるプレゼンス更新のステータス文の送信を許可"}. {"Allow visitors to send voice requests","傍聴者による発言権の要求を許可"}. {"Announcements","アナウンス"}. {"Anyone","誰にでも"}. {"April","4月"}. {"August","8月"}. {"Backup Management","バックアップ管理"}. {"Backup of ~p","バックアップ: ~p"}. {"Backup to File at ","ファイルにバックアップ: "}. {"Backup","バックアップ"}. {"Bad format","不正なフォーマット"}. {"Birthday","誕生日"}. {"Both the username and the resource are required","ユーザー名とリソースの両方が必要"}. {"CAPTCHA web page","CAPTCHA ウェブページ"}. {"Change Password","パスワードを変更"}. {"Change User Password","パスワードを変更"}. {"Changing password is not allowed","パスワード変更の権限がありません"}. {"Channel already exists","チャンネルは既に存在します"}. {"Channel does not exist","チャンネルは存在しません"}. {"Channels","チャンネル"}. {"Characters not allowed:","使用できない文字:"}. {"Chatroom configuration modified","チャットルームの設定が変更されました"}. {"Chatroom is created","チャットルームを作りました"}. {"Chatroom is destroyed","チャットルームを終了しました"}. {"Chatroom is started","チャットルームを開始しました"}. {"Chatroom is stopped","チャットルームを停止しました"}. {"Chatrooms","チャットルーム"}. {"Choose a username and password to register with this server","サーバーに登録するユーザー名とパスワードを選択してください"}. {"Choose storage type of tables","テーブルのストレージタイプを選択"}. {"Choose whether to approve this entity's subscription.","このエントリを承認するかどうかを選択してください"}. {"City","都道府県"}. {"Commands","コマンド"}. {"Conference room does not exist","会議室は存在しません"}. {"Configuration of room ~s","チャットルーム ~s の設定"}. {"Configuration","設定"}. {"Connected Resources:","接続リソース:"}. {"Contact Addresses (normally, room owner or owners)","連絡先 (通常は会議室の主宰者またはその複数)"}. {"Country","国"}. {"CPU Time:","CPU時間:"}. {"Current Discussion Topic","現在の話題"}. {"Database Tables at ~p","データーベーステーブル: ~p"}. {"Database Tables Configuration at ","データーベーステーブル設定 "}. {"Database","データーベース"}. {"December","12月"}. {"Default users as participants","デフォルトのユーザーは参加者"}. {"Delete content","内容を削除"}. {"Delete message of the day on all hosts","全ホストのお知らせメッセージを削除"}. {"Delete message of the day","お知らせメッセージを削除"}. {"Delete Selected","選択した項目を削除"}. {"Delete table","テーブルを削除"}. {"Delete User","ユーザーを削除"}. {"Deliver event notifications","イベント通知を配送する"}. {"Deliver payloads with event notifications","イベント通知と同時にペイロードを配送する"}. {"Description:","説明:"}. {"Disc only copy","ディスクだけのコピー"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","パスワードは誰にも(たとえ XMPP サーバーの管理者でも)教えないようにしてください。"}. {"Dump Backup to Text File at ","テキストファイルにバックアップ: "}. {"Dump to Text File","テキストファイルに出力"}. {"Duplicated groups are not allowed by RFC6121","RFC6121 によりグループの重複は許されません"}. {"Edit Properties","プロパティを編集"}. {"Either approve or decline the voice request.","発言権の要求を承認または却下します。"}. {"ejabberd HTTP Upload service","ejabberd HTTP ファイルアップロード"}. {"ejabberd MUC module","ejabberd MUCモジュール"}. {"ejabberd Multicast service","ejabberdマルチキャストサービス"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe モジュール"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams モジュール"}. {"ejabberd vCard module","ejabberd vCard モジュール"}. {"ejabberd Web Admin","ejabberd ウェブ管理"}. {"ejabberd","ejabberd"}. {"Elements","要素"}. {"Email Address","メールアドレス"}. {"Email","メール"}. {"Enable logging","ロギングを有効"}. {"Enable message archiving","メッセージアーカイブを有効化"}. {"End User Session","エンドユーザーセッション"}. {"Enter nickname you want to register","登録するニックネームを入力してください"}. {"Enter path to backup file","バックアップファイルのパスを入力してください"}. {"Enter path to jabberd14 spool dir","jabberd14 spool ディレクトリのディレクトリを入力してください"}. {"Enter path to jabberd14 spool file","jabberd14 spool ファイルのパスを入力してください"}. {"Enter path to text file","テキストファイルのパスを入力してください"}. {"Enter the text you see","見えているテキストを入力してください"}. {"Erlang XMPP Server","Erlang XMPP サーバー"}. {"Error","エラー"}. {"Exclude Jabber IDs from CAPTCHA challenge","CAPTCHA 入力を免除する Jabber ID"}. {"Export all tables as SQL queries to a file:","すべてのテーブルをSQL形式でファイルにエクスポート: "}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","サーバーにあるすべてのユーザーデータを PIEFXIS ファイルにエクスポート (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","ホストのユーザーデータを PIEFXIS ファイルにエクスポート (XEP-0227):"}. {"Failed to extract JID from your voice request approval","発言権要求の承認から JID を取り出すことに失敗しました"}. {"Failed to parse HTTP response","HTTP 応答のパースに失敗しました"}. {"Family Name","姓"}. {"February","2月"}. {"Fill in the form to search for any matching XMPP User","XMPP ユーザーを検索するには欄に入力してください"}. {"Friday","金曜日"}. {"From ~ts","From ~ts"}. {"From","差出人"}. {"Full List of Room Admins","チャットルーム管理者の一覧"}. {"Full List of Room Owners","チャットルーム主宰者の一覧"}. {"Full Name","氏名"}. {"Get Number of Online Users","オンラインユーザー数を取得"}. {"Get Number of Registered Users","登録ユーザー数を取得"}. {"Get User Last Login Time","最終ログイン時間を取得"}. {"Get User Password","パスワードを取得"}. {"Get User Statistics","ユーザー統計を取得"}. {"Given Name","名"}. {"Grant voice to this person?","この人に発言権を与えますか ?"}. {"Groups","グループ"}. {"Group","グループ"}. {"has been banned","はバンされました"}. {"has been kicked because of a system shutdown","はシステムシャットダウンのためキックされました"}. {"has been kicked because of an affiliation change","は分掌が変更されたためキックされました"}. {"has been kicked because the room has been changed to members-only","はチャットルームがメンバー制に変更されたためキックされました"}. {"has been kicked","はキックされました"}. {"Host unknown","不明なホスト"}. {"Host","ホスト"}. {"HTTP File Upload","HTTP ファイルアップロード"}. {"If you don't see the CAPTCHA image here, visit the web page.","ここに CAPTCHA 画像が表示されない場合、ウェブページを参照してください。"}. {"Import Directory","ディレクトリインポート"}. {"Import File","ファイルからインポート"}. {"Import user data from jabberd14 spool file:","ユーザーデータを jabberd14 Spool ファイルからインポート:"}. {"Import User from File at ","ファイルからユーザーをインポート: "}. {"Import users data from a PIEFXIS file (XEP-0227):","ユーザーデータを PIEFXIS ファイルからインポート (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","ユーザーデータを jabberd14 Spool ディレクトリからインポート:"}. {"Import Users from Dir at ","ディレクトリからユーザーをインポート: "}. {"Import Users From jabberd14 Spool Files","jabberd14 Spool ファイルからユーザーをインポート"}. {"Improper message type","誤ったメッセージタイプです"}. {"Incoming s2s Connections:","内向き s2s コネクション:"}. {"Incorrect data form","データ形式が違います"}. {"Incorrect password","パスワードが違います"}. {"Internal server error","内部サーバーエラー"}. {"Invalid node name","無効なノード名です"}. {"Invitations are not allowed in this conference","この会議では、招待はできません"}. {"IP addresses","IP アドレス"}. {"is now known as","は名前を変更しました: "}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","このルームにエラーメッセージを送ることは許可されていません。参加者(~s)はエラーメッセージを(~s)を送信してルームからキックされました。"}. {"It is not allowed to send private messages of type \"groupchat\"","種別が\"groupchat\" であるプライベートメッセージを送信することはできません"}. {"It is not allowed to send private messages to the conference","この会議にプライベートメッセージを送信することはできません"}. {"It is not allowed to send private messages","プライベートメッセージを送信することはできません"}. {"Jabber ID","Jabber ID"}. {"January","1月"}. {"joins the room","がチャットルームに参加しました"}. {"July","7月"}. {"June","6月"}. {"Just created","作成しました"}. {"Label:","ラベル:"}. {"Last Activity","活動履歴"}. {"Last login","最終ログイン"}. {"Last message","最終メッセージ"}. {"Last month","先月"}. {"Last year","去年"}. {"leaves the room","がチャットルームから退出しました"}. {"List of rooms","チャットルームの一覧"}. {"Low level update script","低レベル更新スクリプト"}. {"Make participants list public","参加者一覧を公開"}. {"Make room CAPTCHA protected","チャットルームを CAPTCHA で保護"}. {"Make room members-only","チャットルームをメンバーのみに制限"}. {"Make room moderated","チャットルームをモデレート化"}. {"Make room password protected","チャットルームをパスワードで保護"}. {"Make room persistent","チャットルームを永続化"}. {"Make room public searchable","チャットルームを検索可"}. {"Malformed username","不正な形式のユーザー名"}. {"March","3月"}. {"Max # of items to persist","アイテムの最大保存数"}. {"Max payload size in bytes","最大ぺイロードサイズ (byte)"}. {"Maximum file size","最大ファイルサイズ"}. {"Maximum Number of Occupants","最大在室者数"}. {"May","5月"}. {"Membership is required to enter this room","このチャットルームに入るにはメンバーでなければなりません"}. {"Members:","メンバー:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","パスワードは記憶するか、紙に書いて安全な場所に保管してください。もしあなたがパスワードを忘れてしまった場合、XMPP ではパスワードのリカバリを自動的に行うことはできません。"}. {"Memory","メモリ"}. {"Message body","本文"}. {"Middle Name","ミドルネーム"}. {"Minimum interval between voice requests (in seconds)","発言権の要求の最小時間間隔 (秒)"}. {"Moderator privileges required","モデレーター権限が必要です"}. {"Moderator","モデレーター"}. {"Modified modules","更新されたモジュール"}. {"Monday","月曜日"}. {"Multicast","マルチキャスト"}. {"Multi-User Chat","マルチユーザーチャット"}. {"Name","名"}. {"Name:","名前:"}. {"Natural-Language Room Name","自然言語での会議室名"}. {"Never","なし"}. {"New Password:","新しいパスワード:"}. {"Nickname can't be empty","ニックネームは空にできません"}. {"Nickname Registration at ","ニックネーム登録: "}. {"Nickname ~s does not exist in the room","ニックネーム ~s はこのチャットルームにいません"}. {"Nickname","ニックネーム"}. {"No body provided for announce message","アナウンスメッセージはありませんでした"}. {"No Data","データなし"}. {"No limit","制限なし"}. {"Node already exists","ノードは既に存在しています"}. {"Node ID","ノードID"}. {"Node not found","ノードが見つかりません"}. {"Node ~p","ノード ~p"}. {"Nodes","ノード"}. {"None","なし"}. {"Not Found","見つかりません"}. {"Notify subscribers when items are removed from the node","アイテムがノードから消された時に購読者へ通知する"}. {"Notify subscribers when the node configuration changes","ノード設定に変更があった時に購読者へ通知する"}. {"Notify subscribers when the node is deleted","ノードが削除された時に購読者へ通知する"}. {"November","11月"}. {"Number of occupants","在室者の数"}. {"Number of Offline Messages","オフラインメッセージ数"}. {"Number of online users","オンラインユーザー数"}. {"Number of registered users","登録ユーザー数"}. {"Occupants are allowed to invite others","在室者は誰かを招待することができます"}. {"Occupants May Change the Subject","ユーザーによる件名の変更を許可"}. {"October","10月"}. {"Offline Messages","オフラインメッセージ"}. {"Offline Messages:","オフラインメッセージ:"}. {"OK","OK"}. {"Old Password:","古いパスワード:"}. {"Online Users","オンラインユーザー"}. {"Online Users:","オンラインユーザー:"}. {"Online","オンライン"}. {"Only deliver notifications to available users","有効なユーザーにのみ告知を送信する"}. {"Only members may query archives of this room","メンバーのみがこのルームのアーカイブを取得できます"}. {"Only moderators and participants are allowed to change the subject in this room","モデレーターと参加者のみがチャットルームの件名を変更できます"}. {"Only moderators are allowed to change the subject in this room","モデレーターのみがチャットルームの件名を変更できます"}. {"Only moderators can approve voice requests","モデレーターだけが発言権の要求を承認できます"}. {"Only occupants are allowed to send messages to the conference","在室者のみがこの会議にメッセージを送ることができます"}. {"Only occupants are allowed to send queries to the conference","在室者のみが会議にクエリーを送信することができます"}. {"Only service administrators are allowed to send service messages","サービス管理者のみがサービスメッセージを送信できます"}. {"Organization Name","会社名"}. {"Organization Unit","部署名"}. {"Outgoing s2s Connections","外向き s2s コネクション"}. {"Outgoing s2s Connections:","外向き s2s コネクション:"}. {"Owner privileges required","主宰者の権限が必要です"}. {"Packet","パケット"}. {"Participant","参加者"}. {"Password Verification","パスワード (確認)"}. {"Password Verification:","パスワード (確認):"}. {"Password","パスワード"}. {"Password:","パスワード:"}. {"Path to Dir","ディレクトリのパス"}. {"Path to File","ファイルのパス"}. {"Pending","保留"}. {"Period: ","期間: "}. {"Persist items to storage","アイテムをストレージに保存する"}. {"Persistent","チャットルームを永続化"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","これらのオプションは組み込みの Mnesia データーベースのバックアップのみを行うことに注意してください。もし ODBC モジュールを使用している場合は、SQL データーベースのバックアップを別に行う必要があります。"}. {"Please, wait for a while before sending new voice request","新しい発言権の要求を送るまで少し間をおいてください"}. {"Pong","Pong"}. {"Present real Jabber IDs to","本当の Jabber ID を公開"}. {"private, ","プライベート、"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","PubSub 購読者のリクエスト"}. {"Purge all items when the relevant publisher goes offline","公開者がオフラインになるときに、すべてのアイテムを削除"}. {"Queries to the conference members are not allowed in this room","このチャットルームでは、会議のメンバーへのクエリーは禁止されています"}. {"RAM and disc copy","RAM, ディスクコピー"}. {"RAM copy","RAM コピー"}. {"Really delete message of the day?","本当にお知らせメッセージを削除しますか ?"}. {"Recipient is not in the conference room","受信者はこの会議室にいません"}. {"Register an XMPP account","XMPP アカウントを登録"}. {"Registered Users","登録ユーザー"}. {"Registered Users:","登録ユーザー:"}. {"Register","登録"}. {"Remote copy","リモートコピー"}. {"Remove All Offline Messages","すべてのオフラインメッセージを削除"}. {"Remove User","ユーザーを削除"}. {"Remove","削除"}. {"Replaced by new connection","新しいコネクションによって置き換えられました"}. {"Resources","リソース"}. {"Restart Service","サービスを再起動"}. {"Restart","再起動"}. {"Restore Backup from File at ","ファイルからバックアップをリストア: "}. {"Restore binary backup after next ejabberd restart (requires less memory):","ejabberd の再起動時にバイナリバックアップからリストア (メモリ少):"}. {"Restore binary backup immediately:","直ちにバイナリバックアップからリストア:"}. {"Restore plain text backup immediately:","直ちにプレーンテキストバックアップからリストア:"}. {"Restore","リストア"}. {"Roles for which Presence is Broadcasted","プレゼンスをブロードキャストするロール"}. {"Room Configuration","チャットルームの設定"}. {"Room creation is denied by service policy","サービスポリシーによってチャットルームの作成が禁止されています"}. {"Room description","チャットルームの説明"}. {"Room Occupants","在室者"}. {"Room title","チャットルームのタイトル"}. {"Roster groups allowed to subscribe","名簿グループは購読を許可しました"}. {"Roster of ~ts","~ts の名簿"}. {"Roster size","名簿サイズ"}. {"Roster:","名簿:"}. {"RPC Call Error","RPC 呼び出しエラー"}. {"Running Nodes","起動ノード"}. {"~s invites you to the room ~s","~s はあなたをチャットルーム ~s に招待しています"}. {"Saturday","土曜日"}. {"Script check","スクリプトチェック"}. {"Search Results for ","検索結果: "}. {"Search users in ","ユーザーの検索: "}. {"Select All","すべて選択"}. {"Send announcement to all online users on all hosts","全ホストのオンラインユーザーにアナウンスを送信"}. {"Send announcement to all online users","すべてのオンラインユーザーにアナウンスを送信"}. {"Send announcement to all users on all hosts","全ホストのユーザーにアナウンスを送信"}. {"Send announcement to all users","すべてのユーザーにアナウンスを送信"}. {"September","9月"}. {"Server:","サーバー:"}. {"Set message of the day and send to online users","お知らせメッセージを設定し、オンラインユーザーに送信"}. {"Set message of the day on all hosts and send to online users","全ホストのお知らせメッセージを設定し、オンラインユーザーに送信"}. {"Shared Roster Groups","共有名簿グループ"}. {"Show Integral Table","累積の表を表示"}. {"Show Ordinary Table","通常の表を表示"}. {"Shut Down Service","サービスを停止"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","XMPP クライアントはコンピューターにパスワードを記憶できます。コンピューターが安全であると信頼できる場合にのみ、この機能を使用してください。"}. {"Specify the access model","アクセスモデルを設定する"}. {"Specify the event message type","イベントメッセージ種別を設定"}. {"Specify the publisher model","公開モデルを指定する"}. {"Stanza ID","スタンザ ID"}. {"Statistics of ~p","~p の統計"}. {"Statistics","統計"}. {"Stopped Nodes","停止ノード"}. {"Stop","停止"}. {"Storage Type","ストレージタイプ"}. {"Store binary backup:","バイナリバックアップを保存:"}. {"Store plain text backup:","プレーンテキストバックアップを保存:"}. {"Subject","件名"}. {"Submitted","送信完了"}. {"Submit","送信"}. {"Subscriber Address","購読者のアドレス"}. {"Subscription","認可"}. {"Sunday","日曜日"}. {"That nickname is already in use by another occupant","そのニックネームは既にほかの在室者によって使用されています"}. {"That nickname is registered by another person","ニックネームはほかの人によって登録されています"}. {"The account already exists","アカウントは既に存在しています"}. {"The account was not unregistered","アカウントは削除されませんでした"}. {"The CAPTCHA is valid.","CAPTCHA は有効です。"}. {"The CAPTCHA verification has failed","CAPTCHA 検証は失敗しました"}. {"The captcha you entered is wrong","入力した CAPTCHA は間違っています"}. {"The collections with which a node is affiliated","提携されたノードの集合です"}. {"The default language of the node","ノードのデフォルトの言語"}. {"The JID of the node creator","ノード作成者の JID"}. {"The name of the node","ノード名"}. {"The password contains unacceptable characters","パスワードに使用できない文字が含まれています"}. {"The password is too weak","このパスワードは単純過ぎます"}. {"the password is","パスワードは"}. {"The password of your XMPP account was successfully changed.","XMPP アカウントのパスワード変更に成功しました。"}. {"The password was not changed","このパスワードは変更されませんでした"}. {"The passwords are different","このパスワードが違います"}. {"The username is not valid","ユーザー名が正しくありません"}. {"There was an error creating the account: ","アカウントの作成中にエラーが発生しました: "}. {"There was an error deleting the account: ","アカウントの削除中にエラーが発生しました: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","大文字と小文字は区別しません: macbeth は MacBeth や Macbeth と同じです。"}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","ここはこの XMPP サーバーにアカウントを作成するページです。あなたの JID (JabberID) は username@server のような形式になります。注意事項どおり、正しく項目を記入してください。"}. {"This page allows to unregister an XMPP account in this XMPP server.","このページはサーバー上のXMPPアカウントを削除するページです。"}. {"This room is not anonymous","このチャットルームは非匿名です"}. {"Thursday","木曜日"}. {"Time delay","遅延時間"}. {"Time","時間"}. {"Too many CAPTCHA requests","CAPTCHA 要求が多すぎます"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","~p回の認証に失敗しました。このIPアドレス(~s)は~s UTCまでブロックされます。"}. {"Too many unacked stanzas","多くのスタンザが応答していません"}. {"Total rooms","チャットルーム数"}. {"To","To"}. {"Traffic rate limit is exceeded","トラフィックレートの制限を超えました"}. {"Transactions Aborted:","トランザクションの失敗:"}. {"Transactions Committed:","トランザクションのコミット:"}. {"Transactions Logged:","トランザクションのログ: "}. {"Transactions Restarted:","トランザクションの再起動:"}. {"~ts's Offline Messages Queue","~ts のオフラインメッセージキュー"}. {"Tuesday","火曜日"}. {"Unable to generate a CAPTCHA","CAPTCHA を生成できません"}. {"Unauthorized","認証されていません"}. {"Unregister an XMPP account","XMPP アカウントを削除"}. {"Unregister","削除"}. {"Unsupported version","対応していないバージョン"}. {"Update message of the day (don't send)","お知らせメッセージを更新 (送信しない)"}. {"Update message of the day on all hosts (don't send)","全ホストのお知らせメッセージを更新 (送信しない)"}. {"Update plan","更新計画"}. {"Update ~p","更新 ~p"}. {"Update script","スクリプトの更新"}. {"Update","更新"}. {"Uptime:","起動時間:"}. {"User already exists","ユーザーは既に存在しています"}. {"User (jid)","ユーザー (JID)"}. {"User JID","ユーザー JID"}. {"User Management","ユーザー管理"}. {"User removed","ユーザーを削除しました"}. {"User ~ts","ユーザー ~ts"}. {"Username:","ユーザー名:"}. {"Users are not allowed to register accounts so quickly","それほど速くアカウントを登録することはできません"}. {"Users Last Activity","ユーザーの活動履歴"}. {"Users","ユーザー"}. {"User","ユーザー"}. {"Validate","検証"}. {"vCard User Search","vCard検索"}. {"View Queue","キューを表示"}. {"View Roster","名簿を表示"}. {"Virtual Hosts","バーチャルホスト"}. {"Visitors are not allowed to change their nicknames in this room","傍聴者はこのチャットルームでニックネームを変更することはできません"}. {"Visitors are not allowed to send messages to all occupants","傍聴者はすべての在室者にメッセージを送信することはできません"}. {"Visitor","傍聴者"}. {"Voice requests are disabled in this conference","この会議では、発言権の要求はできません"}. {"Voice request","発言権を要求"}. {"Wednesday","水曜日"}. {"When to send the last published item","最後の公開アイテムを送信するタイミングで"}. {"Whether to allow subscriptions","購読を許可するかどうか"}. {"XMPP Account Registration","XMPP アカウント登録"}. {"XMPP Domains","XMPP ドメイン"}. {"You are being removed from the room because of a system shutdown","システムシャットダウンのためチャットルームから削除されました"}. {"You can later change your password using an XMPP client.","あなたは後で XMPP クライアントを使用してパスワードを変更できます。"}. {"You have been banned from this room","あなたはこのチャットルームから締め出されています"}. {"You must fill in field \"Nickname\" in the form","フォームの\"ニックネーム\"欄を入力する必要があります"}. {"You need a client that supports x:data and CAPTCHA to register","登録を行うには x:data と CAPTCHA をサポートするクライアントが必要です"}. {"You need a client that supports x:data to register the nickname","ニックネームを登録するには x:data をサポートするクライアントが必要です"}. {"You need an x:data capable client to search","検索を行うためには x:data をサポートするクライアントが必要です"}. {"Your active privacy list has denied the routing of this stanza.","あなたのプライバシーリストはこのスタンザのルーティングを拒否しました。"}. {"Your contact offline message queue is full. The message has been discarded.","相手先のオフラインメッセージキューが一杯です。このメッセージは破棄されます。"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","~s 宛のメッセージはブロックされています。解除するにはこちらを見てください ~s"}. {"Your XMPP account was successfully registered.","XMPP アカウントの登録に成功しました。"}. {"Your XMPP account was successfully unregistered.","XMPP アカウントの削除に成功しました。"}. {"You're not allowed to create nodes","ノードを作成する権限がありません"}. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/de.msg���������������������������������������������������������������������0000644�0002322�0002322�00000137364�14154362354�016701� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Fügen Sie * am Ende des Feldes hinzu um nach Teilzeichenketten zu suchen)"}. {" has set the subject to: "," hat das Thema geändert auf: "}. {"# participants","# Teilnehmer"}. {"A description of the node","Eine Beschreibung des Knotens"}. {"A friendly name for the node","Ein benutzerfreundlicher Name für den Knoten"}. {"A password is required to enter this room","Ein Passwort ist erforderlich um diesen Raum zu betreten"}. {"A Web Page","Eine Webseite"}. {"Accept","Akzeptieren"}. {"Access denied by service policy","Zugriff aufgrund der Dienstrichtlinien verweigert"}. {"Access model of authorize","Zugriffsmodell von 'authorize'"}. {"Access model of open","Zugriffsmodell von 'open'"}. {"Access model of presence","Zugriffsmodell von 'presence'"}. {"Access model of roster","Zugriffsmodell der Kontaktliste"}. {"Access model of whitelist","Zugriffsmodell von 'whitelist'"}. {"Access model","Zugriffsmodell"}. {"Account doesn't exist","Konto existiert nicht"}. {"Action on user","Aktion auf Benutzer"}. {"Add Jabber ID","Jabber-ID hinzufügen"}. {"Add New","Neue(n) hinzufügen"}. {"Add User","Benutzer hinzufügen"}. {"Administration of ","Administration von "}. {"Administration","Verwaltung"}. {"Administrator privileges required","Administratorrechte erforderlich"}. {"All activity","Alle Aktivitäten"}. {"All Users","Alle Benutzer"}. {"Allow subscription","Abonnement erlauben"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Dieser Jabber-ID das Abonnement dieses pubsub-Knotens erlauben?"}. {"Allow this person to register with the room?","Dieser Person erlauben, sich beim Raum anzumelden?"}. {"Allow users to change the subject","Erlaube Benutzern das Thema zu ändern"}. {"Allow users to query other users","Erlaube Benutzern Informationen über andere Benutzer abzufragen"}. {"Allow users to send invites","Erlaube Benutzern Einladungen zu senden"}. {"Allow users to send private messages","Erlaube Benutzern private Nachrichten zu senden"}. {"Allow visitors to change nickname","Erlaube Besuchern ihren Benutzernamen zu ändern"}. {"Allow visitors to send private messages to","Erlaube Besuchern das Senden von privaten Nachrichten an"}. {"Allow visitors to send status text in presence updates","Erlaube Besuchern einen Statustext bei Präsenzupdates zu senden"}. {"Allow visitors to send voice requests","Erlaube Besuchern Sprachrecht-Anforderungen zu senden"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Eine zugehörige LDAP-Gruppe die Raummitgliedschaft definiert; dies sollte ein 'LDAP Distinguished Name' gemäß einer implementierungs- oder bereitstellungsspezifischen Definition einer Gruppe sein."}. {"Announcements","Ankündigungen"}. {"Answer associated with a picture","Antwort verbunden mit einem Bild"}. {"Answer associated with a video","Antwort verbunden mit einem Video"}. {"Answer associated with speech","Antwort verbunden mit Sprache"}. {"Answer to a question","Antwort auf eine Frage"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Jeder in der/den angeführten Kontaktlistengruppe(n) darf Items abonnieren und abrufen"}. {"Anyone may associate leaf nodes with the collection","Jeder darf Blattknoten mit der Sammlung verknüpfen"}. {"Anyone may publish","Jeder darf veröffentlichen"}. {"Anyone may subscribe and retrieve items","Jeder darf Items abonnieren und abrufen"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Jeder mit einem Präsenzabonnement von beiden oder davon darf Items abonnieren oder abrufen"}. {"Anyone with Voice","Jeder mit Stimme"}. {"Anyone","Jeder"}. {"April","April"}. {"Attribute 'channel' is required for this request","Attribut 'channel' ist für diese Anforderung erforderlich"}. {"Attribute 'id' is mandatory for MIX messages","Attribut 'id' ist verpflichtend für MIX-Nachrichten"}. {"Attribute 'jid' is not allowed here","Attribut 'jid' ist hier nicht erlaubt"}. {"Attribute 'node' is not allowed here","Attribut 'node' ist hier nicht erlaubt"}. {"Attribute 'to' of stanza that triggered challenge","Attribut 'to' des Stanza das die Herausforderung ausgelöst hat"}. {"August","August"}. {"Automatic node creation is not enabled","Automatische Knotenerstellung ist nicht aktiviert"}. {"Backup Management","Backupverwaltung"}. {"Backup of ~p","Backup von ~p"}. {"Backup to File at ","Backup in Datei bei "}. {"Backup","Backup"}. {"Bad format","Ungültiges Format"}. {"Birthday","Geburtsdatum"}. {"Both the username and the resource are required","Sowohl der Benutzername als auch die Ressource sind erforderlich"}. {"Bytestream already activated","Bytestream bereits aktiviert"}. {"Cannot remove active list","Kann aktive Liste nicht entfernen"}. {"Cannot remove default list","Kann Standardliste nicht entfernen"}. {"CAPTCHA web page","CAPTCHA -Webseite"}. {"Challenge ID","Herausforderungs-ID"}. {"Change Password","Passwort ändern"}. {"Change User Password","Benutzerpasswort ändern"}. {"Changing password is not allowed","Ändern des Passwortes ist nicht erlaubt"}. {"Changing role/affiliation is not allowed","Ändern der Rolle/Zugehörigkeit ist nicht erlaubt"}. {"Channel already exists","Kanal existiert bereits"}. {"Channel does not exist","Kanal existiert nicht"}. {"Channels","Kanäle"}. {"Characters not allowed:","Nicht erlaubte Zeichen:"}. {"Chatroom configuration modified","Chatraum-Konfiguration geändert"}. {"Chatroom is created","Chatraum ist erstellt"}. {"Chatroom is destroyed","Chatraum ist entfernt"}. {"Chatroom is started","Chatraum ist gestartet"}. {"Chatroom is stopped","Chatraum ist beendet"}. {"Chatrooms","Chaträume"}. {"Choose a username and password to register with this server","Wählen Sie zum Registrieren auf diesem Server einen Benutzernamen und ein Passwort"}. {"Choose storage type of tables","Wähle Speichertyp der Tabellen"}. {"Choose whether to approve this entity's subscription.","Wählen Sie, ob das Abonnement dieser Entität genehmigt werden soll."}. {"City","Stadt"}. {"Client acknowledged more stanzas than sent by server","Client bestätigte mehr Stanzas als vom Server gesendet"}. {"Commands","Befehle"}. {"Conference room does not exist","Konferenzraum existiert nicht"}. {"Configuration of room ~s","Konfiguration des Raumes ~s"}. {"Configuration","Konfiguration"}. {"Connected Resources:","Verbundene Ressourcen:"}. {"Contact Addresses (normally, room owner or owners)","Kontaktadresse (normalerweise Raumbesitzer)"}. {"Country","Land"}. {"CPU Time:","CPU-Zeit:"}. {"Current Discussion Topic","Aktuelles Diskussionsthema"}. {"Database failure","Datenbankfehler"}. {"Database Tables at ~p","Datenbanktabellen bei ~p"}. {"Database Tables Configuration at ","Datenbanktabellen-Konfiguration bei "}. {"Database","Datenbank"}. {"December","Dezember"}. {"Default users as participants","Benutzer werden standardmäßig Teilnehmer"}. {"Delete content","Inhalt löschen"}. {"Delete message of the day on all hosts","Lösche Nachricht des Tages auf allen Hosts"}. {"Delete message of the day","Lösche Nachricht des Tages"}. {"Delete Selected","Markierte löschen"}. {"Delete table","Tabelle löschen"}. {"Delete User","Benutzer löschen"}. {"Deliver event notifications","Ereignisbenachrichtigungen zustellen"}. {"Deliver payloads with event notifications","Nutzdaten mit Ereignisbenachrichtigungen zustellen"}. {"Description:","Beschreibung:"}. {"Disc only copy","Nur auf Festplatte"}. {"'Displayed groups' not added (they do not exist!): ","'Angezeigte Gruppen' nicht hinzugefügt (sie existieren nicht!): "}. {"Displayed:","Angezeigt:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Geben Sie niemandem Ihr Passwort, auch nicht den Administratoren des XMPP-Servers."}. {"Dump Backup to Text File at ","Gib Backup in Textdatei aus bei "}. {"Dump to Text File","Ausgabe in Textdatei"}. {"Duplicated groups are not allowed by RFC6121","Doppelte Gruppen sind laut RFC6121 nicht erlaubt"}. {"Dynamically specify a replyto of the item publisher","Dynamisch ein 'replyto' des Item-Veröffentlichers angeben"}. {"Edit Properties","Eigenschaften ändern"}. {"Either approve or decline the voice request.","Sprachrecht-Anforderung entweder genehmigen oder ablehnen."}. {"ejabberd HTTP Upload service","ejabberd HTTP Upload-Dienst"}. {"ejabberd MUC module","ejabberd MUC-Modul"}. {"ejabberd Multicast service","ejabberd Multicast-Dienst"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe-Modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5-Bytestreams-Modul"}. {"ejabberd vCard module","ejabberd vCard-Modul"}. {"ejabberd Web Admin","ejabberd Web-Admin"}. {"ejabberd","ejabberd"}. {"Elements","Elemente"}. {"Email Address","E-Mail-Adresse"}. {"Email","E-Mail"}. {"Enable logging","Protokollierung aktivieren"}. {"Enable message archiving","Nachrichtenarchivierung aktivieren"}. {"Enabling push without 'node' attribute is not supported","push ohne 'node'-Attribut zu aktivieren wird nicht unterstützt"}. {"End User Session","Benutzersitzung beenden"}. {"Enter nickname you want to register","Geben Sie den Spitznamen ein den Sie registrieren wollen"}. {"Enter path to backup file","Geben Sie den Pfad zur Backupdatei ein"}. {"Enter path to jabberd14 spool dir","Geben Sie den Pfad zum jabberd14-Spoolverzeichnis ein"}. {"Enter path to jabberd14 spool file","Geben Sie den Pfad zur jabberd14-Spooldatei ein"}. {"Enter path to text file","Geben Sie den Pfad zur Textdatei ein"}. {"Enter the text you see","Geben Sie den Text ein den Sie sehen"}. {"Erlang XMPP Server","Erlang XMPP-Server"}. {"Error","Fehler"}. {"Exclude Jabber IDs from CAPTCHA challenge","Jabber-IDs von CAPTCHA-Herausforderung ausschließen"}. {"Export all tables as SQL queries to a file:","Alle Tabellen als SQL-Abfragen in eine Datei exportieren:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Servers in PIEFXIS-Dateien (XEP-0227) exportieren:"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Alle Benutzerdaten des Hosts in PIEFXIS-Dateien (XEP-0227) exportieren:"}. {"External component failure","Fehler externer Komponente"}. {"External component timeout","Zeitüberschreitung externer Komponente"}. {"Failed to activate bytestream","Konnte Bytestream nicht aktivieren"}. {"Failed to extract JID from your voice request approval","Konnte JID nicht aus Ihrer Genehmigung der Sprachrecht-Anforderung extrahieren"}. {"Failed to map delegated namespace to external component","Konnte delegierten Namensraum nicht externer Komponente zuordnen"}. {"Failed to parse HTTP response","Konnte HTTP-Antwort nicht parsen"}. {"Failed to process option '~s'","Konnte Option '~s' nicht verarbeiten"}. {"Family Name","Nachname"}. {"FAQ Entry","FAQ-Eintrag"}. {"February","Februar"}. {"File larger than ~w bytes","Datei größer als ~w Bytes"}. {"Fill in the form to search for any matching XMPP User","Füllen Sie das Formular aus, um nach jeglichen passenden XMPP-Benutzern zu suchen"}. {"Friday","Freitag"}. {"From ~ts","Von ~ts"}. {"From","Von"}. {"Full List of Room Admins","Vollständige Liste der Raumadmins"}. {"Full List of Room Owners","Vollständige Liste der Raumbesitzer"}. {"Full Name","Vollständiger Name"}. {"Get Number of Online Users","Anzahl der angemeldeten Benutzer abrufen"}. {"Get Number of Registered Users","Anzahl der registrierten Benutzer abrufen"}. {"Get Pending","Ausstehende abrufen"}. {"Get User Last Login Time","letzte Anmeldezeit des Benutzers abrufen"}. {"Get User Password","Benutzerpasswort abrufen"}. {"Get User Statistics","Benutzerstatistiken abrufen"}. {"Given Name","Vorname"}. {"Grant voice to this person?","Dieser Person Sprachrechte erteilen?"}. {"Group","Gruppe"}. {"Groups that will be displayed to the members","Gruppen, die den Mitgliedern angezeigt werden"}. {"Groups","Gruppen"}. {"has been banned","wurde gebannt"}. {"has been kicked because of a system shutdown","wurde wegen einer Systemabschaltung hinausgeworfen"}. {"has been kicked because of an affiliation change","wurde wegen einer Änderung der Zugehörigkeit hinausgeworfen"}. {"has been kicked because the room has been changed to members-only","wurde hinausgeworfen weil der Raum zu Nur-Mitglieder geändert wurde"}. {"has been kicked","wurde hinausgeworfen"}. {"Host unknown","Host unbekannt"}. {"Host","Host"}. {"HTTP File Upload","HTTP-Dateiupload"}. {"Idle connection","Inaktive Verbindung"}. {"If you don't see the CAPTCHA image here, visit the web page.","Wenn Sie das CAPTCHA-Bild nicht sehen, besuchen Sie die Webseite."}. {"Import Directory","Verzeichnis importieren"}. {"Import File","Datei importieren"}. {"Import user data from jabberd14 spool file:","Importiere Benutzer von jabberd14-Spooldatei:"}. {"Import User from File at ","Benutzer importieren aus Datei bei "}. {"Import users data from a PIEFXIS file (XEP-0227):","Benutzerdaten von einer PIEFXIS-Datei (XEP-0227) importieren:"}. {"Import users data from jabberd14 spool directory:","Importiere Benutzer von jabberd14-Spoolverzeichnis:"}. {"Import Users from Dir at ","Benutzer importieren aus Verzeichnis bei "}. {"Import Users From jabberd14 Spool Files","Importiere Benutzer aus jabberd14-Spooldateien"}. {"Improper domain part of 'from' attribute","Falscher Domänenteil des 'from'-Attributs"}. {"Improper message type","Unzulässiger Nachrichtentyp"}. {"Incoming s2s Connections:","Eingehende s2s-Verbindungen:"}. {"Incorrect CAPTCHA submit","Falsche CAPTCHA-Eingabe"}. {"Incorrect data form","Falsches Datenformular"}. {"Incorrect password","Falsches Passwort"}. {"Incorrect value of 'action' attribute","Falscher Wert des 'action'-Attributs"}. {"Incorrect value of 'action' in data form","Falscher Wert von 'action' in Datenformular"}. {"Incorrect value of 'path' in data form","Falscher Wert von 'path' in Datenformular"}. {"Insufficient privilege","Unzureichende Privilegien"}. {"Internal server error","Interner Serverfehler"}. {"Invalid 'from' attribute in forwarded message","Ungültiges 'from'-Attribut in weitergeleiteter Nachricht"}. {"Invalid node name","Ungültiger Knotenname"}. {"Invalid 'previd' value","Ungültiger 'previd'-Wert"}. {"Invitations are not allowed in this conference","Einladungen sind in dieser Konferenz nicht erlaubt"}. {"IP addresses","IP-Adressen"}. {"is now known as","ist nun bekannt als"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Es ist nicht erlaubt Fehlermeldungen an den Raum zu senden. Der Teilnehmer (~s) hat eine Fehlermeldung (~s) gesendet und wurde aus dem Raum geworfen"}. {"It is not allowed to send private messages of type \"groupchat\"","Es ist nicht erlaubt private Nachrichten des Typs \"groupchat\" zu senden"}. {"It is not allowed to send private messages to the conference","Es ist nicht erlaubt private Nachrichten an die Konferenz zu senden"}. {"It is not allowed to send private messages","Es ist nicht erlaubt private Nachrichten zu senden"}. {"Jabber ID","Jabber-ID"}. {"January","Januar"}. {"JID normalization denied by service policy","JID-Normalisierung aufgrund der Dienstrichtlinien verweigert"}. {"JID normalization failed","JID-Normalisierung fehlgeschlagen"}. {"joins the room","betritt den Raum"}. {"July","Juli"}. {"June","Juni"}. {"Just created","Gerade erstellt"}. {"Label:","Label:"}. {"Last Activity","Letzte Aktivität"}. {"Last login","Letzte Anmeldung"}. {"Last message","Letzte Nachricht"}. {"Last month","Letzter Monat"}. {"Last year","Letztes Jahr"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Niederwertigstes Bit des SHA-256-Hashes des Textes sollte hexadezimalem Label gleichen"}. {"leaves the room","verlässt den Raum"}. {"List of rooms","Liste von Räumen"}. {"Logging","Protokollierung"}. {"Low level update script","Low-Level-Aktualisierungsscript"}. {"Make participants list public","Teilnehmerliste öffentlich machen"}. {"Make room CAPTCHA protected","Raum mittels CAPTCHA schützen"}. {"Make room members-only","Raum nur für Mitglieder zugänglich machen"}. {"Make room moderated","Raum moderiert machen"}. {"Make room password protected","Raum mit Passwort schützen"}. {"Make room persistent","Raum persistent machen"}. {"Make room public searchable","Raum öffentlich suchbar machen"}. {"Malformed username","Ungültiger Benutzername"}. {"MAM preference modification denied by service policy","Modifikation der MAM-Präferenzen aufgrund der Dienstrichtlinien verweigert"}. {"March","März"}. {"Max # of items to persist","Maximale Anzahl persistenter Items"}. {"Max payload size in bytes","Maximale Nutzdatengröße in Bytes"}. {"Maximum file size","Maximale Dateigröße"}. {"Maximum Number of History Messages Returned by Room","Maximale Anzahl der vom Raum zurückgegebenen History-Nachrichten"}. {"Maximum number of items to persist","Maximale Anzahl persistenter Items"}. {"Maximum Number of Occupants","Maximale Anzahl der Teilnehmer"}. {"May","Mai"}. {"Members not added (inexistent vhost!): ","Mitglieder nicht hinzugefügt (nicht existierender vhost!): "}. {"Membership is required to enter this room","Mitgliedschaft ist erforderlich um diesen Raum zu betreten"}. {"Members:","Mitglieder:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Merken Sie sich Ihr Passwort, oder schreiben Sie es auf einen Zettel den Sie sicher verwahren. Bei XMPP gibt es keine automatische Möglichkeit, das Passwort wiederherzustellen falls Sie es vergessen."}. {"Memory","Speicher"}. {"Mere Availability in XMPP (No Show Value)","Bloße Verfügbarkeit in XMPP (kein Anzeigewert)"}. {"Message body","Nachrichtentext"}. {"Message not found in forwarded payload","Nachricht nicht in weitergeleiteten Nutzdaten gefunden"}. {"Messages from strangers are rejected","Nachrichten von Fremden werden zurückgewiesen"}. {"Messages of type headline","Nachrichten vom Typ 'headline'"}. {"Messages of type normal","Nachrichten vom Typ 'normal'"}. {"Middle Name","Zweiter Vorname"}. {"Minimum interval between voice requests (in seconds)","Mindestdauer zwischen Sprachrecht-Anforderung (in Sekunden)"}. {"Moderator privileges required","Moderatorrechte erforderlich"}. {"Moderator","Moderator"}. {"Moderators Only","nur Moderatoren"}. {"Modified modules","Geänderte Module"}. {"Module failed to handle the query","Modul konnte die Anfrage nicht verarbeiten"}. {"Monday","Montag"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","Mehrere <item/>-Elemente sind laut RFC6121 nicht erlaubt"}. {"Multi-User Chat","Mehrbenutzer-Chat (MUC)"}. {"Name in the rosters where this group will be displayed","Name in den Kontaktlisten wo diese Gruppe angezeigt werden wird"}. {"Name:","Name:"}. {"Name","Vorname"}. {"Natural Language for Room Discussions","Natürliche Sprache für Raumdiskussionen"}. {"Natural-Language Room Name","Raumname in natürlicher Sprache"}. {"Neither 'jid' nor 'nick' attribute found","Weder 'jid'- noch 'nick'-Attribut gefunden"}. {"Neither 'role' nor 'affiliation' attribute found","Weder 'role'- noch 'affiliation'-Attribut gefunden"}. {"Never","Nie"}. {"New Password:","Neues Passwort:"}. {"Nickname can't be empty","Spitzname darf nicht leer sein"}. {"Nickname Registration at ","Registrieren des Spitznamens auf "}. {"Nickname ~s does not exist in the room","Der Spitzname ~s existiert nicht im Raum"}. {"Nickname","Spitzname"}. {"No address elements found","Keine 'address'-Elemente gefunden"}. {"No addresses element found","Kein 'addresses'-Element gefunden"}. {"No 'affiliation' attribute found","Kein 'affiliation'-Attribut gefunden"}. {"No available resource found","Keine verfügbare Ressource gefunden"}. {"No body provided for announce message","Kein Text für die Ankündigungsnachricht angegeben"}. {"No child elements found","Keine 'child'-Elemente gefunden"}. {"No data form found","Kein Datenformular gefunden"}. {"No Data","Keine Daten"}. {"No features available","Keine Eigenschaften verfügbar"}. {"No <forwarded/> element found","Kein <forwarded/>-Element gefunden"}. {"No hook has processed this command","Kein Hook hat diesen Befehl verarbeitet"}. {"No info about last activity found","Keine Informationen über letzte Aktivität gefunden"}. {"No 'item' element found","Kein 'item'-Element gefunden"}. {"No items found in this query","Keine Items in dieser Anfrage gefunden"}. {"No limit","Keine Begrenzung"}. {"No module is handling this query","Kein Modul verarbeitet diese Anfrage"}. {"No node specified","Kein Knoten angegeben"}. {"No 'password' found in data form","Kein 'password' im Datenformular gefunden"}. {"No 'password' found in this query","Kein 'password' in dieser Anfrage gefunden"}. {"No 'path' found in data form","Kein 'path' im Datenformular gefunden"}. {"No pending subscriptions found","Keine ausstehenden Abonnements gefunden"}. {"No privacy list with this name found","Keine Privacy-Liste mit diesem Namen gefunden"}. {"No private data found in this query","Keine privaten Daten in dieser Anfrage gefunden"}. {"No running node found","Kein laufender Knoten gefunden"}. {"No services available","Keine Dienste verfügbar"}. {"No statistics found for this item","Keine Statistiken für dieses Item gefunden"}. {"No 'to' attribute found in the invitation","Kein 'to'-Attribut in der Einladung gefunden"}. {"Nobody","Niemand"}. {"Node already exists","Knoten existiert bereits"}. {"Node ID","Knoten-ID"}. {"Node index not found","Knotenindex nicht gefunden"}. {"Node not found","Knoten nicht gefunden"}. {"Node ~p","Knoten ~p"}. {"Node","Knoten"}. {"Nodeprep has failed","Nodeprep fehlgeschlagen"}. {"Nodes","Knoten"}. {"None","Keine"}. {"Not allowed","Nicht erlaubt"}. {"Not Found","Nicht gefunden"}. {"Not subscribed","Nicht abonniert"}. {"Notify subscribers when items are removed from the node","Abonnenten benachrichtigen, wenn Items vom Knoten entfernt werden"}. {"Notify subscribers when the node configuration changes","Abonnenten benachrichtigen, wenn sich die Knotenkonfiguration ändert"}. {"Notify subscribers when the node is deleted","Abonnenten benachrichtigen, wenn der Knoten gelöscht wird"}. {"November","November"}. {"Number of answers required","Anzahl der erforderlichen Antworten"}. {"Number of occupants","Anzahl der Teilnehmer"}. {"Number of Offline Messages","Anzahl der Offline-Nachrichten"}. {"Number of online users","Anzahl der angemeldeten Benutzer"}. {"Number of registered users","Anzahl der registrierten Benutzer"}. {"Number of seconds after which to automatically purge items","Anzahl der Sekunden, nach der Items automatisch gelöscht werden"}. {"Occupants are allowed to invite others","Teilnehmer dürfen andere einladen"}. {"Occupants May Change the Subject","Teilnehmer dürfen das Thema ändern"}. {"October","Oktober"}. {"Offline Messages","Offline-Nachrichten"}. {"Offline Messages:","Offline-Nachrichten:"}. {"OK","OK"}. {"Old Password:","Altes Passwort:"}. {"Online Users","Angemeldete Benutzer"}. {"Online Users:","Angemeldete Benutzer:"}. {"Online","Angemeldet"}. {"Only admins can see this","Nur Admins können dies sehen"}. {"Only collection node owners may associate leaf nodes with the collection","Nur Sammlungsknoten-Besitzer dürfen Blattknoten mit der Sammlung verknüpfen"}. {"Only deliver notifications to available users","Benachrichtigungen nur an verfügbare Benutzer schicken"}. {"Only <enable/> or <disable/> tags are allowed","Nur <enable/>- oder <disable/>-Tags sind erlaubt"}. {"Only <list/> element is allowed in this query","Nur <list/>-Elemente sind in dieser Anfrage erlaubt"}. {"Only members may query archives of this room","Nur Mitglieder dürfen den Verlauf dieses Raumes abrufen"}. {"Only moderators and participants are allowed to change the subject in this room","Nur Moderatoren und Teilnehmer dürfen das Thema in diesem Raum ändern"}. {"Only moderators are allowed to change the subject in this room","Nur Moderatoren dürfen das Thema in diesem Raum ändern"}. {"Only moderators can approve voice requests","Nur Moderatoren können Sprachrecht-Anforderungen genehmigen"}. {"Only occupants are allowed to send messages to the conference","Nur Teilnehmer dürfen Nachrichten an die Konferenz senden"}. {"Only occupants are allowed to send queries to the conference","Nur Teilnehmer dürfen Anfragen an die Konferenz senden"}. {"Only publishers may publish","Nur Veröffentlicher dürfen veröffentlichen"}. {"Only service administrators are allowed to send service messages","Nur Service-Administratoren dürfen Servicenachrichten senden"}. {"Only those on a whitelist may associate leaf nodes with the collection","Nur jemand auf einer Whitelist darf Blattknoten mit der Sammlung verknüpfen"}. {"Only those on a whitelist may subscribe and retrieve items","Nur jemand auf einer Whitelist darf Items abonnieren und abrufen"}. {"Organization Name","Name der Organisation"}. {"Organization Unit","Abteilung"}. {"Outgoing s2s Connections","Ausgehende s2s-Verbindungen"}. {"Outgoing s2s Connections:","Ausgehende s2s-Verbindungen:"}. {"Owner privileges required","Besitzerrechte erforderlich"}. {"Packet relay is denied by service policy","Paket-Relay aufgrund der Dienstrichtlinien verweigert"}. {"Packet","Paket"}. {"Participant","Teilnehmer"}. {"Password Verification","Passwort bestätigen"}. {"Password Verification:","Passwort bestätigen:"}. {"Password","Passwort"}. {"Password:","Passwort:"}. {"Path to Dir","Pfad zum Verzeichnis"}. {"Path to File","Pfad zur Datei"}. {"Payload type","Nutzdatentyp"}. {"Pending","Ausstehend"}. {"Period: ","Zeitraum: "}. {"Persist items to storage","Items dauerhaft speichern"}. {"Persistent","Persistent"}. {"Ping query is incorrect","Ping-Anfrage ist falsch"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Beachten Sie, dass diese Optionen nur die eingebaute Mnesia-Datenbank sichern. Wenn Sie das ODBC-Modul verwenden, müssen Sie auch Ihre SQL-Datenbank separat sichern."}. {"Please, wait for a while before sending new voice request","Bitte warten Sie ein wenig, bevor Sie eine weitere Sprachrecht-Anforderung senden"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Ein 'ask'-Attribut zu besitzen ist laut RFC6121 nicht erlaubt"}. {"Present real Jabber IDs to","Echte Jabber-IDs anzeigen für"}. {"Previous session not found","Vorherige Sitzung nicht gefunden"}. {"Previous session PID has been killed","Vorherige Sitzungs-PID wurde getötet"}. {"Previous session PID has exited","Vorherige Sitzungs-PID wurde beendet"}. {"Previous session PID is dead","Vorherige Sitzungs-PID ist tot"}. {"Previous session timed out","Zeitüberschreitung bei vorheriger Sitzung"}. {"private, ","privat, "}. {"Public","Öffentlich"}. {"Publish model","Veröffentlichungsmodell"}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","PubSub-Abonnenten-Anforderung"}. {"Purge all items when the relevant publisher goes offline","Alle Items löschen, wenn der relevante Veröffentlicher offline geht"}. {"Push record not found","Push-Eintrag nicht gefunden"}. {"Queries to the conference members are not allowed in this room","Anfragen an die Konferenzteilnehmer sind in diesem Raum nicht erlaubt"}. {"Query to another users is forbidden","Anfrage an andere Benutzer ist verboten"}. {"RAM and disc copy","RAM und Festplatte"}. {"RAM copy","Nur RAM"}. {"Really delete message of the day?","Nachricht des Tages wirklich löschen?"}. {"Receive notification from all descendent nodes","Benachrichtigung von allen abstammenden Nodes erhalten"}. {"Receive notification from direct child nodes only","Benachrichtigung nur von direkten Kindknoten erhalten"}. {"Receive notification of new items only","Benachrichtigung nur von neuen Items erhalten"}. {"Receive notification of new nodes only","Benachrichtigung nur von neuen Knoten erhalten"}. {"Recipient is not in the conference room","Empfänger ist nicht im Konferenzraum"}. {"Register an XMPP account","Ein XMPP-Konto registrieren"}. {"Register","Anmelden"}. {"Registered Users","Registrierte Benutzer"}. {"Registered Users:","Registrierte Benutzer:"}. {"Remote copy","Fernkopie"}. {"Remove All Offline Messages","Alle Offline-Nachrichten löschen"}. {"Remove User","Benutzer löschen"}. {"Remove","Entfernen"}. {"Replaced by new connection","Durch neue Verbindung ersetzt"}. {"Request has timed out","Zeitüberschreitung bei Anforderung"}. {"Request is ignored","Anforderung wird ignoriert"}. {"Requested role","Angeforderte Rolle"}. {"Resources","Ressourcen"}. {"Restart Service","Dienst neustarten"}. {"Restart","Neustart"}. {"Restore Backup from File at ","Backup wiederherstellen aus Datei bei "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Stelle binäres Backup beim nächsten ejabberd-Neustart wieder her (benötigt weniger Speicher):"}. {"Restore binary backup immediately:","Stelle binäres Backup sofort wieder her:"}. {"Restore plain text backup immediately:","Stelle Klartext-Backup sofort wieder her:"}. {"Restore","Wiederherstellung"}. {"Roles and Affiliations that May Retrieve Member List","Rollen und Zugehörigkeiten die Mitgliederliste abrufen dürfen"}. {"Roles for which Presence is Broadcasted","Rollen für welche die Präsenz übertragen wird"}. {"Roles that May Send Private Messages","Rollen die Privatnachrichten senden dürfen"}. {"Room Configuration","Raum-Konfiguration"}. {"Room creation is denied by service policy","Anlegen des Raumes aufgrund der Dienstrichtlinien verweigert"}. {"Room description","Raumbeschreibung"}. {"Room Occupants","Raumteilnehmer"}. {"Room terminates","Raum wird beendet"}. {"Room title","Raumname"}. {"Roster groups allowed to subscribe","Kontaktlistengruppen die abonnieren dürfen"}. {"Roster of ~ts","Kontaktliste von ~ts"}. {"Roster size","Kontaktlistengröße"}. {"Roster:","Kontaktliste:"}. {"RPC Call Error","Fehler bei RPC-Aufruf"}. {"Running Nodes","Laufende Knoten"}. {"~s invites you to the room ~s","~s lädt Sie in den Raum ~s ein"}. {"Saturday","Samstag"}. {"Script check","Script-Überprüfung"}. {"Search from the date","Suche ab Datum"}. {"Search Results for ","Suchergebnisse für "}. {"Search the text","Text durchsuchen"}. {"Search until the date","Suche bis Datum"}. {"Search users in ","Suche Benutzer in "}. {"Select All","Alles auswählen"}. {"Send announcement to all online users on all hosts","Ankündigung an alle angemeldeten Benutzer auf allen Hosts senden"}. {"Send announcement to all online users","Ankündigung an alle angemeldeten Benutzer senden"}. {"Send announcement to all users on all hosts","Ankündigung an alle Benutzer auf allen Hosts senden"}. {"Send announcement to all users","Ankündigung an alle Benutzer senden"}. {"September","September"}. {"Server:","Server:"}. {"Service list retrieval timed out","Zeitüberschreitung bei Abfrage der Serviceliste"}. {"Session state copying timed out","Zeitüberschreitung beim Kopieren des Sitzungszustandes"}. {"Set message of the day and send to online users","Nachricht des Tages setzen und an alle angemeldeten Benutzer senden"}. {"Set message of the day on all hosts and send to online users","Nachricht des Tages auf allen Hosts setzen und an alle angemeldeten Benutzer senden"}. {"Shared Roster Groups","Gruppen der gemeinsamen Kontaktliste"}. {"Show Integral Table","Integral-Tabelle anzeigen"}. {"Show Ordinary Table","Gewöhnliche Tabelle anzeigen"}. {"Shut Down Service","Dienst herunterfahren"}. {"SOCKS5 Bytestreams","SOCKS5-Bytestreams"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Einige XMPP-Clients speichern Ihr Passwort auf dem Computer. Aus Sicherheitsgründen sollten Sie das nur auf Ihrem persönlichen Computer tun."}. {"Specify the access model","Geben Sie das Zugangsmodell an"}. {"Specify the event message type","Geben Sie den Ereignisnachrichtentyp an"}. {"Specify the publisher model","Geben Sie das Veröffentlichermodell an"}. {"Stanza ID","Stanza-ID"}. {"Statically specify a replyto of the node owner(s)","Ein 'replyto' des/der Nodebesitzer(s) statisch angeben"}. {"Statistics of ~p","Statistiken von ~p"}. {"Statistics","Statistiken"}. {"Stop","Anhalten"}. {"Stopped Nodes","Angehaltene Knoten"}. {"Storage Type","Speichertyp"}. {"Store binary backup:","Speichere binäres Backup:"}. {"Store plain text backup:","Speichere Klartext-Backup:"}. {"Stream management is already enabled","Stream-Verwaltung ist bereits aktiviert"}. {"Stream management is not enabled","Stream-Verwaltung ist nicht aktiviert"}. {"Subject","Betreff"}. {"Submit","Senden"}. {"Submitted","Gesendet"}. {"Subscriber Address","Abonnenten-Adresse"}. {"Subscribers may publish","Abonnenten dürfen veröffentlichen"}. {"Subscription requests must be approved and only subscribers may retrieve items","Abonnement-Anforderungen müssen genehmigt werden und nur Abonnenten dürfen Items abrufen"}. {"Subscription","Abonnement"}. {"Subscriptions are not allowed","Abonnements sind nicht erlaubt"}. {"Sunday","Sonntag"}. {"Text associated with a picture","Text verbunden mit einem Bild"}. {"Text associated with a sound","Text verbunden mit einem Klang"}. {"Text associated with a video","Text verbunden mit einem Video"}. {"Text associated with speech","Text verbunden mit Sprache"}. {"That nickname is already in use by another occupant","Dieser Spitzname wird bereits von einem anderen Teilnehmer verwendet"}. {"That nickname is registered by another person","Dieser Spitzname wurde von jemand anderem registriert"}. {"The account already exists","Das Konto existiert bereits"}. {"The account was not unregistered","Das Konto wurde nicht entfernt"}. {"The body text of the last received message","Der Nachrichtenkörper der letzten erhaltenen Nachricht"}. {"The CAPTCHA is valid.","Das CAPTCHA ist gültig."}. {"The CAPTCHA verification has failed","Die CAPTCHA-Verifizierung ist fehlgeschlagen"}. {"The captcha you entered is wrong","Das CAPTCHA das Sie eingegeben haben ist falsch"}. {"The child nodes (leaf or collection) associated with a collection","Die mit einer Sammlung verknüpften Kindknoten (Blatt oder Sammlung)"}. {"The collections with which a node is affiliated","Sammlungen, mit welchen ein Knoten in Verbindung steht"}. {"The DateTime at which a leased subscription will end or has ended","Das DateTime an welchem ein geleastes Abonnement enden wird oder geendet hat"}. {"The datetime when the node was created","Das DateTime an welchem der Knoten erstellt wurde"}. {"The default language of the node","Die voreingestellte Sprache des Knotens"}. {"The feature requested is not supported by the conference","Die angeforderte Eigenschaft wird von der Konferenz nicht unterstützt"}. {"The JID of the node creator","Die JID des Nodeerstellers"}. {"The JIDs of those to contact with questions","Die JIDs jener, die bei Fragen zu kontaktieren sind"}. {"The JIDs of those with an affiliation of owner","Die JIDs jener mit einer Zugehörigkeit von Besitzer"}. {"The JIDs of those with an affiliation of publisher","Die JIDs jener mit einer Zugehörigkeit von Veröffentlicher"}. {"The list of JIDs that may associate leaf nodes with a collection","Die Liste der JIDs die Blattknoten mit einer Sammlung verknüpfen dürfen"}. {"The maximum number of child nodes that can be associated with a collection","Die maximale Anzahl der Kindknoten die mit einer Sammlung verknüpft werden können"}. {"The minimum number of milliseconds between sending any two notification digests","Die minimale Anzahl an Millisekunden zwischen dem Senden von zwei Benachrichtigungs-Übersichten"}. {"The name of the node","Der Name des Knotens"}. {"The node is a collection node","Der Knoten ist ein Sammlungsknoten"}. {"The node is a leaf node (default)","Der Knoten ist ein Blattknoten (Voreinstellung)"}. {"The NodeID of the relevant node","Die NodeID des relevanten Knotens"}. {"The number of pending incoming presence subscription requests","Die Anzahl der ausstehenden eintreffenden Präsenzabonnement-Anforderungen"}. {"The number of subscribers to the node","Die Anzahl der Abonnenten des Knotens"}. {"The number of unread or undelivered messages","Die Anzahl der ungelesenen oder nicht zugestellten Nachrichten"}. {"The password contains unacceptable characters","Das Passwort enthält ungültige Zeichen"}. {"The password is too weak","Das Passwort ist zu schwach"}. {"the password is","das Passwort lautet"}. {"The password of your XMPP account was successfully changed.","Das Passwort Ihres XMPP-Kontos wurde erfolgreich geändert."}. {"The password was not changed","Das Passwort wurde nicht geändert"}. {"The passwords are different","Die Passwörter sind unterschiedlich"}. {"The presence states for which an entity wants to receive notifications","Die Präsenzzustände für welche eine Entität Benachrichtigungen erhalten will"}. {"The query is only allowed from local users","Die Anfrage ist nur von lokalen Benutzern erlaubt"}. {"The query must not contain <item/> elements","Die Anfrage darf keine <item/>-Elemente enthalten"}. {"The room subject can be modified by participants","Das Raum-Thema kann von Teilnehmern geändert werden"}. {"The sender of the last received message","Der Absender der letzten erhaltenen Nachricht"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Das Stanza darf nur ein <active/>-Element, ein <default/>-Element oder ein <list/>-Element enthalten"}. {"The subscription identifier associated with the subscription request","Die mit der Abonnement-Anforderung verknüpfte Abonnement-Bezeichnung"}. {"The type of node data, usually specified by the namespace of the payload (if any)","Die Art der Knotendaten, üblicherweise vom Namensraum der Nutzdaten angegeben (gegebenenfalls)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Die URL einer XSL-Transformation welche auf Nutzdaten angewendet werden kann, um ein geeignetes Nachrichtenkörper-Element zu generieren."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","Die URL einer XSL-Transformation welche auf das Nutzdaten-Format angewendet werden kann, um ein gültiges Data Forms-Ergebnis zu generieren das der Client mit Hilfe einer generischen Data Forms-Rendering-Engine anzeigen könnte"}. {"The username is not valid","Der Benutzername ist nicht gültig"}. {"There was an error changing the password: ","Es trat ein Fehler beim Ändern des Passwortes auf: "}. {"There was an error creating the account: ","Es trat ein Fehler beim Erstellen des Kontos auf: "}. {"There was an error deleting the account: ","Es trat ein Fehler beim Löschen des Kontos auf: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Dies ist schreibungsunabhängig: macbeth ist gleich MacBeth und Macbeth."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Diese Seite erlaubt das Anlegen eines XMPP-Kontos auf diesem XMPP-Server. Ihre JID (Jabber-ID) wird diese Form aufweisen: benutzername@server. Bitte lesen Sie die Anweisungen genau durch, um die Felder korrekt auszufüllen."}. {"This page allows to unregister an XMPP account in this XMPP server.","Diese Seite erlaubt es, ein XMPP-Konto von diesem XMPP-Server zu entfernen."}. {"This room is not anonymous","Dieser Raum ist nicht anonym"}. {"This service can not process the address: ~s","Dieser Dienst kann die Adresse nicht verarbeiten: ~s"}. {"Thursday","Donnerstag"}. {"Time delay","Zeitverzögerung"}. {"Timed out waiting for stream resumption","Zeitüberschreitung beim Warten auf Streamfortsetzung"}. {"Time","Zeit"}. {"To register, visit ~s","Um sich zu registrieren, besuchen Sie ~s"}. {"To ~ts","An ~ts"}. {"To","An"}. {"Token TTL","Token-TTL"}. {"Too many active bytestreams","Zu viele aktive Bytestreams"}. {"Too many CAPTCHA requests","Zu viele CAPTCHA-Anforderungen"}. {"Too many child elements","Zu viele 'child'-Elemente"}. {"Too many <item/> elements","Zu viele <item/>-Elemente"}. {"Too many <list/> elements","Zu viele <list/>-Elemente"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Zu viele (~p) fehlgeschlagene Authentifizierungen von dieser IP-Adresse (~s). Die Adresse wird an ~s UTC entsperrt"}. {"Too many receiver fields were specified","Zu viele Empfängerfelder wurden angegeben"}. {"Too many unacked stanzas","Zu viele unbestätigte Stanzas"}. {"Too many users in this conference","Zu viele Benutzer in dieser Konferenz"}. {"Total rooms","Gesamte Räume"}. {"Traffic rate limit is exceeded","Datenratenlimit wurde überschritten"}. {"Transactions Aborted:","Abgebrochene Transaktionen:"}. {"Transactions Committed:","Übergebene Transaktionen:"}. {"Transactions Logged:","Protokollierte Transaktionen:"}. {"Transactions Restarted:","Neu gestartete Transaktionen:"}. {"~ts's Offline Messages Queue","Offline-Nachrichten-Warteschlange von ~ts"}. {"Tuesday","Dienstag"}. {"Unable to generate a CAPTCHA","Konnte kein CAPTCHA erstellen"}. {"Unable to register route on existing local domain","Konnte Route auf existierender lokaler Domäne nicht registrieren"}. {"Unauthorized","Nicht autorisiert"}. {"Unexpected action","Unerwartete Aktion"}. {"Unexpected error condition: ~p","Unerwarteter Fehlerzustand: ~p"}. {"Unregister an XMPP account","Ein XMPP-Konto entfernen"}. {"Unregister","Deregistrieren"}. {"Unselect All","Alle abwählen"}. {"Unsupported <index/> element","Nicht unterstütztes <index/>-Element"}. {"Unsupported version","Nicht unterstützte Version"}. {"Update message of the day (don't send)","Aktualisiere Nachricht des Tages (nicht senden)"}. {"Update message of the day on all hosts (don't send)","Aktualisiere Nachricht des Tages auf allen Hosts (nicht senden)"}. {"Update plan","Aktualisierungsplan"}. {"Update ~p","~p aktualisieren"}. {"Update script","Aktualisierungsscript"}. {"Update","Aktualisieren"}. {"Uptime:","Betriebszeit:"}. {"URL for Archived Discussion Logs","URL für archivierte Diskussionsprotokolle"}. {"User already exists","Benutzer existiert bereits"}. {"User (jid)","Benutzer (JID)"}. {"User JID","Benutzer-JID"}. {"User Management","Benutzerverwaltung"}. {"User removed","Benutzer entfernt"}. {"User session not found","Benutzersitzung nicht gefunden"}. {"User session terminated","Benutzersitzung beendet"}. {"User ~ts","Benutzer ~ts"}. {"User","Benutzer"}. {"Username:","Benutzername:"}. {"Users are not allowed to register accounts so quickly","Benutzer dürfen Konten nicht so schnell registrieren"}. {"Users Last Activity","Letzte Benutzeraktivität"}. {"Users","Benutzer"}. {"Validate","Validieren"}. {"Value 'get' of 'type' attribute is not allowed","Wert 'get' des 'type'-Attributs ist nicht erlaubt"}. {"Value of '~s' should be boolean","Wert von '~s' sollte boolesch sein"}. {"Value of '~s' should be datetime string","Wert von '~s' sollte DateTime-Zeichenkette sein"}. {"Value of '~s' should be integer","Wert von '~s' sollte eine Ganzzahl sein"}. {"Value 'set' of 'type' attribute is not allowed","Wert 'set' des 'type'-Attributs ist nicht erlaubt"}. {"vCard User Search","vCard-Benutzer-Suche"}. {"View Queue","Warteschlange ansehen"}. {"View Roster","Kontaktliste ansehen"}. {"Virtual Hosts","Virtuelle Hosts"}. {"Visitor","Besucher"}. {"Visitors are not allowed to change their nicknames in this room","Besucher dürfen in diesem Raum ihren Spitznamen nicht ändern"}. {"Visitors are not allowed to send messages to all occupants","Besucher dürfen nicht an alle Teilnehmer Nachrichten versenden"}. {"Voice requests are disabled in this conference","Sprachrecht-Anforderungen sind in diesem Raum deaktiviert"}. {"Voice request","Sprachrecht-Anforderung"}. {"Wednesday","Mittwoch"}. {"When a new subscription is processed and whenever a subscriber comes online","Sobald ein neues Abonnement verarbeitet wird und wann immer ein Abonnent sich anmeldet"}. {"When a new subscription is processed","Sobald ein neues Abonnement verarbeitet wird"}. {"When to send the last published item","Wann das letzte veröffentlichte Item gesendet werden soll"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Ob eine Entität zusätzlich zum Nutzdatenformat einen XMPP-Nachrichtenkörper erhalten will"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Ob eine Entität Übersichten (Gruppierungen) von Benachrichtigungen oder alle Benachrichtigungen separat erhalten will"}. {"Whether an entity wants to receive or disable notifications","Ob eine Entität Benachrichtigungen erhalten oder deaktivieren will"}. {"Whether owners or publisher should receive replies to items","Ob Besitzer oder Veröffentlicher Antworten auf Items erhalten sollen"}. {"Whether the node is a leaf (default) or a collection","Ob der Knoten ein Blatt (Voreinstellung) oder eine Sammlung ist"}. {"Whether to allow subscriptions","Ob Abonnements erlaubt sind"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Ob alle Abonnements temporär gemacht werden sollen, basierend auf der Abonnentenpräsenz"}. {"Whether to notify owners about new subscribers and unsubscribes","Ob Besitzer über neue Abonnenten und Abbestellungen benachrichtigt werden sollen"}. {"Who may associate leaf nodes with a collection","Wer Blattknoten mit einer Sammlung verknüpfen darf"}. {"Wrong parameters in the web formulary","Falsche Parameter im Webformular"}. {"Wrong xmlns","Falscher xmlns"}. {"XMPP Account Registration","XMPP-Konto-Registrierung"}. {"XMPP Domains","XMPP-Domänen"}. {"XMPP Show Value of Away","XMPP-Anzeigewert von Abwesend"}. {"XMPP Show Value of Chat","XMPP-Anzeigewert von Chat"}. {"XMPP Show Value of DND (Do Not Disturb)","XMPP-Anzeigewert von DND (Do Not Disturb/Bitte nicht stören)"}. {"XMPP Show Value of XA (Extended Away)","XMPP-Anzeigewert von XA (Extended Away/für längere Zeit abwesend)"}. {"XMPP URI of Associated Publish-Subscribe Node","XMPP-URI des verknüpften Publish-Subscribe-Knotens"}. {"You are being removed from the room because of a system shutdown","Sie werden wegen einer Systemabschaltung aus dem Raum entfernt"}. {"You are not joined to the channel","Sie sind dem Raum nicht beigetreten"}. {"You can later change your password using an XMPP client.","Sie können Ihr Passwort später mit einem XMPP-Client ändern."}. {"You have been banned from this room","Sie wurden aus diesem Raum verbannt"}. {"You have joined too many conferences","Sie sind zu vielen Konferenzen beigetreten"}. {"You must fill in field \"Nickname\" in the form","Sie müssen das Feld \"Spitzname\" im Formular ausfüllen"}. {"You need a client that supports x:data and CAPTCHA to register","Sie benötigen einen Client der x:data und CAPTCHA unterstützt, um sich zu registrieren"}. {"You need a client that supports x:data to register the nickname","Sie benötigen einen Client der x:data unterstützt, um Ihren Spitznamen zu registrieren"}. {"You need an x:data capable client to search","Sie benötigen einen Client der x:data unterstützt, um zu suchen"}. {"Your active privacy list has denied the routing of this stanza.","Ihre aktive Privacy-Liste hat das Routing dieses Stanzas verweigert."}. {"Your contact offline message queue is full. The message has been discarded.","Die Offline-Nachrichten-Warteschlange Ihres Kontaktes ist voll. Die Nachricht wurde verworfen."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Ihre Abonnement-Anforderung und/oder Nachrichten an ~s wurden blockiert. Um Ihre Abonnement-Anforderungen freizugeben, besuchen Sie ~s"}. {"Your XMPP account was successfully registered.","Ihr XMPP-Konto wurde erfolgreich registriert."}. {"Your XMPP account was successfully unregistered.","Ihr XMPP-Konto wurde erfolgreich entfernt."}. {"You're not allowed to create nodes","Sie dürfen keine Knoten erstellen"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/sv.msg���������������������������������������������������������������������0000644�0002322�0002322�00000040327�14154362354�016731� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," har satt ämnet till: "}. {"A friendly name for the node","Ett vänligt namn for noden"}. {"Access denied by service policy","Åtkomst nekad enligt lokal policy"}. {"Action on user","Handling mot användare"}. {"Add Jabber ID","Lägg till Jabber ID"}. {"Add New","Lägg till ny"}. {"Add User","Lägg till användare"}. {"Administration of ","Administration av "}. {"Administration","Administration"}. {"Administrator privileges required","Administrationsprivilegier krävs"}. {"All activity","All aktivitet"}. {"All Users","Alla användare"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Tillåt denna Jabber ID att prenumerera på denna pubsub node"}. {"Allow users to change the subject","Tillåt användare att byta ämne"}. {"Allow users to query other users","Tillåt användare att söka efter andra användare"}. {"Allow users to send invites","Tillåt användare att skicka inbjudningar"}. {"Allow users to send private messages","Tillåt användare att skicka privata meddelanden"}. {"Allow visitors to change nickname","Tillåt gäster att kunna ändra smeknamn"}. {"Allow visitors to send status text in presence updates","Tillåt gäster att skicka statustext som uppdatering"}. {"Announcements","Meddelanden"}. {"April","April"}. {"August","Augusti"}. {"Backup Management","Hantera säkerhetskopior"}. {"Backup to File at ","Säkerhetskopiera till fil på "}. {"Backup","Säkerhetskopiera"}. {"Bad format","Dåligt format"}. {"Birthday","Födelsedag"}. {"Change Password","Ändra lösenord"}. {"Change User Password","Andra användarlösenord"}. {"Chatroom configuration modified","Chattrum konfiguration modifierad"}. {"Chatrooms","Chattrum"}. {"Choose a username and password to register with this server","Välj ett användarnamn och lösenord för att registrera mot denna server"}. {"Choose storage type of tables","Välj lagringstyp för tabeller"}. {"Choose whether to approve this entity's subscription.","Välj om du vill godkänna hela denna prenumertion."}. {"City","Stad"}. {"Commands","Kommandon"}. {"Conference room does not exist","Rummet finns inte"}. {"Configuration of room ~s","Konfiguration för ~s"}. {"Configuration","Konfiguration"}. {"Connected Resources:","Anslutna resurser:"}. {"Country","Land"}. {"CPU Time:","CPU tid"}. {"Database Tables Configuration at ","Databastabellers konfiguration"}. {"Database","Databas"}. {"December","December"}. {"Default users as participants","Gör om användare till deltagare"}. {"Delete message of the day on all hosts","Ta bort dagens meddelande på alla värdar"}. {"Delete message of the day","Ta bort dagens meddelande"}. {"Delete Selected","Tabort valda"}. {"Delete User","Ta bort användare"}. {"Deliver event notifications","Skicka eventnotifikation"}. {"Deliver payloads with event notifications","Skicka innehåll tillsammans med notifikationer"}. {"Description:","Beskrivning:"}. {"Disc only copy","Endast diskkopia"}. {"Dump Backup to Text File at ","Dumpa säkerhetskopia till textfil på "}. {"Dump to Text File","Dumpa till textfil"}. {"Edit Properties","Redigera egenskaper"}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Publish-Subscribe module","ejabberd publikprenumerations modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestrem modul"}. {"ejabberd vCard module","ejabberd vCard-modul"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"Elements","Elements"}. {"Email","Email"}. {"Enable logging","Möjliggör login"}. {"End User Session","Avsluta användarsession"}. {"Enter nickname you want to register","Skriv in smeknamnet du vill registrera"}. {"Enter path to backup file","Skriv in sökväg till fil för säkerhetskopia"}. {"Enter path to jabberd14 spool dir","Skriv in sökväg till spoolkatalog från jabberd14"}. {"Enter path to jabberd14 spool file","Skriv in sökväg till spoolfil från jabberd14"}. {"Enter path to text file","Skriv in sökväg till textfil"}. {"Enter the text you see","Skriv in sökväg till textfil"}. {"Error","Fel"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportera data av alla användare i servern till en PIEFXIS fil (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportera data av användare i en host till PIEFXIS fil (XEP-0227):"}. {"Family Name","Efternamn"}. {"February","Februari"}. {"Friday","Fredag"}. {"From","Från"}. {"Full Name","Fullständigt namn"}. {"Get Number of Online Users","Hämta antal inloggade användare"}. {"Get Number of Registered Users","Hämta antal registrerade användare"}. {"Get User Last Login Time","Hämta användarens senast inloggade tid"}. {"Get User Password","Hämta användarlösenord"}. {"Get User Statistics","Hämta användarstatistik"}. {"Group","Grupp"}. {"Groups","Grupper"}. {"has been banned","har blivit bannad"}. {"has been kicked because of a system shutdown","har blivit kickad p.g.a en systemnerstängning"}. {"has been kicked because of an affiliation change","har blivit kickad p.g.a en ändring av tillhörighet"}. {"has been kicked because the room has been changed to members-only","har blivit kickad p.g.a att rummet har ändrats till endast användare"}. {"has been kicked","har blivit kickad"}. {"Host","Server"}. {"Import Directory","Importera katalog"}. {"Import File","Importera fil"}. {"Import user data from jabberd14 spool file:","Importera användare från jabberd14 Spool filer"}. {"Import User from File at ","Importera användare från fil på "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importera användardata från en PIEFXIS fil (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importera användare från jabberd14 Spool directory:"}. {"Import Users from Dir at ","Importera användare från katalog på "}. {"Import Users From jabberd14 Spool Files","Importera användare från jabberd14 Spool filer"}. {"Improper message type","Felaktig medelandetyp"}. {"Incorrect password","Fel lösenord"}. {"IP addresses","IP adresser"}. {"is now known as","är känd som"}. {"It is not allowed to send private messages of type \"groupchat\"","Det är inte tillåtet att skicka privata medelanden med typen \"groupchat\""}. {"It is not allowed to send private messages to the conference","Det är inte tillåtet att skicka privata medelanden till den här konferensen"}. {"It is not allowed to send private messages","Det ar inte tillåtet att skicka privata meddelanden"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","joinar rummet"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Senast aktivitet"}. {"Last login","Senaste login"}. {"Last month","Senaste månaden"}. {"Last year","Senaste året"}. {"leaves the room","lämnar rummet"}. {"Low level update script","Uppdaterade laglevel skript"}. {"Make participants list public","Gör deltagarlistan publik"}. {"Make room members-only","Gör om rummet till endast medlemmar"}. {"Make room moderated","Gör rummet modererat"}. {"Make room password protected","Gör losenorden i rummet publika"}. {"Make room persistent","Gör rummet permanent"}. {"Make room public searchable","Gör rummet publikt sökbart"}. {"March","Mars"}. {"Max # of items to persist","Högsta antal dataposter som sparas"}. {"Max payload size in bytes","Högsta innehållsstorlek i bytes"}. {"Maximum Number of Occupants","Maximalt antal av användare"}. {"May","Maj"}. {"Membership is required to enter this room","Du måste vara medlem för att komma in i det här rummet"}. {"Members:","Medlemmar:"}. {"Memory","Minne"}. {"Message body","Meddelande kropp"}. {"Middle Name","Mellannamn"}. {"Moderator privileges required","Moderatorprivilegier krävs"}. {"Modified modules","Uppdaterade moduler"}. {"Monday","Måndag"}. {"Name","Namn"}. {"Name:","Namn:"}. {"Never","Aldrig"}. {"Nickname Registration at ","Registrera smeknamn på "}. {"Nickname ~s does not exist in the room","Smeknamnet ~s existerar inte i det här rummet"}. {"Nickname","Smeknamn"}. {"No body provided for announce message","Ingen kropp behövs för dessa meddelanden"}. {"No Data","Ingen data"}. {"No limit","Ingen gräns"}. {"Node ID","Node ID"}. {"Node not found","Noden finns inte"}. {"Nodes","Noder"}. {"None","Inga"}. {"Not Found","Noden finns inte"}. {"Notify subscribers when items are removed from the node","Meddela prenumeranter när dataposter tas bort från noden"}. {"Notify subscribers when the node configuration changes","Meddela prenumeranter när nodens konfiguration ändras"}. {"Notify subscribers when the node is deleted","Meddela prenumeranter när noden tas bort"}. {"November","November"}. {"Number of occupants","Antal besökare"}. {"Number of online users","Antal inloggade användare"}. {"Number of registered users","Antal registrerade användare"}. {"October","Oktober"}. {"Offline Messages","Offline meddelanden"}. {"Offline Messages:","Offline meddelanden:"}. {"OK","OK"}. {"Online Users","Anslutna användare"}. {"Online Users:","Inloggade användare"}. {"Online","Ansluten"}. {"Only deliver notifications to available users","Skicka notifikationer bara till uppkopplade användare"}. {"Only moderators and participants are allowed to change the subject in this room","Endast moderatorer och deltagare har tillåtelse att ändra ämnet i det här rummet"}. {"Only occupants are allowed to send messages to the conference","Utomstående får inte skicka medelanden till den här konferensen"}. {"Only occupants are allowed to send queries to the conference","Utomstående får inte skicka iq-queries till den här konferensen"}. {"Only service administrators are allowed to send service messages","Endast administratörer får skicka tjänstmeddelanden"}. {"Organization Name","Organisationsnamn"}. {"Organization Unit","Organisationsenhet"}. {"Outgoing s2s Connections","Utgaende s2s anslutning"}. {"Outgoing s2s Connections:","Utgående s2s anslutning"}. {"Owner privileges required","Ägarprivilegier krävs"}. {"Packet","Paket"}. {"Password Verification","Lösenordsverifikation"}. {"Password","Lösenord"}. {"Password:","Lösenord:"}. {"Path to Dir","Sökväg till katalog"}. {"Path to File","Sökväg till fil"}. {"Pending","Ännu inte godkända"}. {"Period: ","Period: "}. {"Persist items to storage","Spara dataposter permanent"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Kom ihåg att dessa inställningar endast tar backup pa builtin Mnesias databas. Om du använder ODBC modul så måste du ta backup på SQLs databas enskilt"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Nuvarande äkta Jabber IDs till"}. {"private, ","privat, "}. {"Publish-Subscribe","Publikprenumeration"}. {"PubSub subscriber request","Pubsub prenumerationsforfrågan"}. {"Queries to the conference members are not allowed in this room","Det är förbjudet att skicka iq-queries till konferensdeltagare"}. {"RAM and disc copy","RAM- och diskkopia"}. {"RAM copy","RAM-kopia"}. {"Really delete message of the day?","Verkligen ta bort dagens meddelanden?"}. {"Recipient is not in the conference room","Mottagaren finns inte i rummet"}. {"Registered Users","Registrerade användare"}. {"Registered Users:","Registrerade användare"}. {"Remote copy","Sparas inte lokalt"}. {"Remove User","Ta bort användare"}. {"Remove","Ta bort"}. {"Replaced by new connection","Ersatt av ny anslutning"}. {"Resources","Resurser"}. {"Restart Service","Starta om servicen"}. {"Restart","Omstart"}. {"Restore Backup from File at ","Återställ säkerhetskopia från fil på "}. {"Restore binary backup after next ejabberd restart (requires less memory):","återställ den binära backupen efter nästa ejabberd omstart"}. {"Restore binary backup immediately:","återställ den binära backupen omedelbart"}. {"Restore plain text backup immediately:","återställ textbackup omedelbart"}. {"Restore","Återställ"}. {"Room Configuration","Rumkonfiguration"}. {"Room creation is denied by service policy","Skapandet av rum är förbjudet enligt lokal policy"}. {"Room Occupants","Antal besökare"}. {"Room title","Rumstitel"}. {"Roster groups allowed to subscribe","Rostergrupper tillåts att prenumerera"}. {"Roster size","Roster storlek"}. {"RPC Call Error","RPC Uppringningserror"}. {"Running Nodes","Körande noder"}. {"Saturday","Lördag"}. {"Script check","Skript kollat"}. {"Search Results for ","Sökresultat för"}. {"Search users in ","Sök efter användare på "}. {"Send announcement to all online users on all hosts","Sänd meddelanden till alla inloggade användare på alla värdar"}. {"Send announcement to all online users","Sänd meddelanden till alla inloggade användare"}. {"Send announcement to all users on all hosts","Sänd meddelanden till alla användare på alla värdar"}. {"Send announcement to all users","Sänd meddelanden till alla användare"}. {"September","September"}. {"Set message of the day and send to online users","Sätt dagens status meddelande och skicka till alla användare"}. {"Set message of the day on all hosts and send to online users","Sätt dagens status meddelande pa alla värdar och skicka till alla användare"}. {"Shared Roster Groups","Delade Rostergrupper"}. {"Show Integral Table","Visa kumulativ tabell"}. {"Show Ordinary Table","Visa normal tabell"}. {"Shut Down Service","Stäng ner servicen"}. {"Specify the access model","Specificera accessmodellen"}. {"Specify the publisher model","Ange publiceringsmodell"}. {"Statistics of ~p","Statistik på ~p"}. {"Statistics","Statistik"}. {"Stopped Nodes","Stannade noder"}. {"Stop","Stoppa"}. {"Storage Type","Lagringstyp"}. {"Store binary backup:","Lagra den binära backupen"}. {"Store plain text backup:","Lagra textbackup"}. {"Subject","Ämne"}. {"Submit","Skicka"}. {"Submitted","Skicka in"}. {"Subscriber Address","Prenumerationsadress"}. {"Subscription","Prenumeration"}. {"Sunday","Söndag"}. {"That nickname is registered by another person","Smeknamnet är reserverat"}. {"The CAPTCHA is valid.","Din CAPTCHA är godkänd."}. {"the password is","Lösenordet är"}. {"This room is not anonymous","Detta rum är inte anonymt"}. {"Thursday","Torsdag"}. {"Time delay","Tidsförsening"}. {"Time","Tid"}. {"To","Till"}. {"Traffic rate limit is exceeded","Trafikgränsen har överstigits"}. {"Transactions Aborted:","Transaktioner borttagna"}. {"Transactions Committed:","Transaktioner kommittade"}. {"Transactions Logged:","Transaktioner loggade "}. {"Transactions Restarted:","Transaktioner omstartade"}. {"Tuesday","Tisdag"}. {"Unauthorized","Ej auktoriserad"}. {"Update message of the day (don't send)","Uppdatera dagens status meddelande (skicka inte)"}. {"Update message of the day on all hosts (don't send)","Uppdatera dagens status meddelande på alla värdar (skicka inte)"}. {"Update plan","Uppdateringsplan"}. {"Update script","Uppdatera skript"}. {"Update","Uppdatera"}. {"Uptime:","Tid upp"}. {"User Management","Användarmanagement"}. {"User","Användarnamn"}. {"Users are not allowed to register accounts so quickly","Det är inte tillåtet för användare att skapa konton så fort"}. {"Users Last Activity","Användarens senaste aktivitet"}. {"Users","Användare"}. {"Validate","Validera"}. {"vCard User Search","vCard användare sök"}. {"Virtual Hosts","Virtuella servrar"}. {"Visitors are not allowed to change their nicknames in this room","Det är inte tillåtet for gäster att ändra sina smeknamn i detta rummet"}. {"Visitors are not allowed to send messages to all occupants","Besökare får inte skicka medelande till alla"}. {"Wednesday","Onsdag"}. {"When to send the last published item","När att skicka senast publicerade ämne"}. {"Whether to allow subscriptions","Tillåta prenumerationer?"}. {"You have been banned from this room","Du har blivit bannlyst från det här rummet"}. {"You must fill in field \"Nickname\" in the form","Du måste fylla i fält \"smeknamn\" i formen"}. {"You need an x:data capable client to search","Du behöver en klient som stödjer x:data, för att kunna söka"}. {"Your contact offline message queue is full. The message has been discarded.","Din kontaktkö for offlinekontakter ar full"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Dina meddelanden till ~s är blockerade. För att avblockera dem, gå till ~s"}. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/cs.msg���������������������������������������������������������������������0000644�0002322�0002322�00000070106�14154362354�016704� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," změnil(a) téma na: "}. {"A friendly name for the node","Přívětivé jméno pro uzel"}. {"A password is required to enter this room","Pro vstup do místnosti musíte zadat heslo"}. {"Accept","Přijmout"}. {"Access denied by service policy","Přístup byl zamítnut nastavením služby"}. {"Action on user","Akce aplikovaná na uživatele"}. {"Add Jabber ID","Přidat Jabber ID"}. {"Add New","Přidat nový"}. {"Add User","Přidat uživatele"}. {"Administration of ","Administrace "}. {"Administration","Administrace"}. {"Administrator privileges required","Potřebujete práva administrátora"}. {"All activity","Všechny aktivity"}. {"All Users","Všichni uživatelé"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Povolit tomuto Jabber ID odebírat tento pubsub uzel?"}. {"Allow users to change the subject","Povolit uživatelům měnit téma místnosti"}. {"Allow users to query other users","Povolit uživatelům odesílat požadavky (query) ostatním uživatelům"}. {"Allow users to send invites","Povolit uživatelům posílání pozvánek"}. {"Allow users to send private messages","Povolit uživatelům odesílat soukromé zprávy"}. {"Allow visitors to change nickname","Povolit návštěvníkům měnit přezdívku"}. {"Allow visitors to send private messages to","Povolit návštěvníkům odesílat soukromé zprávy"}. {"Allow visitors to send status text in presence updates","Povolit návštěvníkům posílat stavové zprávy ve statusu"}. {"Allow visitors to send voice requests","Povolit uživatelům posílat žádosti o voice práva"}. {"Announcements","Oznámení"}. {"April",". dubna"}. {"August",". srpna"}. {"Automatic node creation is not enabled","Automatické vytváření uzlů není povoleno"}. {"Backup Management","Správa zálohování"}. {"Backup of ~p","Záloha ~p"}. {"Backup to File at ","Záloha do souboru na "}. {"Backup","Zálohovat"}. {"Bad format","Nesprávný formát"}. {"Birthday","Datum narození"}. {"Both the username and the resource are required","Uživatelské jméno i zdroj jsou požadované položky"}. {"Bytestream already activated","Bytestream již byl aktivován"}. {"Cannot remove active list","Aktivní seznam nelze odebrat"}. {"Cannot remove default list","Výchozí seznam nelze odebrat"}. {"CAPTCHA web page","Webová stránka CAPTCHA"}. {"Change Password","Změnit heslo"}. {"Change User Password","Změnit heslo uživatele"}. {"Changing password is not allowed","Změna hesla není povolena"}. {"Changing role/affiliation is not allowed","Změna role/příslušnosti není povolena"}. {"Characters not allowed:","Nepřípustné znaky:"}. {"Chatroom configuration modified","Nastavení diskuzní místnosti bylo změněno"}. {"Chatroom is created","Místnost vytvořena"}. {"Chatroom is destroyed","Místnost zrušena"}. {"Chatroom is started","Místnost spuštěna"}. {"Chatroom is stopped","Místnost zastavena"}. {"Chatrooms","Místnosti"}. {"Choose a username and password to register with this server","Zadejte jméno uživatele a heslo pro registraci na tomto serveru"}. {"Choose storage type of tables","Vyberte typ úložiště pro tabulky"}. {"Choose whether to approve this entity's subscription.","Zvolte, zda chcete schválit odebírání touto entitou."}. {"City","Město"}. {"Commands","Příkazy"}. {"Conference room does not exist","Místnost neexistuje"}. {"Configuration of room ~s","Konfigurace místnosti ~s"}. {"Configuration","Konfigurace"}. {"Connected Resources:","Připojené zdroje:"}. {"Country","Země"}. {"CPU Time:","Čas procesoru:"}. {"Database failure","Chyba databáze"}. {"Database Tables at ~p","Databázové tabulky na ~p"}. {"Database Tables Configuration at ","Konfigurace databázových tabulek "}. {"Database","Databáze"}. {"December",". prosince"}. {"Default users as participants","Uživatelé jsou implicitně členy"}. {"Delete message of the day on all hosts","Smazat zprávu dne na všech hostitelích"}. {"Delete message of the day","Smazat zprávu dne"}. {"Delete Selected","Smazat vybrané"}. {"Delete User","Smazat uživatele"}. {"Deliver event notifications","Doručovat upozornění na události"}. {"Deliver payloads with event notifications","Doručovat náklad s upozorněním na událost"}. {"Description:","Popis:"}. {"Disc only copy","Jen kopie disku"}. {"Dump Backup to Text File at ","Uložit zálohu do textového souboru na "}. {"Dump to Text File","Uložit do textového souboru"}. {"Edit Properties","Upravit vlastnosti"}. {"Either approve or decline the voice request.","Povolit nebo odmítnout voice žádost."}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Multicast service","Služba ejabberd Multicast"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modul"}. {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","Webová administrace ejabberd"}. {"Elements","Položek"}. {"Email","E-mail"}. {"Enable logging","Zaznamenávat konverzace"}. {"Enable message archiving","Povolit ukládání historie zpráv"}. {"Enabling push without 'node' attribute is not supported","Aktivováno push bez atributu 'node' není podporováno"}. {"End User Session","Ukončit sezení uživatele"}. {"Enter nickname you want to register","Zadejte přezdívku, kterou chcete zaregistrovat"}. {"Enter path to backup file","Zadajte cestu k souboru se zálohou"}. {"Enter path to jabberd14 spool dir","Zadejte cestu k jabberd14 spool adresáři"}. {"Enter path to jabberd14 spool file","Zadejte cestu k spool souboru jabberd14"}. {"Enter path to text file","Zadajte cestu k textovému souboru"}. {"Enter the text you see","Zadejte text, který vidíte"}. {"Error","Chyba"}. {"Exclude Jabber IDs from CAPTCHA challenge","Vyloučit Jabber ID z procesu CAPTCHA ověřování"}. {"Export all tables as SQL queries to a file:","Zálohovat všechny tabulky jako SQL dotazy do souboru:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportovat všechny uživatele do souboru ve formátu PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportovat uživatele na hostiteli do souboru ve formátu PIEFXIS (XEP-0227):"}. {"External component failure","Chyba externí komponenty"}. {"External component timeout","Timeout externí komponenty"}. {"Failed to activate bytestream","Chyba při aktivaci bytestreamu"}. {"Failed to extract JID from your voice request approval","Došlo k chybě při získávání Jabber ID z vaší žádosti o voice práva"}. {"Failed to map delegated namespace to external component","Chyba při mapování namespace pro externí komponentu"}. {"Failed to parse HTTP response","Chyba parsování HTTP odpovědi"}. {"Failed to process option '~s'","Chyba při zpracování možnosti '~s'"}. {"Family Name","Příjmení"}. {"February",". února"}. {"File larger than ~w bytes","Soubor větší než ~w bytů"}. {"Friday","Pátek"}. {"From","Od"}. {"Full Name","Celé jméno"}. {"Get Number of Online Users","Získat počet online uživatelů"}. {"Get Number of Registered Users","Získat počet registrovaných uživatelů"}. {"Get User Last Login Time","Získat čas podleního přihlášení uživatele"}. {"Get User Password","Získat heslo uživatele"}. {"Get User Statistics","Získat statistiky uživatele"}. {"Given Name","Křestní jméno"}. {"Grant voice to this person?","Udělit voice práva této osobě?"}. {"Group","Skupina"}. {"Groups","Skupiny"}. {"has been banned","byl(a) zablokován(a)"}. {"has been kicked because of a system shutdown","byl(a) vyhozen(a), protože dojde k vypnutí systému"}. {"has been kicked because of an affiliation change","byl(a) vyhozen(a) kvůli změně přiřazení"}. {"has been kicked because the room has been changed to members-only","byl(a) vyhozen(a), protože mísnost je nyní pouze pro členy"}. {"has been kicked","byl(a) vyhozen(a) z místnosti"}. {"Host unknown","Neznámý hostitel"}. {"Host","Hostitel"}. {"If you don't see the CAPTCHA image here, visit the web page.","Pokud zde nevidíte obrázek CAPTCHA, přejděte na webovou stránku."}. {"Import Directory","Import adresáře"}. {"Import File","Import souboru"}. {"Import user data from jabberd14 spool file:","Importovat uživatele z jabberd14 spool souborů:"}. {"Import User from File at ","Importovat uživatele ze souboru na "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importovat uživatele ze souboru ve formátu PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importovat uživatele z jabberd14 spool souborů:"}. {"Import Users from Dir at ","Importovat uživatele z adresáře na "}. {"Import Users From jabberd14 Spool Files","Importovat uživatele z jabberd14 spool souborů"}. {"Improper domain part of 'from' attribute","Nesprávná část s doménou atributu 'from'"}. {"Improper message type","Nesprávný typ zprávy"}. {"Incoming s2s Connections:","Příchozí s2s spojení:"}. {"Incorrect CAPTCHA submit","Nesprávné odeslání CAPTCHA"}. {"Incorrect data form","Nesprávný datový formulář"}. {"Incorrect password","Nesprávné heslo"}. {"Incorrect value of 'action' attribute","Nesprávná hodnota atributu 'action'"}. {"Incorrect value of 'action' in data form","Nesprávná hodnota atributu 'action' v datovém formuláři"}. {"Incorrect value of 'path' in data form","Nesprávná hodnota atributu 'path' v datovém formuláři"}. {"Insufficient privilege","Nedostatečné oprávnění"}. {"Invalid 'from' attribute in forwarded message","Nesprávný atribut 'from' v přeposlané zprávě"}. {"Invitations are not allowed in this conference","Pozvánky nejsou povoleny v této místnosti"}. {"IP addresses","IP adresy"}. {"is now known as","se přejmenoval(a) na"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Není povoleno posílat chybové zprávy do místnosti. Účastník (~s) odeslal chybovou zprávu (~s) a byl vyhozen z místnosti"}. {"It is not allowed to send private messages of type \"groupchat\"","Není dovoleno odeslání soukromé zprávy typu \"skupinová zpráva\" "}. {"It is not allowed to send private messages to the conference","Není povoleno odesílat soukromé zprávy v této místnosti"}. {"It is not allowed to send private messages","Je zakázáno posílat soukromé zprávy"}. {"Jabber ID","Jabber ID"}. {"January",". ledna"}. {"joins the room","vstoupil(a) do místnosti"}. {"July",". července"}. {"June",". června"}. {"Last Activity","Poslední aktivita"}. {"Last login","Poslední přihlášení"}. {"Last month","Poslední měsíc"}. {"Last year","Poslední rok"}. {"leaves the room","opustil(a) místnost"}. {"List of rooms","Seznam místností"}. {"Low level update script","Nízkoúrovňový aktualizační skript"}. {"Make participants list public","Nastavit seznam účastníků jako veřejný"}. {"Make room CAPTCHA protected","Chránit místnost pomocí CAPTCHA"}. {"Make room members-only","Zpřístupnit místnost jen členům"}. {"Make room moderated","Nastavit místnost jako moderovanou"}. {"Make room password protected","Chránit místnost heslem"}. {"Make room persistent","Nastavit místnost jako stálou"}. {"Make room public searchable","Nastavit místnost jako veřejnou"}. {"Malformed username","Chybně formátováné jméno uživatele"}. {"March",". března"}. {"Max # of items to persist","Maximální počet položek, které je možné natrvalo uložit"}. {"Max payload size in bytes","Maximální náklad v bajtech"}. {"Maximum Number of Occupants","Počet účastníků"}. {"May",". května"}. {"Members:","Členové:"}. {"Membership is required to enter this room","Pro vstup do místnosti musíte být členem"}. {"Memory","Paměť"}. {"Message body","Tělo zprávy"}. {"Message not found in forwarded payload","Zpráva nebyla nalezena v přeposlaném obsahu"}. {"Middle Name","Druhé jméno"}. {"Minimum interval between voice requests (in seconds)","Minimální interval mezi žádostmi o voice práva (v sekundách)"}. {"Moderator privileges required","Potřebujete práva moderátora"}. {"Moderator","Moderátor"}. {"Modified modules","Aktualizované moduly"}. {"Module failed to handle the query","Modul chyboval při zpracování dotazu"}. {"Monday","Pondělí"}. {"Multicast","Multicast"}. {"Multi-User Chat","Víceuživatelský chat"}. {"Name","Jméno"}. {"Name:","Jméno:"}. {"Neither 'jid' nor 'nick' attribute found","Nebyl nalezen atribut 'jid' ani 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Nebyl nalezen atribut 'role' ani 'affiliation'"}. {"Never","Nikdy"}. {"New Password:","Nové heslo:"}. {"Nickname Registration at ","Registrace přezdívky na "}. {"Nickname ~s does not exist in the room","Přezdívka ~s v místnosti neexistuje"}. {"Nickname","Přezdívka"}. {"No 'affiliation' attribute found","Chybějící atribut 'affiliation'"}. {"No available resource found","Nebyl nalezen žádný dostupný zdroj"}. {"No body provided for announce message","Zpráva neobsahuje text"}. {"No data form found","Nebyl nalezen datový formulář"}. {"No Data","Žádná data"}. {"No features available","Žádné funce nejsou dostupné"}. {"No hook has processed this command","Žádný hook nebyl zpracován tímto příkazem"}. {"No info about last activity found","Nebyla žádná informace o poslední aktivitě"}. {"No 'item' element found","Element 'item' nebyl nalezen"}. {"No items found in this query","Žádné položky nebyly nalezeny v tomto dotazu"}. {"No limit","Bez limitu"}. {"No module is handling this query","Žádný modul neobsluhuje tento dotaz"}. {"No node specified","Žádný uzel nebyl specifikován"}. {"No 'password' found in data form","Chybějící atribut 'password' v datovém formuláři"}. {"No 'password' found in this query","Chybějící atribut 'password' v tomto dotazu"}. {"No 'path' found in data form","Chybějící atribut 'path' v datovém formuláři"}. {"No pending subscriptions found","Žádné čekající předplatné nebylo nalezeno"}. {"No privacy list with this name found","Žádný privacy list s tímto jménem nebyl nalezen"}. {"No private data found in this query","Žádná soukromá data nebyla nalezena tímto dotazem"}. {"No running node found","Nebyl nalezen žádný běžící uzel"}. {"No services available","Žádné služby nejsou dostupné"}. {"No statistics found for this item","Nebyly nalezeny statistiky pro uvedenou položku"}. {"No 'to' attribute found in the invitation","Chybějící atribut 'to' v pozvánce"}. {"Node already exists","Uzel již existuje"}. {"Node ID","ID uzlu"}. {"Node index not found","Index uzlu nebyl nalezen"}. {"Node not found","Uzel nenalezen"}. {"Node ~p","Uzel ~p"}. {"Nodeprep has failed","Nodeprep chyboval"}. {"Nodes","Uzly"}. {"None","Nic"}. {"Not Found","Nenalezeno"}. {"Not subscribed","Není odebíráno"}. {"Notify subscribers when items are removed from the node","Upozornit odběratele na odstranění položek z uzlu"}. {"Notify subscribers when the node configuration changes","Upozornit odběratele na změnu nastavení uzlu"}. {"Notify subscribers when the node is deleted","Upozornit odběratele na smazání uzlu"}. {"November",". listopadu"}. {"Number of occupants","Počet účastníků"}. {"Number of online users","Počet online uživatelů"}. {"Number of registered users","Počet registrovaných uživatelů"}. {"October",". října"}. {"Offline Messages","Offline zprávy"}. {"Offline Messages:","Offline zprávy:"}. {"OK","OK"}. {"Old Password:","Současné heslo:"}. {"Online Users","Připojení uživatelé"}. {"Online Users:","Připojení uživatelé:"}. {"Online","Online"}. {"Only deliver notifications to available users","Doručovat upozornění jen právě přihlášeným uživatelům"}. {"Only <enable/> or <disable/> tags are allowed","Pouze značky <enable/> nebo <disable/>jsou povoleny"}. {"Only <list/> element is allowed in this query","Pouze element <list/> je povolen v tomto dotazu"}. {"Only members may query archives of this room","Pouze moderátoři mají povoleno měnit téma místnosti"}. {"Only moderators and participants are allowed to change the subject in this room","Jen moderátoři a účastníci mají povoleno měnit téma této místnosti"}. {"Only moderators are allowed to change the subject in this room","Jen moderátoři mají povoleno měnit téma místnosti"}. {"Only moderators can approve voice requests","Pouze moderátoři mohou schválit žádosti o voice práva"}. {"Only occupants are allowed to send messages to the conference","Jen členové mají povolené zasílat zprávy do místnosti"}. {"Only occupants are allowed to send queries to the conference","Jen členové mohou odesílat požadavky (query) do místnosti"}. {"Only service administrators are allowed to send service messages","Pouze správci služby smí odesílat servisní zprávy"}. {"Organization Name","Název firmy"}. {"Organization Unit","Oddělení"}. {"Outgoing s2s Connections","Odchozí s2s spojení"}. {"Outgoing s2s Connections:","Odchozí s2s spojení:"}. {"Owner privileges required","Jsou vyžadována práva vlastníka"}. {"Packet","Paket"}. {"Participant","Účastník"}. {"Password Verification","Ověření hesla"}. {"Password Verification:","Ověření hesla:"}. {"Password","Heslo"}. {"Password:","Heslo:"}. {"Path to Dir","Cesta k adresáři"}. {"Path to File","Cesta k souboru"}. {"Pending","Čekající"}. {"Period: ","Čas: "}. {"Persist items to storage","Uložit položky natrvalo do úložiště"}. {"Ping query is incorrect","Ping dotaz je nesprávný"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Podotýkáme, že tato nastavení budou zálohována do zabudované databáze Mnesia. Pokud používáte ODBC modul, musíte zálohovat svoji SQL databázi samostatně."}. {"Please, wait for a while before sending new voice request","Prosím, počkejte chvíli před posláním nové žádosti o voice práva"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Odhalovat skutečná Jabber ID"}. {"private, ","soukromá, "}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","Žádost odběratele PubSub"}. {"Purge all items when the relevant publisher goes offline","Smazat všechny položky, pokud se příslušný poskytovatel odpojí"}. {"Queries to the conference members are not allowed in this room","Požadavky (queries) na členy místnosti nejsou v této místnosti povolené"}. {"Query to another users is forbidden","Dotaz na jiné uživatele je zakázán"}. {"RAM and disc copy","Kopie RAM a disku"}. {"RAM copy","Kopie RAM"}. {"Really delete message of the day?","Skutečně smazat zprávu dne?"}. {"Recipient is not in the conference room","Příjemce se nenachází v místnosti"}. {"Registered Users","Registrovaní uživatelé"}. {"Registered Users:","Registrovaní uživatelé:"}. {"Register","Zaregistrovat se"}. {"Remote copy","Vzdálená kopie"}. {"Remove All Offline Messages","Odstranit všechny offline zprávy"}. {"Remove User","Odstranit uživatele"}. {"Remove","Odstranit"}. {"Replaced by new connection","Nahrazeno novým spojením"}. {"Resources","Zdroje"}. {"Restart Service","Restartovat službu"}. {"Restart","Restart"}. {"Restore Backup from File at ","Obnovit zálohu ze souboru na "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Obnovit binární zálohu při následujícím restartu ejabberd (vyžaduje méně paměti):"}. {"Restore binary backup immediately:","Okamžitě obnovit binární zálohu:"}. {"Restore plain text backup immediately:","Okamžitě obnovit zálohu z textového souboru:"}. {"Restore","Obnovit"}. {"Roles for which Presence is Broadcasted","Role, pro které je zpráva o stavu šířena"}. {"Room Configuration","Nastavení místnosti"}. {"Room creation is denied by service policy","Pravidla služby nepovolují vytvořit místnost"}. {"Room description","Popis místnosti"}. {"Room Occupants","Počet účastníků"}. {"Room title","Název místnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktů, které mohou odebírat"}. {"Roster size","Velikost seznamu kontaktů"}. {"RPC Call Error","Chyba RPC volání"}. {"Running Nodes","Běžící uzly"}. {"Saturday","Sobota"}. {"Script check","Kontrola skriptu"}. {"Search Results for ","Výsledky hledání pro "}. {"Search users in ","Hledat uživatele v "}. {"Send announcement to all online users on all hosts","Odeslat oznámení všem online uživatelům na všech hostitelích"}. {"Send announcement to all online users","Odeslat oznámení všem online uživatelům"}. {"Send announcement to all users on all hosts","Odeslat oznámení všem uživatelům na všech hostitelích"}. {"Send announcement to all users","Odeslat oznámení všem uživatelům"}. {"September",". září"}. {"Server:","Server:"}. {"Set message of the day and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. {"Set message of the day on all hosts and send to online users","Nastavit zprávu dne a odeslat ji online uživatelům"}. {"Shared Roster Groups","Skupiny pro sdílený seznam kontaktů"}. {"Show Integral Table","Zobrazit kompletní tabulku"}. {"Show Ordinary Table","Zobrazit běžnou tabulku"}. {"Shut Down Service","Vypnout službu"}. {"Specify the access model","Uveďte přístupový model"}. {"Specify the event message type","Zvolte typ zpráv pro události"}. {"Specify the publisher model","Specifikovat model pro publikování"}. {"Statistics of ~p","Statistiky ~p"}. {"Statistics","Statistiky"}. {"Stopped Nodes","Zastavené uzly"}. {"Stop","Stop"}. {"Storage Type","Typ úložiště"}. {"Store binary backup:","Uložit binární zálohu:"}. {"Store plain text backup:","Uložit zálohu do textového souboru:"}. {"Subject","Předmět"}. {"Submit","Odeslat"}. {"Submitted","Odeslané"}. {"Subscriber Address","Adresa odběratele"}. {"Subscription","Přihlášení"}. {"Subscriptions are not allowed","Předplatné není povoleno"}. {"Sunday","Neděle"}. {"That nickname is already in use by another occupant","Přezdívka je již používána jiným členem"}. {"That nickname is registered by another person","Přezdívka je zaregistrována jinou osobou"}. {"The CAPTCHA is valid.","CAPTCHA souhlasí."}. {"The CAPTCHA verification has failed","Ověření CAPTCHA se nezdařilo"}. {"The collections with which a node is affiliated","Kolekce, se kterými je uzel spřízněn"}. {"The feature requested is not supported by the conference","Požadovaná vlastnost není podporována touto místností"}. {"The password contains unacceptable characters","Heslo obsahuje nepovolené znaky"}. {"The password is too weak","Heslo je příliš slabé"}. {"the password is","heslo je"}. {"The query is only allowed from local users","Dotaz je povolen pouze pro místní uživatele"}. {"The query must not contain <item/> elements","Dotaz nesmí obsahovat elementy <item/>"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Stanza MUSÍ obsahovat pouze jeden element <active/>, jeden element <default/> nebo jeden element <list/>"}. {"There was an error creating the account: ","Při vytváření účtu došlo k chybě:"}. {"There was an error deleting the account: ","Při mazání účtu došlo k chybě: "}. {"This room is not anonymous","Tato místnost není anonymní"}. {"Thursday","Čtvrtek"}. {"Time delay","Časový posun"}. {"Time","Čas"}. {"To register, visit ~s","Pokud se chcete zaregistrovat, navštivte ~s"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Příliš mnoho aktivních bytestreamů"}. {"Too many CAPTCHA requests","Přiliš mnoho CAPTCHA žádostí"}. {"Too many <item/> elements","Příliš mnoho elementů <item/>"}. {"Too many <list/> elements","Přilíš mnoho elementů <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Příliš mnoho (~p) chybných pokusů o přihlášení z této IP adresy (~s). Adresa bude zablokována do ~s UTC"}. {"Too many unacked stanzas","Příliš mnoho nepotvrzených stanz"}. {"Too many users in this conference","Přiliš mnoho uživatelů v této místnosti"}. {"To","Pro"}. {"Total rooms","Celkem místností"}. {"Traffic rate limit is exceeded","Byl překročen limit"}. {"Transactions Aborted:","Transakcí zrušených:"}. {"Transactions Committed:","Transakcí potvrzených:"}. {"Transactions Logged:","Transakcí zaznamenaných:"}. {"Transactions Restarted:","Transakcí restartovaných:"}. {"Tuesday","Úterý"}. {"Unable to generate a CAPTCHA","Nebylo možné vygenerovat CAPTCHA"}. {"Unable to register route on existing local domain","Není možné zaregistrovat routu na existující místní doménu"}. {"Unauthorized","Nemáte oprávnění"}. {"Unexpected action","Neočekávaná akce"}. {"Unregister","Zrušit registraci"}. {"Unsupported <index/> element","Nepodporovaný <index/> element"}. {"Update message of the day (don't send)","Aktualizovat zprávu dne (neodesílat)"}. {"Update message of the day on all hosts (don't send)","Aktualizovat zprávu dne pro všechny hostitele (neodesílat)"}. {"Update ~p","Aktualizovat ~p"}. {"Update plan","Aktualizovat plán"}. {"Update script","Aktualizované skripty"}. {"Update","Aktualizovat"}. {"Uptime:","Čas běhu:"}. {"User already exists","Uživatel již existuje"}. {"User JID","Jabber ID uživatele"}. {"User (jid)","Uživatel (JID)"}. {"User Management","Správa uživatelů"}. {"User session not found","Sezení uživatele nebylo nalezeno"}. {"User session terminated","Sezení uživatele bylo ukončeno"}. {"Username:","Uživatelské jméno:"}. {"Users are not allowed to register accounts so quickly","Je zakázáno registrovat účty v tak rychlém sledu"}. {"Users Last Activity","Poslední aktivita uživatele"}. {"Users","Uživatelé"}. {"User","Uživatel"}. {"Validate","Ověřit"}. {"Value 'get' of 'type' attribute is not allowed","Hodnota 'get' atrubutu 'type' není povolena"}. {"Value of '~s' should be boolean","Hodnota '~s' by měla být boolean"}. {"Value of '~s' should be datetime string","Hodnota '~s' by měla být datetime řetězec"}. {"Value of '~s' should be integer","Hodnota '~s' by měla být celé číslo"}. {"Value 'set' of 'type' attribute is not allowed","Hodnota 'set' atrubutu 'type' není povolena"}. {"vCard User Search","Hledání uživatelů ve vizitkách"}. {"Virtual Hosts","Virtuální hostitelé"}. {"Visitor","Návštěvník"}. {"Visitors are not allowed to change their nicknames in this room","Návštěvníkům této místnosti je zakázáno měnit přezdívku"}. {"Visitors are not allowed to send messages to all occupants","Návštevníci nemají povoleno zasílat zprávy všem účastníkům v této místnosti"}. {"Voice requests are disabled in this conference","Voice žádosti jsou v této místnosti zakázány"}. {"Voice request","Žádost o voice práva"}. {"Wednesday","Středa"}. {"When to send the last published item","Kdy odeslat poslední publikovanou položku"}. {"Whether to allow subscriptions","Povolit odebírání"}. {"You have been banned from this room","Byl jste vyloučen z této místnosti"}. {"You have joined too many conferences","Vstoupil jste do příliš velkého množství místností"}. {"You must fill in field \"Nickname\" in the form","Musíte vyplnit políčko \"Přezdívka\" ve formuláři"}. {"You need a client that supports x:data and CAPTCHA to register","Pro registraci potřebujete klienta s podporou x:data a CAPTCHA"}. {"You need a client that supports x:data to register the nickname","Pro registraci přezdívky potřebujete klienta s podporou x:data"}. {"You need an x:data capable client to search","K vyhledávání potřebujete klienta podporujícího x:data"}. {"Your active privacy list has denied the routing of this stanza.","Vaše nastavení soukromí znemožnilo směrování této stance."}. {"Your contact offline message queue is full. The message has been discarded.","Fronta offline zpráv pro váš kontakt je plná. Zpráva byla zahozena."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Nesmíte posílat zprávy na ~s. Pro povolení navštivte ~s"}. {"You're not allowed to create nodes","Nemáte povoleno vytvářet uzly"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/pt-br.msg������������������������������������������������������������������0000644�0002322�0002322�00000137356�14154362354�017336� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Adicione * no final do campo para combinar com a substring)"}. {" has set the subject to: "," mudou o assunto para: "}. {"# participants","# participantes"}. {"A description of the node","Uma descrição do nó"}. {"A friendly name for the node","Um nome familiar para o nó"}. {"A password is required to enter this room","Se necessita senha para entrar nesta sala"}. {"A Web Page","Uma página da web"}. {"Accept","Aceito"}. {"Access denied by service policy","Acesso negado pela política do serviço"}. {"Access model of authorize","Modelo de acesso da autorização"}. {"Access model of open","Modelo para acesso aberto"}. {"Access model of presence","Modelo para acesso presença"}. {"Access model of roster","Modelo para acesso lista"}. {"Access model of whitelist","Modelo de acesso da lista branca"}. {"Access model","Modelo de acesso"}. {"Account doesn't exist","A conta não existe"}. {"Action on user","Ação no usuário"}. {"Add Jabber ID","Adicionar ID jabber"}. {"Add New","Adicionar novo"}. {"Add User","Adicionar usuário"}. {"Administration of ","Administração de "}. {"Administration","Administração"}. {"Administrator privileges required","Se necessita privilégios de administrador"}. {"All activity","Todas atividades"}. {"All Users","Todos os usuários"}. {"Allow subscription","Permitir a assinatura"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autorizar este Jabber ID para a inscrição neste tópico pubsub?"}. {"Allow this person to register with the room?","Permita que esta pessoa se registe na sala?"}. {"Allow users to change the subject","Permitir a usuários modificar o assunto"}. {"Allow users to query other users","Permitir a usuários pesquisar informações sobre os demais"}. {"Allow users to send invites","Permitir a usuários envio de convites"}. {"Allow users to send private messages","Permitir a usuários enviarem mensagens privadas"}. {"Allow visitors to change nickname","Permitir mudança de apelido aos visitantes"}. {"Allow visitors to send private messages to","Permitir visitantes enviar mensagem privada para"}. {"Allow visitors to send status text in presence updates","Permitir atualizações de status aos visitantes"}. {"Allow visitors to send voice requests","Permitir aos visitantes o envio de requisições de voz"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Um grupo LDAP associado que define a adesão à sala; este deve ser um Nome Distinto LDAP de acordo com uma definição específica da implementação ou da implantação específica de um grupo."}. {"Announcements","Anúncios"}. {"Answer associated with a picture","Resposta associada com uma foto"}. {"Answer associated with a video","Resposta associada com um vídeo"}. {"Answer associated with speech","Resposta associada com a fala"}. {"Answer to a question","Resposta para uma pergunta"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Qualquer pessoa do(s) grupo(s) informado(s) podem se inscrever e recuperar itens"}. {"Anyone may associate leaf nodes with the collection","Qualquer pessoa pode associar nós das páginas à coleção"}. {"Anyone may publish","Qualquer pessoa pode publicar"}. {"Anyone may subscribe and retrieve items","Qualquer pessoa pode se inscrever e recuperar os itens"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Qualquer pessoa com uma assinatura presente dos dois ou de ambos pode se inscrever e recuperar os itens"}. {"Anyone with Voice","Qualquer pessoa com voz"}. {"Anyone","Qualquer pessoa"}. {"April","Abril"}. {"Attribute 'channel' is required for this request","O atributo 'canal' é necessário para esta solicitação"}. {"Attribute 'id' is mandatory for MIX messages","O atributo 'id' é obrigatório para mensagens MIX"}. {"Attribute 'jid' is not allowed here","O atributo 'jid' não é permitido aqui"}. {"Attribute 'node' is not allowed here","O Atributo 'nó' não é permitido aqui"}. {"Attribute 'to' of stanza that triggered challenge","O atributo 'para' da estrofe que desencadeou o desafio"}. {"August","Agosto"}. {"Automatic node creation is not enabled","Criação automatizada de nós está desabilitada"}. {"Backup Management","Gestão de Backup"}. {"Backup of ~p","Backup de ~p"}. {"Backup to File at ","Salvar backup para arquivo em "}. {"Backup","Salvar cópia de segurança"}. {"Bad format","Formato incorreto"}. {"Birthday","Aniversário"}. {"Both the username and the resource are required","Nome de usuário e recurso são necessários"}. {"Bytestream already activated","Bytestream já foi ativado"}. {"Cannot remove active list","Não é possível remover uma lista ativa"}. {"Cannot remove default list","Não é possível remover uma lista padrão"}. {"CAPTCHA web page","CAPTCHA web page"}. {"Challenge ID","ID do desafio"}. {"Change Password","Mudar senha"}. {"Change User Password","Alterar Senha do Usuário"}. {"Changing password is not allowed","Não é permitida a alteração da senha"}. {"Changing role/affiliation is not allowed","Não é permitida a alteração da função/afiliação"}. {"Channel already exists","O canal já existe"}. {"Channel does not exist","O canal não existe"}. {"Channels","Canais"}. {"Characters not allowed:","Caracteres não aceitos:"}. {"Chatroom configuration modified","Configuração da sala de bate-papo modificada"}. {"Chatroom is created","A sala de chat está criada"}. {"Chatroom is destroyed","A sala de chat está destruída"}. {"Chatroom is started","A sala de chat está iniciada"}. {"Chatroom is stopped","A sala de chat está parada"}. {"Chatrooms","Salas de Chat"}. {"Choose a username and password to register with this server","Escolha um nome de usuário e senha para registrar-se neste servidor"}. {"Choose storage type of tables","Selecione o tipo de armazenamento das tabelas"}. {"Choose whether to approve this entity's subscription.","Aprovar esta assinatura."}. {"City","Cidade"}. {"Client acknowledged more stanzas than sent by server","O cliente reconheceu mais estrofes do que as enviadas pelo servidor"}. {"Commands","Comandos"}. {"Conference room does not exist","A sala de conferência não existe"}. {"Configuration of room ~s","Configuração para ~s"}. {"Configuration","Configuração"}. {"Connected Resources:","Recursos conectados:"}. {"Contact Addresses (normally, room owner or owners)","Endereços de contato (normalmente, o proprietário ou os proprietários da sala)"}. {"Country","País"}. {"CPU Time:","Tempo da CPU:"}. {"Current Discussion Topic","Assunto em discussão"}. {"Database failure","Falha no banco de dados"}. {"Database Tables at ~p","Tabelas da Base de dados em ~p"}. {"Database Tables Configuration at ","Configuração de Tabelas de Base de dados em "}. {"Database","Base de dados"}. {"December","Dezembro"}. {"Default users as participants","Usuários padrões como participantes"}. {"Delete content","Excluir o conteúdo"}. {"Delete message of the day on all hosts","Apagar a mensagem do dia em todos os hosts"}. {"Delete message of the day","Apagar mensagem do dia"}. {"Delete Selected","Remover os selecionados"}. {"Delete table","Excluir a tabela"}. {"Delete User","Deletar Usuário"}. {"Deliver event notifications","Entregar as notificações de evento"}. {"Deliver payloads with event notifications","Enviar payloads junto com as notificações de eventos"}. {"Description:","Descrição:"}. {"Disc only copy","Somente cópia em disco"}. {"'Displayed groups' not added (they do not exist!): ","Os 'Grupos exibidos' não foi adicionado (eles não existem!): "}. {"Displayed:","Exibido:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Não revele a sua senha para ninguém, nem mesmo para o administrador deste servidor XMPP."}. {"Dump Backup to Text File at ","Exportar backup para texto em "}. {"Dump to Text File","Exportar para arquivo texto"}. {"Duplicated groups are not allowed by RFC6121","Os grupos duplicados não são permitidos pela RFC6121"}. {"Dynamically specify a replyto of the item publisher","Definir de forma dinâmica uma resposta da editora do item"}. {"Edit Properties","Editar propriedades"}. {"Either approve or decline the voice request.","Você deve aprovar/desaprovar a requisição de voz."}. {"ejabberd HTTP Upload service","serviço HTTP de upload ejabberd"}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. {"ejabberd Multicast service","Serviço multicast ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo para Publicar Tópicos do ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Modulo ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. {"Elements","Elementos"}. {"Email Address","Endereço de e-mail"}. {"Email","Email"}. {"Enable logging","Permitir criação de logs"}. {"Enable message archiving","Habilitar arquivamento de mensagens"}. {"Enabling push without 'node' attribute is not supported","Abilitar push sem o atributo 'node' não é suportado"}. {"End User Session","Terminar Sessão do Usuário"}. {"Enter nickname you want to register","Introduza o apelido que quer registrar"}. {"Enter path to backup file","Introduza o caminho do arquivo de backup"}. {"Enter path to jabberd14 spool dir","Introduza o caminho para o diretório de fila do jabberd14"}. {"Enter path to jabberd14 spool file","Insira o caminho para a fila (arquivo) do jabberd14"}. {"Enter path to text file","Introduza caminho para o arquivo texto"}. {"Enter the text you see","Insira o texto que você vê"}. {"Erlang XMPP Server","Servidor XMPP Erlang"}. {"Error","Erro"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluir IDs Jabber de serem submetidos ao CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exportar todas as tabelas como SQL para um arquivo:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar todos os dados de todos os usuários no servidor, para arquivos formato PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar dados dos usuários em um host, para arquivos PIEFXIS (XEP-0227):"}. {"External component failure","Falha de componente externo"}. {"External component timeout","Tempo esgotado à espera de componente externo"}. {"Failed to activate bytestream","Falha ao ativar bytestream"}. {"Failed to extract JID from your voice request approval","Não foi possível extrair o JID (Jabber ID) da requisição de voz"}. {"Failed to map delegated namespace to external component","Falha ao mapear namespace delegado ao componente externo"}. {"Failed to parse HTTP response","Falha ao analisar resposta HTTP"}. {"Failed to process option '~s'","Falha ao processar opção '~s'"}. {"Family Name","Sobrenome"}. {"FAQ Entry","Registro das perguntas frequentes"}. {"February","Fevereiro"}. {"File larger than ~w bytes","Arquivo maior que ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Preencha campos para procurar por quaisquer usuários XMPP"}. {"Friday","Sexta"}. {"From ~ts","De ~s"}. {"From","De"}. {"Full List of Room Admins","Lista completa dos administradores das salas"}. {"Full List of Room Owners","Lista completa dos proprietários das salas"}. {"Full Name","Nome completo"}. {"Get Number of Online Users","Obter Número de Usuários Online"}. {"Get Number of Registered Users","Obter Número de Usuários Registrados"}. {"Get Pending","Obter os pendentes"}. {"Get User Last Login Time","Obter a Data do Último Login"}. {"Get User Password","Obter Senha do Usuário"}. {"Get User Statistics","Obter Estatísticas do Usuário"}. {"Given Name","Sobrenome"}. {"Grant voice to this person?","Dar voz a esta pessoa?"}. {"Group","Grupo"}. {"Groups that will be displayed to the members","Os grupos que serão exibidos para os membros"}. {"Groups","Grupos"}. {"has been banned","foi banido"}. {"has been kicked because of a system shutdown","foi desconectado porque o sistema foi desligado"}. {"has been kicked because of an affiliation change","foi desconectado porque por afiliação inválida"}. {"has been kicked because the room has been changed to members-only","foi desconectado porque a política da sala mudou, só membros são permitidos"}. {"has been kicked","foi removido"}. {"Host unknown","Máquina desconhecida"}. {"Host","Máquina"}. {"HTTP File Upload","Upload de arquivo HTTP"}. {"Idle connection","Conexão inativa"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se você não conseguir ver o CAPTCHA aqui, visite a web page."}. {"Import Directory","Importar diretório"}. {"Import File","Importar arquivo"}. {"Import user data from jabberd14 spool file:","Importar dados dos usuários de uma fila jabberd14:"}. {"Import User from File at ","Importar usuário a partir do arquivo em "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importe os usuários de um arquivo PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar dados dos usuários de um diretório-fila jabberd14:"}. {"Import Users from Dir at ","Importar usuários a partir do diretório em "}. {"Import Users From jabberd14 Spool Files","Importar usuários de arquivos jabberd14 (spool files)"}. {"Improper domain part of 'from' attribute","Atributo 'from' contém domínio incorreto"}. {"Improper message type","Tipo de mensagem incorreto"}. {"Incoming s2s Connections:","Conexões s2s de Entrada:"}. {"Incorrect CAPTCHA submit","CAPTCHA submetido incorretamente"}. {"Incorrect data form","Formulário dos dados incorreto"}. {"Incorrect password","Senha incorreta"}. {"Incorrect value of 'action' attribute","Valor incorreto do atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorreto de 'action' no formulário de dados"}. {"Incorrect value of 'path' in data form","Valor incorreto de 'path' no formulário de dados"}. {"Insufficient privilege","Privilégio insuficiente"}. {"Internal server error","Erro interno do servidor"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from' inválido na mensagem reenviada"}. {"Invalid node name","Nome do nó inválido"}. {"Invalid 'previd' value","Valor 'previd' inválido"}. {"Invitations are not allowed in this conference","Os convites não são permitidos nesta conferência"}. {"IP addresses","Endereços IP"}. {"is now known as","é agora conhecido como"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Não é permitido o envio de mensagens de erro para a sala. O membro (~s) enviou uma mensagem de erro (~s) e foi expulso da sala"}. {"It is not allowed to send private messages of type \"groupchat\"","Não é permitido enviar mensagens privadas do tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Não é permitido enviar mensagens privadas para a sala de conferência"}. {"It is not allowed to send private messages","Não é permitido enviar mensagens privadas"}. {"Jabber ID","ID Jabber"}. {"January","Janeiro"}. {"JID normalization denied by service policy","Normalização JID negada por causa da política de serviços"}. {"JID normalization failed","A normalização JID falhou"}. {"joins the room","Entrar na sala"}. {"July","Julho"}. {"June","Junho"}. {"Just created","Acabou de ser criado"}. {"Label:","Rótulo:"}. {"Last Activity","Última atividade"}. {"Last login","Último login"}. {"Last message","Última mensagem"}. {"Last month","Último mês"}. {"Last year","Último ano"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Bits menos significativos do hash sha-256 do texto devem ser iguais ao rótulo hexadecimal"}. {"leaves the room","Sair da sala"}. {"List of rooms","Lista de salas"}. {"Logging","Registrando no log"}. {"Low level update script","Script de atualização low level"}. {"Make participants list public","Tornar pública a lista de participantes"}. {"Make room CAPTCHA protected","Tornar protegida a senha da sala"}. {"Make room members-only","Tornar sala apenas para membros"}. {"Make room moderated","Tornar a sala moderada"}. {"Make room password protected","Tornar sala protegida à senha"}. {"Make room persistent","Tornar sala persistente"}. {"Make room public searchable","Tornar sala pública possível de ser encontrada"}. {"Malformed username","Nome de usuário inválido"}. {"MAM preference modification denied by service policy","Modificação de preferência MAM negada por causa da política de serviços"}. {"March","Março"}. {"Max # of items to persist","Máximo # de elementos que persistem"}. {"Max payload size in bytes","Máximo tamanho do payload em bytes"}. {"Maximum file size","Tamanho máximo do arquivo"}. {"Maximum Number of History Messages Returned by Room","Quantidade máxima das mensagens do histórico que foram devolvidas por sala"}. {"Maximum number of items to persist","Quantidade máxima dos itens para manter"}. {"Maximum Number of Occupants","Número máximo de participantes"}. {"May","Maio"}. {"Members not added (inexistent vhost!): ","Membros que não foram adicionados (o vhost não existe!): "}. {"Membership is required to enter this room","É necessário ser membro desta sala para poder entrar"}. {"Members:","Membros:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Memorize a sua senha ou anote-a em um papel guardado em um local seguro. No XMPP, não há uma maneira automatizada de recuperar a sua senha caso a esqueça."}. {"Memory","Memória"}. {"Mere Availability in XMPP (No Show Value)","Mera disponibilidade no XMPP (Sem valor para ser exibido)"}. {"Message body","Corpo da mensagem"}. {"Message not found in forwarded payload","Mensagem não encontrada em conteúdo encaminhado"}. {"Messages from strangers are rejected","As mensagens vindas de estranhos são rejeitadas"}. {"Messages of type headline","Mensagens do tipo do título"}. {"Messages of type normal","Mensagens do tipo normal"}. {"Middle Name","Nome do meio"}. {"Minimum interval between voice requests (in seconds)","O intervalo mínimo entre requisições de voz (em segundos)"}. {"Moderator privileges required","Se necessita privilégios de moderador"}. {"Moderator","Moderador"}. {"Moderators Only","Somente moderadores"}. {"Modified modules","Módulos atualizados"}. {"Module failed to handle the query","Módulo falhou ao processar a consulta"}. {"Monday","Segunda"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","Vários elementos <item/> não são permitidos pela RFC6121"}. {"Multi-User Chat","Chat multi-usuário"}. {"Name in the rosters where this group will be displayed","O nome nas listas onde este grupo será exibido"}. {"Name","Nome"}. {"Name:","Nome:"}. {"Natural Language for Room Discussions","Idioma nativo para as discussões na sala"}. {"Natural-Language Room Name","Nome da sala no idioma nativo"}. {"Neither 'jid' nor 'nick' attribute found","Nem o atributo 'jid' nem 'nick' foram encontrados"}. {"Neither 'role' nor 'affiliation' attribute found","Nem o atributo 'role' nem 'affiliation' foram encontrados"}. {"Never","Nunca"}. {"New Password:","Nova Senha:"}. {"Nickname can't be empty","O apelido não pode ser vazio"}. {"Nickname Registration at ","Registro do apelido em "}. {"Nickname ~s does not exist in the room","O apelido ~s não existe na sala"}. {"Nickname","Apelido"}. {"No address elements found","Nenhum elemento endereço foi encontrado"}. {"No addresses element found","Nenhum elemento endereços foi encontrado"}. {"No 'affiliation' attribute found","Atributo 'affiliation' não foi encontrado"}. {"No available resource found","Nenhum recurso disponível foi encontrado"}. {"No body provided for announce message","Nenhum corpo de texto fornecido para anunciar mensagem"}. {"No child elements found","Nenhum elemento filho foi encontrado"}. {"No data form found","Nenhum formulário de dados foi encontrado"}. {"No Data","Nenhum dado"}. {"No features available","Nenhuma funcionalidade disponível"}. {"No <forwarded/> element found","Nenhum elemento <forwarded/> foi encontrado"}. {"No hook has processed this command","Nenhum hook processou este comando"}. {"No info about last activity found","Não foi encontrada informação sobre última atividade"}. {"No 'item' element found","O elemento 'item' não foi encontrado"}. {"No items found in this query","Nenhum item encontrado nesta consulta"}. {"No limit","Ilimitado"}. {"No module is handling this query","Nenhum módulo está processando esta consulta"}. {"No node specified","Nenhum nó especificado"}. {"No 'password' found in data form","'password' não foi encontrado em formulário de dados"}. {"No 'password' found in this query","'password' não foi encontrado nesta consulta"}. {"No 'path' found in data form","'path' não foi encontrado em formulário de dados"}. {"No pending subscriptions found","Não foram encontradas subscrições"}. {"No privacy list with this name found","Nenhuma lista de privacidade encontrada com este nome"}. {"No private data found in this query","Nenhum dado privado encontrado nesta consulta"}. {"No running node found","Nenhum nó em execução foi encontrado"}. {"No services available","Não há serviços disponíveis"}. {"No statistics found for this item","Não foram encontradas estatísticas para este item"}. {"No 'to' attribute found in the invitation","Atributo 'to' não foi encontrado no convite"}. {"Nobody","Ninguém"}. {"Node already exists","Nó já existe"}. {"Node ID","ID do Tópico"}. {"Node index not found","O índice do nó não foi encontrado"}. {"Node not found","Nó não encontrado"}. {"Node ~p","Nó ~p"}. {"Node","Nó"}. {"Nodeprep has failed","Processo de identificação de nó falhou (nodeprep)"}. {"Nodes","Nós"}. {"None","Nenhum"}. {"Not allowed","Não é permitido"}. {"Not Found","Não encontrado"}. {"Not subscribed","Não subscrito"}. {"Notify subscribers when items are removed from the node","Notificar assinantes quando itens forem eliminados do nó"}. {"Notify subscribers when the node configuration changes","Notificar assinantes a configuração do nó mudar"}. {"Notify subscribers when the node is deleted","Notificar assinantes quando o nó for eliminado se elimine"}. {"November","Novembro"}. {"Number of answers required","Quantidade de respostas necessárias"}. {"Number of occupants","Número de participantes"}. {"Number of Offline Messages","Quantidade das mensagens offline"}. {"Number of online users","Número de usuários online"}. {"Number of registered users","Número de usuários registrados"}. {"Number of seconds after which to automatically purge items","Quantidade de segundos para excluir os itens automaticamente"}. {"Occupants are allowed to invite others","As pessoas estão autorizadas a convidar outras pessoas"}. {"Occupants May Change the Subject","As pessoas talvez possam alterar o assunto"}. {"October","Outubro"}. {"Offline Messages","Mensagens offline"}. {"Offline Messages:","Mensagens offline:"}. {"OK","OK"}. {"Old Password:","Senha Antiga:"}. {"Online Users","Usuários conectados"}. {"Online Users:","Usuários online:"}. {"Online","Conectado"}. {"Only admins can see this","Apenas administradores podem ver isso"}. {"Only collection node owners may associate leaf nodes with the collection","Apenas um grupo dos proprietários dos nós podem associar as páginas na coleção"}. {"Only deliver notifications to available users","Somente enviar notificações aos usuários disponíveis"}. {"Only <enable/> or <disable/> tags are allowed","Apenas tags <enable/> ou <disable/> são permitidas"}. {"Only <list/> element is allowed in this query","Apenas elemento <list/> é permitido nesta consulta"}. {"Only members may query archives of this room","Somente os membros podem procurar nos arquivos desta sala"}. {"Only moderators and participants are allowed to change the subject in this room","Somente os moderadores e os participamentes podem alterar o assunto desta sala"}. {"Only moderators are allowed to change the subject in this room","Somente os moderadores podem alterar o assunto desta sala"}. {"Only moderators can approve voice requests","Somente moderadores podem aprovar requisições de voz"}. {"Only occupants are allowed to send messages to the conference","Somente os ocupantes podem enviar mensagens à sala de conferência"}. {"Only occupants are allowed to send queries to the conference","Somente os ocupantes podem enviar consultas à sala de conferência"}. {"Only publishers may publish","Apenas os editores podem publicar"}. {"Only service administrators are allowed to send service messages","Apenas administradores possuem permissão para enviar mensagens de serviço"}. {"Only those on a whitelist may associate leaf nodes with the collection","Apenas aqueles presentes em uma lista branca podem associar páginas na coleção"}. {"Only those on a whitelist may subscribe and retrieve items","Apenas aqueles presentes em uma lista branca podem se inscrever e recuperar os itens"}. {"Organization Name","Nome da organização"}. {"Organization Unit","Departamento/Unidade"}. {"Outgoing s2s Connections","Conexões s2s de Saída"}. {"Outgoing s2s Connections:","Saída das conexões s2s:"}. {"Owner privileges required","Se requer privilégios de proprietário da sala"}. {"Packet relay is denied by service policy","A retransmissão de pacote é negada por causa da política de serviço"}. {"Packet","Pacote"}. {"Participant","Participante"}. {"Password Verification:","Verificação da Senha:"}. {"Password Verification","Verificação de Senha"}. {"Password","Senha"}. {"Password:","Senha:"}. {"Path to Dir","Caminho para o diretório"}. {"Path to File","Caminho do arquivo"}. {"Payload type","Tipo da carga útil"}. {"Pending","Pendente"}. {"Period: ","Período: "}. {"Persist items to storage","Persistir elementos ao armazenar"}. {"Persistent","Persistente"}. {"Ping query is incorrect","A consulta ping está incorreta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Observe que tais opções farão backup apenas da base de dados Mnesia. Caso você esteja utilizando o modulo ODBC, você precisará fazer backup de sua base de dados SQL separadamente."}. {"Please, wait for a while before sending new voice request","Por favor, espere antes de enviar uma nova requisição de voz"}. {"Pong","Pong"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Possuir o atributo 'ask' não é permitido pela RFC6121"}. {"Present real Jabber IDs to","Tornar o Jabber ID real visível por"}. {"Previous session not found","A sessão anterior não foi encontrada"}. {"Previous session PID has been killed","O PID da sessão anterior foi excluído"}. {"Previous session PID has exited","O PID da sessão anterior foi encerrado"}. {"Previous session PID is dead","O PID da sessão anterior está morto"}. {"Previous session timed out","A sessão anterior expirou"}. {"private, ","privado, "}. {"Public","Público"}. {"Publish model","Publicar o modelo"}. {"Publish-Subscribe","Publicação de Tópico"}. {"PubSub subscriber request","PubSub requisição de assinante"}. {"Purge all items when the relevant publisher goes offline","Descartar todos os itens quando o publicante principal estiver offline"}. {"Push record not found","O registro push não foi encontrado"}. {"Queries to the conference members are not allowed in this room","Nesta sala de conferência, consultas aos membros não são permitidas"}. {"Query to another users is forbidden","Consultar a outro usuário é proibido"}. {"RAM and disc copy","Cópias na RAM e disco rígido"}. {"RAM copy","Cópia em RAM"}. {"Really delete message of the day?","Deletar realmente a mensagem do dia?"}. {"Receive notification from all descendent nodes","Receba a notificação de todos os nós descendentes"}. {"Receive notification from direct child nodes only","Receba apenas as notificações dos nós relacionados"}. {"Receive notification of new items only","Receba apenas as notificações dos itens novos"}. {"Receive notification of new nodes only","Receba apenas as notificações dos nós novos"}. {"Recipient is not in the conference room","O receptor não está na sala de conferência"}. {"Register an XMPP account","Registre uma conta XMPP"}. {"Registered Users:","Usuários registrados:"}. {"Registered Users","Usuários Registrados"}. {"Register","Registrar"}. {"Remote copy","Cópia remota"}. {"Remove All Offline Messages","Remover Todas as Mensagens Offline"}. {"Remove User","Remover usuário"}. {"Remove","Remover"}. {"Replaced by new connection","Substituído por nova conexão"}. {"Request has timed out","O pedido expirou"}. {"Request is ignored","O pedido foi ignorado"}. {"Requested role","Função solicitada"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar Serviço"}. {"Restart","Reiniciar"}. {"Restore Backup from File at ","Restaurar backup a partir do arquivo em "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar backup binário após reinicialização do ejabberd (requer menos memória):"}. {"Restore binary backup immediately:","Restaurar imediatamente o backup binário:"}. {"Restore plain text backup immediately:","Restaurar backup formato texto imediatamente:"}. {"Restore","Restaurar"}. {"Roles and Affiliations that May Retrieve Member List","As funções e as afiliações que podem recuperar a lista dos membros"}. {"Roles for which Presence is Broadcasted","Para quem a presença será notificada"}. {"Roles that May Send Private Messages","Atribuições que talvez possam enviar mensagens privadas"}. {"Room Configuration","Configuração de salas"}. {"Room creation is denied by service policy","Sala não pode ser criada devido à política do serviço"}. {"Room description","Descrição da Sala"}. {"Room Occupants","Número de participantes"}. {"Room terminates","Terminação da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Listar grupos autorizados"}. {"Roster of ~ts","Lista de ~ts"}. {"Roster size","Tamanho da Lista"}. {"Roster:","Lista:"}. {"RPC Call Error","Erro de chamada RPC"}. {"Running Nodes","Nós em execução"}. {"~s invites you to the room ~s","~s convidaram você para a sala ~s"}. {"Saturday","Sábado"}. {"Script check","Verificação de Script"}. {"Search from the date","Pesquise a partir da data"}. {"Search Results for ","Resultados de pesquisa para "}. {"Search the text","Pesquise o texto"}. {"Search until the date","Pesquise até a data"}. {"Search users in ","Procurar usuários em "}. {"Select All","Selecione tudo"}. {"Send announcement to all online users on all hosts","Enviar anúncio a todos usuários online em todas as máquinas"}. {"Send announcement to all online users","Enviar anúncio a todos os usuárions online"}. {"Send announcement to all users on all hosts","Enviar aviso para todos os usuários em todos os hosts"}. {"Send announcement to all users","Enviar anúncio a todos os usuários"}. {"September","Setembro"}. {"Server:","Servidor:"}. {"Service list retrieval timed out","A recuperação da lista dos serviços expirou"}. {"Session state copying timed out","A cópia do estado da sessão expirou"}. {"Set message of the day and send to online users","Definir mensagem do dia e enviar a todos usuários online"}. {"Set message of the day on all hosts and send to online users","Definir mensagem do dia em todos os hosts e enviar para os usuários online"}. {"Shared Roster Groups","Grupos Shared Roster"}. {"Show Integral Table","Mostrar Tabela Integral"}. {"Show Ordinary Table","Mostrar Tabela Ordinária"}. {"Shut Down Service","Parar Serviço"}. {"SOCKS5 Bytestreams","Bytestreams SOCKS5"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Alguns clientes XMPP podem armazenar a sua senha no seu computador, só faça isso no seu computador particular por questões de segurança."}. {"Specify the access model","Especificar os modelos de acesso"}. {"Specify the event message type","Especificar o tipo de mensagem para o evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. {"Stanza ID","ID da estrofe"}. {"Statically specify a replyto of the node owner(s)","Defina uma resposta fixa do(s) proprietário(s) do nó"}. {"Statistics of ~p","Estatísticas de ~p"}. {"Statistics","Estatísticas"}. {"Stop","Parar"}. {"Stopped Nodes","Nós parados"}. {"Storage Type","Tipo de armazenamento"}. {"Store binary backup:","Armazenar backup binário:"}. {"Store plain text backup:","Armazenar backup em texto:"}. {"Stream management is already enabled","A gestão do fluxo já está ativada"}. {"Stream management is not enabled","O gerenciamento do fluxo não está ativado"}. {"Subject","Assunto"}. {"Submit","Enviar"}. {"Submitted","Submetido"}. {"Subscriber Address","Endereço dos Assinantes"}. {"Subscribers may publish","Os assinantes podem publicar"}. {"Subscription requests must be approved and only subscribers may retrieve items","Os pedidos de assinatura devem ser aprovados e apenas os assinantes podem recuperar os itens"}. {"Subscriptions are not allowed","Subscrições não estão permitidas"}. {"Subscription","Subscrição"}. {"Sunday","Domingo"}. {"Text associated with a picture","Um texto associado a uma imagem"}. {"Text associated with a sound","Um texto associado a um som"}. {"Text associated with a video","Um texto associado a um vídeo"}. {"Text associated with speech","Um texto associado à fala"}. {"That nickname is already in use by another occupant","O apelido (nick) já está sendo utilizado"}. {"That nickname is registered by another person","O apelido já está registrado por outra pessoa"}. {"The account already exists","A conta já existe"}. {"The account was not unregistered","A conta não estava não registrada"}. {"The body text of the last received message","O corpo do texto da última mensagem que foi recebida"}. {"The CAPTCHA is valid.","O CAPTCHA é inválido."}. {"The CAPTCHA verification has failed","A verificação do CAPTCHA falhou"}. {"The captcha you entered is wrong","O captcha que você digitou está errado"}. {"The child nodes (leaf or collection) associated with a collection","Os nós relacionados (página ou coleção) associados com uma coleção"}. {"The collections with which a node is affiliated","As coleções com as quais o nó está relacionado"}. {"The DateTime at which a leased subscription will end or has ended","A data e a hora que uma assinatura alugada terminará ou terá terminado"}. {"The datetime when the node was created","A data em que o nó foi criado"}. {"The default language of the node","O idioma padrão do nó"}. {"The feature requested is not supported by the conference","A funcionalidade solicitada não é suportada pela sala de conferência"}. {"The JID of the node creator","O JID do criador do nó"}. {"The JIDs of those to contact with questions","Os JIDs daqueles para entrar em contato com perguntas"}. {"The JIDs of those with an affiliation of owner","Os JIDs daqueles com uma afiliação de proprietário"}. {"The JIDs of those with an affiliation of publisher","Os JIDs daqueles com uma afiliação de editor"}. {"The list of JIDs that may associate leaf nodes with a collection","A lista dos JIDs que podem associar as páginas dos nós em uma coleção"}. {"The maximum number of child nodes that can be associated with a collection","A quantidade máxima dos nós relacionados que podem ser associados com uma coleção"}. {"The minimum number of milliseconds between sending any two notification digests","O número mínimo de milissegundos entre o envio do resumo das duas notificações"}. {"The name of the node","O nome do nó"}. {"The node is a collection node","O nó é um nó da coleção"}. {"The node is a leaf node (default)","O nó é uma página do nó (padrão)"}. {"The NodeID of the relevant node","O NodeID do nó relevante"}. {"The number of pending incoming presence subscription requests","A quantidade pendente dos pedidos da presença da assinatura"}. {"The number of subscribers to the node","A quantidade dos assinantes para o nó"}. {"The number of unread or undelivered messages","A quantidade das mensagens que não foram lidas ou não foram entregues"}. {"The password contains unacceptable characters","A senha contém caracteres proibidos"}. {"The password is too weak","Senha considerada muito fraca"}. {"the password is","a senha é"}. {"The password of your XMPP account was successfully changed.","A senha da sua conta XMPP foi alterada com sucesso."}. {"The password was not changed","A senha não foi alterada"}. {"The passwords are different","As senhas não batem"}. {"The presence states for which an entity wants to receive notifications","As condições da presença para os quais uma entidade queira receber as notificações"}. {"The query is only allowed from local users","Esta consulta só é permitida a partir de usuários locais"}. {"The query must not contain <item/> elements","A consulta não pode conter elementos <item/>"}. {"The room subject can be modified by participants","O tema da sala pode ser alterada pelos próprios participantes"}. {"The sender of the last received message","O remetente da última mensagem que foi recebida"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","A instância DEVE conter apenas um elemento <active/>, um elemento <default/>, ou um elemento <list/>"}. {"The subscription identifier associated with the subscription request","O identificador da assinatura associado à solicitação da assinatura"}. {"The type of node data, usually specified by the namespace of the payload (if any)","O tipo dos dados do nó, normalmente definido pelo espaço dos nomes da carga útil (caso haja)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","O URL da transformação XSL que pode ser aplicada nas cargas úteis para gerar um elemento apropriado no corpo da mensagem."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","A URL de uma transformação XSL que pode ser aplicada ao formato de carga útil para gerar um Formulário de Dados válido onde o cliente possa exibir usando um mecanismo genérico de renderização do Formulários de Dados"}. {"The username is not valid","O nome do usuário não é válido"}. {"There was an error changing the password: ","Houve um erro ao alterar a senha: "}. {"There was an error creating the account: ","Houve um erro ao criar esta conta: "}. {"There was an error deleting the account: ","Houve um erro ao deletar esta conta: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","O tamanho da caixa não importa: macbeth é o mesmo que MacBeth e Macbeth."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Esta pagina permite a criação de novas contas XMPP neste servidor. O seu JID (Identificador Jabber) será da seguinte maneira: usuário@servidor. Por favor, leia cuidadosamente as instruções para preencher todos os campos corretamente."}. {"This page allows to unregister an XMPP account in this XMPP server.","Esta página permite a exclusão de uma conta XMPP neste servidor."}. {"This room is not anonymous","Essa sala não é anônima"}. {"This service can not process the address: ~s","Este serviço não pode processar o endereço: ~s"}. {"Thursday","Quinta"}. {"Time delay","Intervalo (Tempo)"}. {"Timed out waiting for stream resumption","Tempo limite expirou durante à espera da retomada da transmissão"}. {"Time","Tempo"}. {"To register, visit ~s","Para registrar, visite ~s"}. {"To ~ts","Para ~s"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Quantidade excessiva de bytestreams ativos"}. {"Too many CAPTCHA requests","Número excessivo de requisições para o CAPTCHA"}. {"Too many child elements","Quantidade excessiva de elementos filho"}. {"Too many <item/> elements","Número excessivo de elementos <item/>"}. {"Too many <list/> elements","Número excessivo de elementos <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Número excessivo (~p) de tentativas falhas de autenticação (~s). O endereço será desbloqueado às ~s UTC"}. {"Too many receiver fields were specified","Foram definidos receptores demais nos campos"}. {"Too many unacked stanzas","Número excessivo de instâncias sem confirmação"}. {"Too many users in this conference","Há uma quantidade excessiva de usuários nesta conferência"}. {"To","Para"}. {"Total rooms","Salas no total"}. {"Traffic rate limit is exceeded","Limite de banda excedido"}. {"Transactions Aborted:","Transações abortadas:"}. {"Transactions Committed:","Transações salvas:"}. {"Transactions Logged:","Transações de log:"}. {"Transactions Restarted:","Transações reiniciadas:"}. {"~ts's Offline Messages Queue","~s's Fila de Mensagens Offline"}. {"Tuesday","Terça"}. {"Unable to generate a CAPTCHA","Impossível gerar um CAPTCHA"}. {"Unable to register route on existing local domain","Não foi possível registrar rota no domínio local existente"}. {"Unauthorized","Não Autorizado"}. {"Unexpected action","Ação inesperada"}. {"Unexpected error condition: ~p","Condição de erro inesperada: ~p"}. {"Unregister an XMPP account","Excluir uma conta XMPP"}. {"Unregister","Deletar registro"}. {"Unselect All","Desmarcar todos"}. {"Unsupported <index/> element","Elemento <index/> não suportado"}. {"Unsupported version","Versão sem suporte"}. {"Update message of the day (don't send)","Atualizar mensagem do dia (não enviar)"}. {"Update message of the day on all hosts (don't send)","Atualizar a mensagem do dia em todos os host (não enviar)"}. {"Update ~p","Atualizar ~p"}. {"Update plan","Plano de Atualização"}. {"Update script","Script de atualização"}. {"Update","Atualizar"}. {"Uptime:","Tempo de atividade:"}. {"URL for Archived Discussion Logs","A URL para o arquivamento dos registros da discussão"}. {"User already exists","Usuário já existe"}. {"User (jid)","Usuário (jid)"}. {"User JID","Usuário JID"}. {"User Management","Gerenciamento de Usuários"}. {"User removed","O usuário foi removido"}. {"User session not found","A sessão do usuário não foi encontrada"}. {"User session terminated","Sessão de usuário terminada"}. {"User ~ts","Usuário ~s"}. {"Username:","Usuário:"}. {"Users are not allowed to register accounts so quickly","Usuários não estão autorizados a registrar contas imediatamente"}. {"Users Last Activity","Últimas atividades dos usuários"}. {"Users","Usuários"}. {"User","Usuário"}. {"Validate","Validar"}. {"Value 'get' of 'type' attribute is not allowed","Valor 'get' não permitido para atributo 'type'"}. {"Value of '~s' should be boolean","Value de '~s' deveria ser um booleano"}. {"Value of '~s' should be datetime string","Valor de '~s' deveria ser data e hora"}. {"Value of '~s' should be integer","Valor de '~s' deveria ser um inteiro"}. {"Value 'set' of 'type' attribute is not allowed","Valor 'set' não permitido para atributo 'type'"}. {"vCard User Search","Busca de Usuário vCard"}. {"View Queue","Exibir a fila"}. {"View Roster","Ver a lista"}. {"Virtual Hosts","Hosts virtuais"}. {"Visitors are not allowed to change their nicknames in this room","Nesta sala, os visitantes não podem mudar seus apelidos"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes não podem enviar mensagens a todos os ocupantes"}. {"Visitor","Visitante"}. {"Voice request","Requisição de voz"}. {"Voice requests are disabled in this conference","Requisições de voz estão desabilitadas nesta sala de conferência"}. {"Wednesday","Quarta"}. {"When a new subscription is processed and whenever a subscriber comes online","Quando uma nova assinatura é processada e sempre que um assinante fica online"}. {"When a new subscription is processed","Quando uma nova assinatura é processada"}. {"When to send the last published item","Quando enviar o último tópico publicado"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Caso uma entidade queira receber o corpo de uma mensagem XMPP além do formato de carga útil"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Caso uma entidade queira receber os resumos (as agregações) das notificações ou todas as notificações individualmente"}. {"Whether an entity wants to receive or disable notifications","Caso uma entidade queira receber ou desativar as notificações"}. {"Whether owners or publisher should receive replies to items","Caso os proprietários ou a editora devam receber as respostas nos itens"}. {"Whether the node is a leaf (default) or a collection","Caso o nó seja uma folha (padrão) ou uma coleção"}. {"Whether to allow subscriptions","Permitir subscrições"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Caso todas as assinaturas devam ser temporárias, com base na presença do assinante"}. {"Whether to notify owners about new subscribers and unsubscribes","Caso deva notificar os proprietários sobre os novos assinantes e aqueles que cancelaram a assinatura"}. {"Who may associate leaf nodes with a collection","Quem pode associar as folhas dos nós em uma coleção"}. {"Wrong parameters in the web formulary","O formulário web está com os parâmetros errados"}. {"Wrong xmlns","Xmlns errado"}. {"XMPP Account Registration","Registo da Conta XMPP"}. {"XMPP Domains","Domínios XMPP"}. {"XMPP Show Value of Away","XMPP Exiba o valor da ausência"}. {"XMPP Show Value of Chat","XMPP Exiba o valor do chat"}. {"XMPP Show Value of DND (Do Not Disturb)","XMPP Exiba o valor do DND (Não Perturbe)"}. {"XMPP Show Value of XA (Extended Away)","XMPP Exiba o valor do XA (Ausência Estendida)"}. {"XMPP URI of Associated Publish-Subscribe Node","XMPP URI da publicação do nó associado da assinatura"}. {"You are being removed from the room because of a system shutdown","Você está sendo removido da sala por causa do desligamento do sistema"}. {"You are not joined to the channel","Você não está inscrito no canal"}. {"You can later change your password using an XMPP client.","Você pode alterar a sua senha mais tarde usando um cliente XMPP."}. {"You have been banned from this room","Você foi banido desta sala"}. {"You have joined too many conferences","Você entrou em um número excessivo de salas de conferência"}. {"You must fill in field \"Nickname\" in the form","Você deve completar o campo \"Apelido\" no formulário"}. {"You need a client that supports x:data and CAPTCHA to register","Você precisa de um cliente com suporte de x:data para poder registrar o apelido"}. {"You need a client that supports x:data to register the nickname","Você precisa de um cliente com suporte a x:data para registrar o seu apelido"}. {"You need an x:data capable client to search","Necessitas um cliente com suporte de x:data para poder buscar"}. {"Your active privacy list has denied the routing of this stanza.","Sua lista de privacidade ativa negou o roteamento desta instância."}. {"Your contact offline message queue is full. The message has been discarded.","A fila de contatos offline esta cheia. A sua mensagem foi descartada."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Suas mensagens para ~s estão bloqueadas. Para desbloqueá-las, visite: ~s"}. {"Your XMPP account was successfully registered.","A sua conta XMPP foi registrada com sucesso."}. {"Your XMPP account was successfully unregistered.","Sua conta XMPP foi excluída com sucesso."}. {"You're not allowed to create nodes","Você não tem autorização para criar nós"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/eo.msg���������������������������������������������������������������������0000644�0002322�0002322�00000057330�14154362354�016706� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Aldonu * al la fino de la kampo por kongruigi subĉenon)"}. {" has set the subject to: "," ŝanĝis la temon al: "}. {"# participants","Nombro de partoprenantoj"}. {"A description of the node","Priskribo de la nodo"}. {"A friendly name for the node","Kromnomo de la nodo"}. {"A password is required to enter this room","Pasvorto estas bezonata por eniri ĉi tiun babilejon"}. {"A Web Page","Retpaĝo"}. {"Accept","Akcepti"}. {"Access denied by service policy","Atingo rifuzita de serv-politiko"}. {"Access model of open","Atingomodelo de malfermo"}. {"Access model of presence","Atingomodelo de ĉeesto"}. {"Access model of whitelist","Atingomodelo de permesolisto"}. {"Access model","Atingomodelo"}. {"Account doesn't exist","Konto ne ekzistas"}. {"Action on user","Ago je uzanto"}. {"Add Jabber ID","Aldonu Jabber ID"}. {"Add New","Aldonu novan"}. {"Add User","Aldonu Uzanton"}. {"Administration of ","Mastrumado de "}. {"Administration","Administro"}. {"Administrator privileges required","Administrantaj rajtoj bezonata"}. {"All activity","Ĉiu aktiveco"}. {"All Users","Ĉiuj Uzantoj"}. {"Allow subscription","Permesi abonon"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Ĉu permesi ĉi tiun Jabber ID aboni al la jena PubAbo-nodo?"}. {"Allow users to change the subject","Permesu uzantojn ŝanĝi la temon"}. {"Allow users to query other users","Permesu uzantojn informpeti aliajn uzantojn"}. {"Allow users to send invites","Permesu uzantojn sendi invitojn"}. {"Allow users to send private messages","Permesu uzantojn sendi privatajn mesaĝojn"}. {"Allow visitors to change nickname","Permesu al vizitantoj ŝanĝi siajn kaŝnomojn"}. {"Allow visitors to send private messages to","Permesu uzantojn sendi privatajn mesaĝojn al"}. {"Allow visitors to send status text in presence updates","Permesu al vizitantoj sendi statmesaĝon en ĉeest-sciigoj"}. {"Allow visitors to send voice requests","Permesu uzantojn sendi voĉ-petojn"}. {"Announcements","Anoncoj"}. {"Answer associated with a picture","Respondo asociita kun bildo"}. {"Answer associated with a video","Respondo asociita kun filmeto"}. {"Answer associated with speech","Respondo asociita kun parolo"}. {"Answer to a question","Respondo al demando"}. {"Anyone may publish","Ĉiu rajtas publici"}. {"Anyone with Voice","Iu ajn kun Voĉo"}. {"Anyone","Iu ajn"}. {"April","Aprilo"}. {"Attribute 'channel' is required for this request","Atributo 'channel' necesas por ĉi tiu peto"}. {"Attribute 'id' is mandatory for MIX messages","Atributo 'id' estas deviga en MIX-mesaĝo"}. {"Attribute 'jid' is not allowed here","Atributo 'jid' ne estas permesita ĉi tie"}. {"Attribute 'node' is not allowed here","Atributo 'node' ne estas permesita ĉi tie"}. {"August","Aŭgusto"}. {"Backup Management","Mastrumado de sekurkopioj"}. {"Backup of ~p","Sekurkopio de ~p"}. {"Backup to File at ","Faru sekurkopion je "}. {"Backup","Faru Sekurkopion"}. {"Bad format","Malĝusta formo"}. {"Birthday","Naskiĝtago"}. {"Cannot remove active list","Ne povas forigi aktivan liston"}. {"CAPTCHA web page","CAPTCHA teksaĵ-paĝo"}. {"Challenge ID","Identigilo de Defio"}. {"Change Password","Ŝanĝu pasvorton"}. {"Change User Password","Ŝanĝu pasvorton de uzanto"}. {"Channel already exists","Kanalo jam ekzistas"}. {"Channel does not exist","Kanalo ne ekzistas"}. {"Channels","Kanaloj"}. {"Characters not allowed:","Karaktroj ne permesata:"}. {"Chatroom configuration modified","Agordo de babilejo ŝanĝita"}. {"Chatroom is created","Babilejo kreita"}. {"Chatroom is destroyed","Babilejo neniigita"}. {"Chatroom is started","Babilejo lanĉita"}. {"Chatroom is stopped","Babilejo haltita"}. {"Chatrooms","Babilejoj"}. {"Choose a username and password to register with this server","Elektu uzantnomon kaj pasvorton por registri je ĉi tiu servilo"}. {"Choose storage type of tables","Elektu konserv-tipon de tabeloj"}. {"Choose whether to approve this entity's subscription.","Elektu ĉu permesi la abonon de ĉi tiu ento."}. {"City","Urbo"}. {"Commands","Ordonoj"}. {"Conference room does not exist","Babilejo ne ekzistas"}. {"Configuration of room ~s","Agordo de babilejo ~s"}. {"Configuration","Agordo"}. {"Connected Resources:","Konektataj risurcoj:"}. {"Country","Lando"}. {"CPU Time:","CPU-tempo"}. {"Current Discussion Topic","Aktuala Diskuta Temo"}. {"Database Tables at ~p","Datumbaz-tabeloj je ~p"}. {"Database Tables Configuration at ","Agordo de datumbaz-tabeloj je "}. {"Database","Datumbazo"}. {"December","Decembro"}. {"Default users as participants","Kutime farigu uzantojn kiel partpoprenantoj"}. {"Delete message of the day on all hosts","Forigu mesaĝo de la tago je ĉiu gastigo"}. {"Delete message of the day","Forigu mesaĝo de la tago"}. {"Delete Selected","Forigu elektata(j)n"}. {"Delete User","Forigu Uzanton"}. {"Deliver event notifications","Liveru event-sciigojn"}. {"Deliver payloads with event notifications","Liveru aĵojn de event-sciigoj"}. {"Description:","Priskribo:"}. {"Disc only copy","Nur disk-kopio"}. {"Dump Backup to Text File at ","Skribu sekurkopion en plata teksto al "}. {"Dump to Text File","Skribu en plata tekst-dosiero"}. {"Duplicated groups are not allowed by RFC6121","RFC 6121 ne permesas duplikatajn grupojn"}. {"Edit Properties","Redaktu atributojn"}. {"Either approve or decline the voice request.","Ĉu aprobu, aŭ malaprobu la voĉ-peton."}. {"ejabberd MUC module","ejabberd MUC-modulo"}. {"ejabberd Multicast service","ejabberd Multicast-servo"}. {"ejabberd Publish-Subscribe module","ejabberd Public-Abonada modulo"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bajtfluo modulo"}. {"ejabberd vCard module","ejabberd vCard-modulo"}. {"ejabberd Web Admin","ejabberd Teksaĵa Administro"}. {"ejabberd","ejabberd"}. {"Elements","Eroj"}. {"Email Address","Retpoŝta Adreso"}. {"Email","Retpoŝto"}. {"Enable logging","Ŝaltu protokoladon"}. {"Enable message archiving","Ŝaltu mesaĝo-arkivo"}. {"End User Session","Haltigu Uzant-seancon"}. {"Enter nickname you want to register","Enmetu kaŝnomon kiun vi volas registri"}. {"Enter path to backup file","Enmetu vojon por sekurkopio"}. {"Enter path to jabberd14 spool dir","Enmetu vojon al jabberd14-uzantdosierujo"}. {"Enter path to jabberd14 spool file","Enmetu vojon al jabberd14-uzantdosiero"}. {"Enter path to text file","Enmetu vojon al plata teksto"}. {"Enter the text you see","Enmetu montrita teksto"}. {"Error","Eraro"}. {"Exclude Jabber IDs from CAPTCHA challenge","Esceptu Ĵabber-identigilojn je CAPTCHA-defio"}. {"Export all tables as SQL queries to a file:","Eksportu ĉiuj tabeloj kiel SQL-informmendo al dosierujo:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksportu datumojn de ĉiuj uzantoj en servilo al PIEFXIS dosieroj (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksportu datumoj de uzantoj en gastigo al PIEFXIS dosieroj (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Malsukcesis ekstrakti JID-on de via voĉ-pet-aprobo"}. {"Family Name","Lasta Nomo"}. {"February","Februaro"}. {"File larger than ~w bytes","Dosiero pli granda ol ~w bajtoj"}. {"Friday","Vendredo"}. {"From","De"}. {"Full Name","Plena Nomo"}. {"Get Number of Online Users","Montru nombron de konektataj uzantoj"}. {"Get Number of Registered Users","Montru nombron de registritaj uzantoj"}. {"Get User Last Login Time","Montru tempon de lasta ensaluto"}. {"Get User Password","Montru pasvorton de uzanto"}. {"Get User Statistics","Montru statistikojn de uzanto"}. {"Given Name","Persona Nomo"}. {"Grant voice to this person?","Koncedu voĉon al ĉi-persono?"}. {"Group","Grupo"}. {"Groups","Grupoj"}. {"has been banned","estas forbarita"}. {"has been kicked because of a system shutdown","estas forpelita pro sistem-haltigo"}. {"has been kicked because of an affiliation change","estas forpelita pro aparteneca ŝanĝo"}. {"has been kicked because the room has been changed to members-only","estas forpelita ĉar la babilejo fariĝis sole por membroj"}. {"has been kicked","estas forpelita"}. {"Host","Gastigo"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se vi ne vidas la CAPTCHA-imagon jene, vizitu la teksaĵ-paĝon."}. {"Import Directory","Importu dosierujo"}. {"Import File","Importu dosieron"}. {"Import user data from jabberd14 spool file:","Importu uzantojn de jabberd14-uzantdosieroj"}. {"Import User from File at ","Importu uzanton de dosiero el "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importu uzanto-datumojn de PIEFXIS dosiero (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importu uzantojn de jabberd14-uzantdosieroj"}. {"Import Users from Dir at ","Importu uzantojn de dosierujo ĉe "}. {"Import Users From jabberd14 Spool Files","Importu uzantojn de jabberd14-uzantdosieroj"}. {"Improper message type","Malĝusta mesaĝo-tipo"}. {"Incorrect CAPTCHA submit","Neĝusta CAPTCHA-submeto"}. {"Incorrect password","Nekorekta pasvorto"}. {"IP addresses","IP-adresoj"}. {"is now known as","nun nomiĝas"}. {"It is not allowed to send private messages of type \"groupchat\"","Malpermesas sendi mesaĝojn de tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Nur partoprenantoj rajtas sendi privatajn mesaĝojn al la babilejo"}. {"It is not allowed to send private messages","Ne estas permesata sendi privatajn mesaĝojn"}. {"Jabber ID","Jabber ID"}. {"January","Januaro"}. {"joins the room","eniras la babilejo"}. {"July","Julio"}. {"June","Junio"}. {"Just created","Ĵus kreita"}. {"Label:","Etikedo:"}. {"Last Activity","Lasta aktiveco"}. {"Last login","Lasta ensaluto"}. {"Last month","Lasta monato"}. {"Last year","Lasta jaro"}. {"leaves the room","eliras la babilejo"}. {"List of rooms","Listo de babilejoj"}. {"Low level update script","Bazanivela ĝisdatigo-skripto"}. {"Make participants list public","Farigu partoprento-liston publika"}. {"Make room CAPTCHA protected","Protektu babilejon per CAPTCHA"}. {"Make room members-only","Farigu babilejon sole por membroj"}. {"Make room moderated","Farigu babilejon moderigata"}. {"Make room password protected","Farigu babilejon protektata per pasvorto"}. {"Make room persistent","Farigu babilejon daŭra"}. {"Make room public searchable","Farigu babilejon publike trovebla"}. {"March","Marĉo"}. {"Max # of items to persist","Maksimuma kiomo de eroj en konservado"}. {"Max payload size in bytes","Maksimuma aĵo-grando je bajtoj"}. {"Maximum file size","Maksimuma grando de dosiero"}. {"Maximum Number of Occupants","Limigo de nombro de partoprenantoj"}. {"May","Majo"}. {"Membership is required to enter this room","Membreco estas bezonata por eniri ĉi tiun babilejon"}. {"Members:","Membroj:"}. {"Memory","Memoro"}. {"Message body","Teksto de mesaĝo"}. {"Middle Name","Meza Nomo"}. {"Minimum interval between voice requests (in seconds)","Minimuma intervalo inter voĉ-petoj (je sekundoj)"}. {"Moderator privileges required","Moderantaj rajtoj bezonata"}. {"Modified modules","Ĝisdatigitaj moduloj"}. {"Module failed to handle the query","Modulo malsukcesis trakti la informpeton"}. {"Monday","Lundo"}. {"Multicast","Multicast"}. {"Multiple <item/> elements are not allowed by RFC6121","RFC 6121 ne permesas plurajn <item/>-elementojn"}. {"Multi-User Chat","Grupbabilado"}. {"Name","Nomo"}. {"Name:","Nomo:"}. {"Natural Language for Room Discussions","Homa Lingvo por Diskutoj en Babilejo"}. {"Never","Neniam"}. {"New Password:","Nova Pasvorto:"}. {"Nickname Registration at ","Kaŝnomo-registrado je "}. {"Nickname ~s does not exist in the room","Kaŝnomo ~s ne ekzistas en la babilejo"}. {"Nickname","Kaŝnomo"}. {"No address elements found","Adresa elemento ne trovita"}. {"No addresses element found","Adresa elemento ne trovita"}. {"No body provided for announce message","Neniu teksto donita por anonc-mesaĝo"}. {"No child elements found","Ida elemento ne trovita"}. {"No Data","Neniu datumo"}. {"No <forwarded/> element found","Elemento <forwarded/> ne trovita"}. {"No items found in this query","Neniu elemento <item/> trovita en ĉi tiu informpeto"}. {"No limit","Neniu limigo"}. {"No module is handling this query","Neniu modulo traktas ĉi tiun informpeton"}. {"No 'password' found in this query","Neniu pasvorto trovita en ĉi tiu informpeto"}. {"No private data found in this query","Neniu privata dateno trovita en ĉi tiu informpeto"}. {"Node ID","Nodo ID"}. {"Node not found","Nodo ne trovita"}. {"Node ~p","Nodo ~p"}. {"Nodes","Nodoj"}. {"None","Nenio"}. {"Not Found","Ne trovita"}. {"Notify subscribers when items are removed from the node","Sciigu abonantoj kiam eroj estas forigita de la nodo"}. {"Notify subscribers when the node configuration changes","Sciigu abonantoj kiam la agordo de la nodo ŝanĝas"}. {"Notify subscribers when the node is deleted","Sciigu abonantoj kiam la nodo estas forigita"}. {"November","Novembro"}. {"Number of occupants","Nombro de ĉeestantoj"}. {"Number of online users","Nombro de konektataj uzantoj"}. {"Number of registered users","Nombro de registritaj uzantoj"}. {"October","Oktobro"}. {"Offline Messages","Liverontaj mesaĝoj"}. {"Offline Messages:","Liverontaj mesaĝoj"}. {"OK","Bone"}. {"Old Password:","Malnova Pasvorto:"}. {"Online Users:","Konektataj uzantoj:"}. {"Online Users","Konektataj Uzantoj"}. {"Online","Konektata"}. {"Only deliver notifications to available users","Nur liveru sciigojn al konektataj uzantoj"}. {"Only <list/> element is allowed in this query","Nur la elemento <list/> estas permesita en ĉi tiu informpeto"}. {"Only moderators and participants are allowed to change the subject in this room","Nur moderigantoj kaj partoprenantoj rajtas ŝanĝi la temon en ĉi tiu babilejo"}. {"Only moderators are allowed to change the subject in this room","Nur moderigantoj rajtas ŝanĝi la temon en ĉi tiu babilejo"}. {"Only moderators can approve voice requests","Nur moderigantoj povas aprobi voĉ-petojn"}. {"Only occupants are allowed to send messages to the conference","Nur partoprenantoj rajtas sendi mesaĝojn al la babilejo"}. {"Only occupants are allowed to send queries to the conference","Nur partoprenantoj rajtas sendi informmendojn al la babilejoj"}. {"Only publishers may publish","Nur publicantoj rajtas publici"}. {"Only service administrators are allowed to send service messages","Nur servo-administrantoj rajtas sendi serv-mesaĝojn"}. {"Only those on a whitelist may associate leaf nodes with the collection","Nur tiuj en permesolisto rajtas asocii foliajn nodojn kun la kolekto"}. {"Only those on a whitelist may subscribe and retrieve items","Nur tiuj en permesolisto rajtas aboni kaj preni erojn"}. {"Organization Name","Organiz-nomo"}. {"Organization Unit","Organiz-parto"}. {"Outgoing s2s Connections","Elirantaj s-al-s-konektoj"}. {"Outgoing s2s Connections:","Elirantaj s-al-s-konektoj:"}. {"Owner privileges required","Mastraj rajtoj bezonata"}. {"Packet","Pakaĵo"}. {"Password Verification","Pasvortkontrolo"}. {"Password Verification:","Pasvortkontrolo:"}. {"Password","Pasvorto"}. {"Password:","Pasvorto:"}. {"Path to Dir","Vojo al dosierujo"}. {"Path to File","Voje de dosiero"}. {"Pending","Atendanta"}. {"Period: ","Periodo: "}. {"Persist items to storage","Savu erojn en konservado"}. {"Ping","Sondaĵo"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Rimarku ke ĉi tiuj elektebloj nur sekurkopias la propran Mnesia-datumbazon. Se vi uzas la ODBC-modulon, vi ankaŭ devas sekurkopii tiujn SQL-datumbazoj aparte."}. {"Please, wait for a while before sending new voice request","Bonvolu atendi iomete antaŭ ol sendi plian voĉ-peton"}. {"Pong","Resondaĵo"}. {"Present real Jabber IDs to","Montru verajn Jabber ID-ojn al"}. {"private, ","privata, "}. {"Publish model","Publici modelon"}. {"Publish-Subscribe","Publici-Aboni"}. {"PubSub subscriber request","PubAbo abonpeto"}. {"Purge all items when the relevant publisher goes offline","Forigu ĉiujn erojn kiam la rilata publicanto malkonektiĝas"}. {"Queries to the conference members are not allowed in this room","Malpermesas informmendoj al partoprenantoj en ĉi tiu babilejo"}. {"Query to another users is forbidden","Informpeto al aliaj uzantoj estas malpermesita"}. {"RAM and disc copy","RAM- kaj disk-kopio"}. {"RAM copy","RAM-kopio"}. {"Really delete message of the day?","Ĉu vere forigi mesaĝon de la tago?"}. {"Recipient is not in the conference room","Ricevanto ne ĉeestas en la babilejo"}. {"Registered Users","Registritaj uzantoj"}. {"Registered Users:","Registritaj uzantoj:"}. {"Register","Registru"}. {"Remote copy","Fora kopio"}. {"Remove All Offline Messages","Forigu ĉiujn liverontajn mesaĝojn"}. {"Remove User","Forigu uzanton"}. {"Remove","Forigu"}. {"Replaced by new connection","Anstataŭigita je nova konekto"}. {"Resources","Risurcoj"}. {"Restart Service","Restartu Servon"}. {"Restart","Restartu"}. {"Restore Backup from File at ","Restaŭrigu de dosiero el "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaŭrigu duuman sekurkopion post sekvonta ejabberd-restarto"}. {"Restore binary backup immediately:","Restaŭrigu duuman sekurkopion tuj:"}. {"Restore plain text backup immediately:","Restaŭrigu sekurkopion el plata tekstdosiero tuj"}. {"Restore","Restaŭru"}. {"Room Configuration","Babilejo-agordo"}. {"Room creation is denied by service policy","Ĉi tiu serv-politiko ne permesas babilejo-kreadon"}. {"Room description","Babilejo-priskribo"}. {"Room Occupants","Nombro de ĉeestantoj"}. {"Room title","Babilejo-nomo"}. {"Roster groups allowed to subscribe","Kontaktlist-grupoj kiuj rajtas aboni"}. {"Roster size","Kontaktlist-grando"}. {"RPC Call Error","Eraro de RPC-alvoko"}. {"Running Nodes","Funkciantaj Nodoj"}. {"Saturday","Sabato"}. {"Script check","Skript-kontrolo"}. {"Search Results for ","Serĉ-rezultoj de "}. {"Search users in ","Serĉu uzantojn en "}. {"Send announcement to all online users on all hosts","Sendu anoncon al ĉiu konektata uzanto de ĉiu gastigo"}. {"Send announcement to all online users","Sendu anoncon al ĉiu konektata uzanto"}. {"Send announcement to all users on all hosts","Sendu anoncon al ĉiu uzanto de ĉiu gastigo"}. {"Send announcement to all users","Sendu anoncon al ĉiu uzanto"}. {"September","Septembro"}. {"Server:","Servilo:"}. {"Set message of the day and send to online users","Enmetu mesaĝon de la tago kaj sendu al konektataj uzantoj"}. {"Set message of the day on all hosts and send to online users","Enmetu mesaĝon de la tago je ĉiu gastigo kaj sendu al konektataj uzantoj"}. {"Shared Roster Groups","Komuna Kontaktlist-grupo"}. {"Show Integral Table","Montru integran tabelon"}. {"Show Ordinary Table","Montru ordinaran tabelon"}. {"Shut Down Service","Haltigu Servon"}. {"Specify the access model","Specifu atingo-modelon"}. {"Specify the event message type","Specifu tipo de event-mesaĝo"}. {"Specify the publisher model","Enmetu publikadan modelon"}. {"Statistics of ~p","Statistikoj de ~p"}. {"Statistics","Statistikoj"}. {"Stop","Haltigu"}. {"Stopped Nodes","Neaktivaj Nodoj"}. {"Storage Type","Konserv-tipo"}. {"Store binary backup:","Konservu duuman sekurkopion:"}. {"Store plain text backup:","Skribu sekurkopion en plata tekstdosiero"}. {"Subject","Temo"}. {"Submit","Sendu"}. {"Submitted","Sendita"}. {"Subscriber Address","Abonanta adreso"}. {"Subscribers may publish","Abonantoj rajtas publici"}. {"Subscription","Abono"}. {"Sunday","Dimanĉo"}. {"That nickname is already in use by another occupant","Tiu kaŝnomo jam estas uzata de alia partoprenanto"}. {"That nickname is registered by another person","Kaŝnomo estas registrita de alia persono"}. {"The CAPTCHA is valid.","La CAPTCHA ĝustas."}. {"The CAPTCHA verification has failed","La CAPTCHA-kontrolado malsukcesis"}. {"The captcha you entered is wrong","La CAPTCHA enigita de vi malĝustas"}. {"The collections with which a node is affiliated","Aro kun kiu nodo estas filigita"}. {"The password is too weak","La pasvorto estas ne sufiĉe forta"}. {"the password is","la pasvorto estas"}. {"The query is only allowed from local users","La informpeto estas permesita nur de lokaj uzantoj"}. {"The query must not contain <item/> elements","La informpeto devas ne enhavi elementojn <item/>"}. {"There was an error creating the account: ","Estis eraro dum kreado de la konto:"}. {"There was an error deleting the account: ","Estis eraro dum forigado de la konto:"}. {"This room is not anonymous","Ĉi tiu babilejo ne estas anonima"}. {"Thursday","Ĵaŭdo"}. {"Time delay","Prokrasto"}. {"Time","Tempo"}. {"To","Ĝis"}. {"Too many CAPTCHA requests","Tro multaj CAPTCHA-petoj"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Tro da malsukcesaj aŭtentprovoj (~p) de ĉi tiu IP-adreso (~s). La adreso estos malbarata je ~s UTC."}. {"Too many unacked stanzas","Tro da neagnoskitaj stancoj"}. {"Total rooms","Babilejoj"}. {"Traffic rate limit is exceeded","Trafikrapida limigo superita"}. {"Transactions Aborted:","Transakcioj nuligitaj"}. {"Transactions Committed:","Transakcioj enmetitaj"}. {"Transactions Logged:","Transakcioj protokolitaj"}. {"Transactions Restarted:","Transakcioj restartitaj"}. {"Tuesday","Mardo"}. {"Unable to generate a CAPTCHA","Ne eblis krei CAPTCHA"}. {"Unauthorized","Nepermesita"}. {"Unregister","Malregistru"}. {"Update message of the day (don't send)","Ŝanĝu mesaĝon de la tago (ne sendu)"}. {"Update message of the day on all hosts (don't send)","Ŝanĝu mesaĝon de la tago je ĉiu gastigo (ne sendu)"}. {"Update ~p","Ĝisdatigu ~p-n"}. {"Update plan","Ĝisdatigo-plano"}. {"Update script","Ĝisdatigo-skripto"}. {"Update","Ĝisdatigu"}. {"Uptime:","Daŭro de funkciado"}. {"URL for Archived Discussion Logs","Retpaĝa adreso de Enarkivigitaj Diskutprotokoloj"}. {"User JID","Uzant-JID"}. {"User Management","Uzanto-administrado"}. {"Username:","Uzantnomo"}. {"Users are not allowed to register accounts so quickly","Ne estas permesata al uzantoj registri tiel rapide"}. {"Users Last Activity","Lasta aktiveco de uzanto"}. {"Users","Uzantoj"}. {"User","Uzanto"}. {"Validate","Validigu"}. {"vCard User Search","Serĉado de vizitkartoj"}. {"Virtual Hosts","Virtual-gastigoj"}. {"Visitors are not allowed to change their nicknames in this room","Ne estas permesata al vizitantoj ŝanĝi siajn kaŝnomojn en ĉi tiu ĉambro"}. {"Visitors are not allowed to send messages to all occupants","Vizitantoj ne rajtas sendi mesaĝojn al ĉiuj partoprenantoj"}. {"Voice requests are disabled in this conference","Voĉ-petoj estas malebligita en jena babilejo"}. {"Voice request","Voĉ-peto"}. {"Wednesday","Merkredo"}. {"When to send the last published item","Kiam sendi la laste publicitan eron"}. {"Whether to allow subscriptions","Ĉu permesi aboni"}. {"Wrong xmlns","Malĝusta XML-nomspaco (xmlns)"}. {"XMPP Account Registration","Registrado de XMPP-Konto"}. {"You have been banned from this room","Vi estas malpermesata en ĉi tiu babilejo"}. {"You must fill in field \"Nickname\" in the form","Vi devas kompletigi la \"Kaŝnomo\" kampon"}. {"You need a client that supports x:data and CAPTCHA to register","Vi bezonas klienton subtenante x:data-funkcio kaj CAPTCHA por registri kaŝnomon"}. {"You need a client that supports x:data to register the nickname","Vi bezonas klienton subtenante x:data-funkcio por registri kaŝnomon"}. {"You need an x:data capable client to search","Vi bezonas klienton kun x:data-funkcio por serĉado"}. {"Your active privacy list has denied the routing of this stanza.","Via aktiva privatec-listo malpermesas enkursigi ĉi-tiun pakaĵon"}. {"Your contact offline message queue is full. The message has been discarded.","Mesaĝo-atendovico de la senkonekta kontakto estas plena. La mesaĝo estas forĵetita"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Viaj mesaĝoj al ~s estas blokata. Por malbloki ilin, iru al ~s"}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/it.msg���������������������������������������������������������������������0000644�0002322�0002322�00000051766�14154362354�016726� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," Riempire il modulo per la ricerca di utenti Jabber corrispondenti ai criteri (Aggiungere * alla fine del campo per la ricerca di una sottostringa"}. {" has set the subject to: "," ha modificato l'oggetto in: "}. {"A friendly name for the node","Un nome comodo per il nodo"}. {"A password is required to enter this room","Per entrare in questa stanza è prevista una password"}. {"Access denied by service policy","Accesso impedito dalle politiche del servizio"}. {"Action on user","Azione sull'utente"}. {"Add Jabber ID","Aggiungere un Jabber ID (Jabber ID)"}. {"Add New","Aggiungere nuovo"}. {"Add User","Aggiungere un utente"}. {"Administration of ","Amministrazione di "}. {"Administration","Amministrazione"}. {"Administrator privileges required","Necessari i privilegi di amministratore"}. {"All activity","Tutta l'attività"}. {"All Users","Tutti gli utenti"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Consentire a questo Jabber ID l'iscrizione a questo nodo pubsub?"}. {"Allow users to change the subject","Consentire agli utenti di cambiare l'oggetto"}. {"Allow users to query other users","Consentire agli utenti query verso altri utenti"}. {"Allow users to send invites","Consentire agli utenti l'invio di inviti"}. {"Allow users to send private messages","Consentire agli utenti l'invio di messaggi privati"}. {"Allow visitors to change nickname","Consentire ai visitatori di cambiare il nickname"}. {"Allow visitors to send private messages to","Consentire agli ospiti l'invio di messaggi privati a"}. {"Allow visitors to send status text in presence updates","Consentire ai visitatori l'invio di testo sullo stato in aggiornamenti sulla presenza"}. {"Allow visitors to send voice requests","Consentire agli ospiti l'invio di richieste di parola"}. {"Announcements","Annunci"}. {"April","Aprile"}. {"August","Agosto"}. {"Backup Management","Gestione dei salvataggi"}. {"Backup to File at ","Salvataggio sul file "}. {"Backup","Salvare"}. {"Bad format","Formato non valido"}. {"Birthday","Compleanno"}. {"CAPTCHA web page","Pagina web CAPTCHA"}. {"Change Password","Modificare la password"}. {"Change User Password","Cambiare la password dell'utente"}. {"Characters not allowed:","Caratteri non consentiti:"}. {"Chatroom configuration modified","Configurazione della stanza modificata"}. {"Chatroom is created","La stanza è creata"}. {"Chatroom is destroyed","La stanza è eliminata"}. {"Chatroom is started","La stanza è avviata"}. {"Chatroom is stopped","La stanza è arrestata"}. {"Chatrooms","Stanze"}. {"Choose a username and password to register with this server","Scegliere un nome utente e una password per la registrazione con questo server"}. {"Choose storage type of tables","Selezionare una modalità di conservazione delle tabelle"}. {"Choose whether to approve this entity's subscription.","Scegliere se approvare l'iscrizione per questa entità"}. {"City","Città"}. {"Commands","Comandi"}. {"Conference room does not exist","La stanza per conferenze non esiste"}. {"Configuration of room ~s","Configurazione per la stanza ~s"}. {"Configuration","Configurazione"}. {"Connected Resources:","Risorse connesse:"}. {"Country","Paese"}. {"CPU Time:","Tempo CPU:"}. {"Database Tables Configuration at ","Configurazione delle tabelle del database su "}. {"Database","Database"}. {"December","Dicembre"}. {"Default users as participants","Definire per default gli utenti come partecipanti"}. {"Delete message of the day on all hosts","Eliminare il messaggio del giorno (MOTD) su tutti gli host"}. {"Delete message of the day","Eliminare il messaggio del giorno (MOTD)"}. {"Delete Selected","Eliminare gli elementi selezionati"}. {"Delete User","Eliminare l'utente"}. {"Deliver event notifications","Inviare notifiche degli eventi"}. {"Deliver payloads with event notifications","Inviare il contenuto del messaggio con la notifica dell'evento"}. {"Description:","Descrizione:"}. {"Disc only copy","Copia su disco soltanto"}. {"Dump Backup to Text File at ","Trascrivere il salvataggio sul file di testo "}. {"Dump to Text File","Trascrivere su file di testo"}. {"Edit Properties","Modificare le proprietà"}. {"Either approve or decline the voice request.","Approva oppure respingi la richiesta di parola."}. {"ejabberd MUC module","Modulo MUC per ejabberd"}. {"ejabberd Publish-Subscribe module","Modulo Pubblicazione/Iscrizione (PubSub) per ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Modulo SOCKS5 Bytestreams per ejabberd"}. {"ejabberd vCard module","Modulo vCard per ejabberd"}. {"ejabberd Web Admin","Amministrazione web ejabberd"}. {"Elements","Elementi"}. {"Email","E-mail"}. {"Enable logging","Abilitare i log"}. {"End User Session","Terminare la sessione dell'utente"}. {"Enter nickname you want to register","Immettere il nickname che si vuole registrare"}. {"Enter path to backup file","Immettere il percorso del file di salvataggio"}. {"Enter path to jabberd14 spool dir","Immettere il percorso della directory di spool di jabberd14"}. {"Enter path to jabberd14 spool file","Immettere il percorso del file di spool di jabberd14"}. {"Enter path to text file","Immettere il percorso del file di testo"}. {"Enter the text you see","Immettere il testo visibile"}. {"Error","Errore"}. {"Exclude Jabber IDs from CAPTCHA challenge","Escludi degli ID Jabber dal passaggio CAPTCHA"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Esportare i dati di tutti gli utenti nel server in file PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Esportare i dati degli utenti di un host in file PIEFXIS (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Impossibile estrarre il JID dall'approvazione della richiesta di parola"}. {"Family Name","Cognome"}. {"February","Febbraio"}. {"Friday","Venerdì"}. {"From","Da"}. {"Full Name","Nome completo"}. {"Get Number of Online Users","Ottenere il numero di utenti online"}. {"Get Number of Registered Users","Ottenere il numero di utenti registrati"}. {"Get User Last Login Time","Ottenere la data di ultimo accesso dell'utente"}. {"Get User Password","Ottenere la password dell'utente"}. {"Get User Statistics","Ottenere le statistiche dell'utente"}. {"Grant voice to this person?","Dare parola a questa persona?"}. {"Group","Gruppo"}. {"Groups","Gruppi"}. {"has been banned","è stata/o bandita/o"}. {"has been kicked because of a system shutdown","è stato espulso a causa dello spegnimento del sistema"}. {"has been kicked because of an affiliation change","è stato espulso a causa di un cambiamento di appartenenza"}. {"has been kicked because the room has been changed to members-only","è stato espulso per la limitazione della stanza ai soli membri"}. {"has been kicked","è stata/o espulsa/o"}. {"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Se qui non vedi l'immagine CAPTCHA, visita la pagina web."}. {"Import Directory","Importare una directory"}. {"Import File","Importare un file"}. {"Import user data from jabberd14 spool file:","Importare i dati utente da file di spool di jabberd14:"}. {"Import User from File at ","Importare un utente dal file "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importare i dati utenti da un file PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importare i dati utenti da directory di spool di jabberd14:"}. {"Import Users from Dir at ","Importare utenti dalla directory "}. {"Import Users From jabberd14 Spool Files","Importare utenti da file di spool di jabberd14"}. {"Improper message type","Tipo di messaggio non corretto"}. {"Incorrect password","Password non esatta"}. {"IP addresses","Indirizzi IP"}. {"is now known as","è ora conosciuta/o come"}. {"It is not allowed to send private messages of type \"groupchat\"","Non è consentito l'invio di messaggi privati di tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Non è consentito l'invio di messaggi privati alla conferenza"}. {"It is not allowed to send private messages","Non è consentito l'invio di messaggi privati"}. {"Jabber ID","Jabber ID (Jabber ID)"}. {"January","Gennaio"}. {"joins the room","entra nella stanza"}. {"July","Luglio"}. {"June","Giugno"}. {"Last Activity","Ultima attività"}. {"Last login","Ultimo accesso"}. {"Last month","Ultimo mese"}. {"Last year","Ultimo anno"}. {"leaves the room","esce dalla stanza"}. {"Low level update script","Script di aggiornamento di basso livello"}. {"Make participants list public","Rendere pubblica la lista dei partecipanti"}. {"Make room CAPTCHA protected","Rendere la stanza protetta da CAPTCHA"}. {"Make room members-only","Rendere la stanza riservata ai membri"}. {"Make room moderated","Rendere la stanza moderata"}. {"Make room password protected","Rendere la stanza protetta da password"}. {"Make room persistent","Rendere la stanza persistente"}. {"Make room public searchable","Rendere la sala visibile al pubblico"}. {"March","Marzo"}. {"Max # of items to persist","Numero massimo di elementi da conservare persistentemente"}. {"Max payload size in bytes","Dimensione massima del contenuto del messaggio in byte"}. {"Maximum Number of Occupants","Numero massimo di occupanti"}. {"May","Maggio"}. {"Membership is required to enter this room","Per entrare in questa stanza è necessario essere membro"}. {"Members:","Membri:"}. {"Memory","Memoria"}. {"Message body","Corpo del messaggio"}. {"Middle Name","Altro nome"}. {"Minimum interval between voice requests (in seconds)","Intervallo minimo fra due richieste di parola (in secondi)"}. {"Moderator privileges required","Necessari i privilegi di moderatore"}. {"Modified modules","Moduli modificati"}. {"Monday","Lunedì"}. {"Name","Nome"}. {"Name:","Nome:"}. {"Never","Mai"}. {"New Password:","Nuova password:"}. {"Nickname Registration at ","Registrazione di un nickname su "}. {"Nickname ~s does not exist in the room","Il nickname ~s non esiste nella stanza"}. {"Nickname","Nickname"}. {"No body provided for announce message","Nessun corpo fornito per il messaggio di annuncio"}. {"No Data","Nessuna informazione"}. {"No limit","Nessun limite"}. {"Node ID","ID del nodo"}. {"Node not found","Nodo non trovato"}. {"Nodes","Nodi"}. {"None","Nessuno"}. {"Not Found","Non trovato"}. {"Notify subscribers when items are removed from the node","Notificare gli iscritti quando sono eliminati degli elementi dal nodo"}. {"Notify subscribers when the node configuration changes","Notificare gli iscritti quando la configurazione del nodo cambia"}. {"Notify subscribers when the node is deleted","Notificare gli iscritti quando il nodo è cancellato"}. {"November","Novembre"}. {"Number of occupants","Numero di presenti"}. {"Number of online users","Numero di utenti online"}. {"Number of registered users","Numero di utenti registrati"}. {"October","Ottobre"}. {"Offline Messages","Messaggi offline"}. {"Offline Messages:","Messaggi offline:"}. {"OK","OK"}. {"Old Password:","Vecchia password:"}. {"Online Users:","Utenti connessi:"}. {"Online Users","Utenti online"}. {"Online","Online"}. {"Only deliver notifications to available users","Inviare le notifiche solamente agli utenti disponibili"}. {"Only moderators and participants are allowed to change the subject in this room","La modifica dell'oggetto di questa stanza è consentita soltanto ai moderatori e ai partecipanti"}. {"Only moderators are allowed to change the subject in this room","La modifica dell'oggetto di questa stanza è consentita soltanto ai moderatori"}. {"Only moderators can approve voice requests","Soltanto i moderatori possono approvare richieste di parola"}. {"Only occupants are allowed to send messages to the conference","L'invio di messaggi alla conferenza è consentito soltanto ai presenti"}. {"Only occupants are allowed to send queries to the conference","L'invio di query alla conferenza è consentito ai soli presenti"}. {"Only service administrators are allowed to send service messages","L'invio di messaggi di servizio è consentito solamente agli amministratori del servizio"}. {"Organization Name","Nome dell'organizzazione"}. {"Organization Unit","Unità dell'organizzazione"}. {"Outgoing s2s Connections","Connessioni s2s in uscita"}. {"Outgoing s2s Connections:","Connessioni s2s in uscita:"}. {"Owner privileges required","Necessari i privilegi di proprietario"}. {"Packet","Pacchetto"}. {"Password Verification","Verifica della password"}. {"Password Verification:","Verifica della password:"}. {"Password","Password"}. {"Password:","Password:"}. {"Path to Dir","Percorso della directory"}. {"Path to File","Percorso del file"}. {"Pending","Pendente"}. {"Period: ","Periodo:"}. {"Persist items to storage","Conservazione persistente degli elementi"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","N.B.: Queste opzioni comportano il salvataggio solamente del database interno Mnesia. Se si sta utilizzando il modulo ODBC, è necessario salvare anche il proprio database SQL separatamente."}. {"Please, wait for a while before sending new voice request","Attendi qualche istante prima di inviare una nuova richiesta di parola"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Rendere visibile il Jabber ID reale a"}. {"private, ","privato, "}. {"Publish-Subscribe","Pubblicazione-Iscrizione"}. {"PubSub subscriber request","Richiesta di iscrizione per PubSub"}. {"Purge all items when the relevant publisher goes offline","Cancella tutti gli elementi quando chi li ha pubblicati non è più online"}. {"Queries to the conference members are not allowed in this room","In questa stanza non sono consentite query ai membri della conferenza"}. {"RAM and disc copy","Copia in memoria (RAM) e su disco"}. {"RAM copy","Copia in memoria (RAM)"}. {"Really delete message of the day?","Si conferma l'eliminazione del messaggio del giorno (MOTD)?"}. {"Recipient is not in the conference room","Il destinatario non è nella stanza per conferenze"}. {"Registered Users","Utenti registrati"}. {"Registered Users:","Utenti registrati:"}. {"Register","Registra"}. {"Remote copy","Copia remota"}. {"Remove All Offline Messages","Eliminare tutti i messaggi offline"}. {"Remove User","Eliminare l'utente"}. {"Remove","Eliminare"}. {"Replaced by new connection","Sostituito da una nuova connessione"}. {"Resources","Risorse"}. {"Restart Service","Riavviare il servizio"}. {"Restart","Riavviare"}. {"Restore Backup from File at ","Recuperare il salvataggio dal file "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Recuperare un salvataggio binario dopo il prossimo riavvio di ejabberd (necessita di meno memoria):"}. {"Restore binary backup immediately:","Recuperare un salvataggio binario adesso:"}. {"Restore plain text backup immediately:","Recuperare un salvataggio come semplice testo adesso:"}. {"Restore","Recuperare"}. {"Room Configuration","Configurazione della stanza"}. {"Room creation is denied by service policy","La creazione di stanze è impedita dalle politiche del servizio"}. {"Room description","Descrizione della stanza"}. {"Room Occupants","Presenti nella stanza"}. {"Room title","Titolo della stanza"}. {"Roster groups allowed to subscribe","Gruppi roster abilitati alla registrazione"}. {"Roster size","Dimensione della lista dei contatti"}. {"RPC Call Error","Errore di chiamata RPC"}. {"Running Nodes","Nodi attivi"}. {"Saturday","Sabato"}. {"Script check","Verifica dello script"}. {"Search Results for ","Risultati della ricerca per "}. {"Search users in ","Cercare utenti in "}. {"Send announcement to all online users on all hosts","Inviare l'annuncio a tutti gli utenti online su tutti gli host"}. {"Send announcement to all online users","Inviare l'annuncio a tutti gli utenti online"}. {"Send announcement to all users on all hosts","Inviare l'annuncio a tutti gli utenti su tutti gli host"}. {"Send announcement to all users","Inviare l'annuncio a tutti gli utenti"}. {"September","Settembre"}. {"Server:","Server:"}. {"Set message of the day and send to online users","Impostare il messaggio del giorno (MOTD) ed inviarlo agli utenti online"}. {"Set message of the day on all hosts and send to online users","Impostare il messaggio del giorno (MOTD) su tutti gli host e inviarlo agli utenti online"}. {"Shared Roster Groups","Gruppi di liste di contatti comuni"}. {"Show Integral Table","Mostrare la tabella integrale"}. {"Show Ordinary Table","Mostrare la tabella normale"}. {"Shut Down Service","Terminare il servizio"}. {"Specify the access model","Specificare il modello di accesso"}. {"Specify the event message type","Specificare il tipo di messaggio di evento"}. {"Specify the publisher model","Definire il modello di pubblicazione"}. {"Statistics of ~p","Statistiche di ~p"}. {"Statistics","Statistiche"}. {"Stop","Arrestare"}. {"Stopped Nodes","Nodi arrestati"}. {"Storage Type","Tipo di conservazione"}. {"Store binary backup:","Conservare un salvataggio binario:"}. {"Store plain text backup:","Conservare un salvataggio come semplice testo:"}. {"Subject","Oggetto"}. {"Submit","Inviare"}. {"Submitted","Inviato"}. {"Subscriber Address","Indirizzo dell'iscritta/o"}. {"Subscription","Iscrizione"}. {"Sunday","Domenica"}. {"That nickname is already in use by another occupant","Il nickname è già in uso all'interno della conferenza"}. {"That nickname is registered by another person","Questo nickname è registrato da un'altra persona"}. {"The CAPTCHA is valid.","Il CAPTCHA è valido."}. {"The CAPTCHA verification has failed","La verifica del CAPTCHA ha avuto esito negativo"}. {"The collections with which a node is affiliated","Le collezioni a cui è affiliato un nodo"}. {"The password is too weak","La password è troppo debole"}. {"the password is","la password è"}. {"There was an error creating the account: ","Si è verificato un errore nella creazione dell'account: "}. {"There was an error deleting the account: ","Si è verificato un errore nella cancellazione dell'account: "}. {"This room is not anonymous","Questa stanza non è anonima"}. {"Thursday","Giovedì"}. {"Time delay","Ritardo"}. {"Time","Ora"}. {"To","A"}. {"Too many CAPTCHA requests","Troppe richieste CAPTCHA"}. {"Traffic rate limit is exceeded","Limite di traffico superato"}. {"Transactions Aborted:","Transazioni abortite:"}. {"Transactions Committed:","Transazioni avvenute:"}. {"Transactions Logged:","Transazioni con log:"}. {"Transactions Restarted:","Transazioni riavviate:"}. {"Tuesday","Martedì"}. {"Unable to generate a CAPTCHA","Impossibile generare un CAPTCHA"}. {"Unauthorized","Non autorizzato"}. {"Unregister","Elimina"}. {"Update message of the day (don't send)","Aggiornare il messaggio del giorno (MOTD) (non inviarlo)"}. {"Update message of the day on all hosts (don't send)","Aggiornare il messaggio del giorno (MOTD) su tutti gli host (non inviarlo)"}. {"Update plan","Piano di aggiornamento"}. {"Update script","Script di aggiornamento"}. {"Update","Aggiornare"}. {"Uptime:","Tempo dall'avvio:"}. {"User JID","JID utente"}. {"User Management","Gestione degli utenti"}. {"Username:","Nome utente:"}. {"Users are not allowed to register accounts so quickly","Non è consentito agli utenti registrare account così rapidamente"}. {"Users Last Activity","Ultima attività degli utenti"}. {"Users","Utenti"}. {"User","Utente"}. {"Validate","Validare"}. {"vCard User Search","Ricerca di utenti per vCard"}. {"Virtual Hosts","Host Virtuali"}. {"Visitors are not allowed to change their nicknames in this room","Non è consentito ai visitatori cambiare il nickname in questa stanza"}. {"Visitors are not allowed to send messages to all occupants","Non è consentito ai visitatori l'invio di messaggi a tutti i presenti"}. {"Voice request","Richiesta di parola"}. {"Voice requests are disabled in this conference","In questa conferenza le richieste di parola sono escluse"}. {"Wednesday","Mercoledì"}. {"When to send the last published item","Quando inviare l'ultimo elemento pubblicato"}. {"Whether to allow subscriptions","Consentire iscrizioni?"}. {"You have been banned from this room","Sei stata/o bandita/o da questa stanza"}. {"You must fill in field \"Nickname\" in the form","Si deve riempire il campo \"Nickname\" nel modulo"}. {"You need a client that supports x:data and CAPTCHA to register","La registrazione richiede un client che supporti x:data e CAPTCHA"}. {"You need a client that supports x:data to register the nickname","Per registrare il nickname è necessario un client che supporti x:data"}. {"You need an x:data capable client to search","Per effettuare ricerche è necessario un client che supporti x:data"}. {"Your active privacy list has denied the routing of this stanza.","In base alla tua attuale lista privacy questa stanza è stata esclusa dalla navigazione."}. {"Your contact offline message queue is full. The message has been discarded.","La coda dei messaggi offline del contatto è piena. Il messaggio è stato scartato"}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","I messaggi verso ~s sono bloccati. Per sbloccarli, visitare ~s"}. ����������ejabberd-21.12/priv/msgs/hu.msg���������������������������������������������������������������������0000644�0002322�0002322�00000100010�14154362354�016677� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (adjon * karaktert a mező végéhez a részkarakterláncra illesztéshez)"}. {" has set the subject to: "," beállította a tárgyat erre: "}. {"A password is required to enter this room","Jelszó szükséges a szobába történő belépéshez"}. {"Accept","Elfogadás"}. {"Access denied by service policy","Hozzáférés megtagadva a szolgáltatási irányelv miatt"}. {"Account doesn't exist","A fiók nem létezik"}. {"Action on user","Művelet a felhasználón"}. {"Add Jabber ID","Jabber-azonosító hozzáadása"}. {"Add New","Új hozzáadása"}. {"Add User","Felhasználó hozzáadása"}. {"Administration of ","Adminisztrációja ennek: "}. {"Administration","Adminisztráció"}. {"Administrator privileges required","Adminisztrátori jogosultságok szükségesek"}. {"All activity","Összes tevékenység"}. {"All Users","Összes felhasználó"}. {"Allow users to change the subject","Lehetővé tenni a felhasználóknak a tárgy megváltoztatását"}. {"Allow users to query other users","Lehetővé tenni a felhasználóknak más felhasználók lekérdezését"}. {"Allow users to send invites","Lehetővé tenni a felhasználóknak meghívók küldését"}. {"Allow users to send private messages","Lehetővé tenni a felhasználóknak személyes üzenetek küldését"}. {"Allow visitors to change nickname","Lehetővé tenni a látogatóknak a becenév megváltoztatását"}. {"Allow visitors to send private messages to","Lehetővé tenni a látogatóknak személyes üzenetek küldését"}. {"Allow visitors to send status text in presence updates","Lehetővé tenni a látogatóknak állapotszöveg küldését a jelenlét frissítéseiben"}. {"Announcements","Közlemények"}. {"April","április"}. {"Attribute 'channel' is required for this request","A „channel” attribútum kötelező ennél a kérésnél"}. {"Attribute 'id' is mandatory for MIX messages","Az „id” attribútum kötelező a MIX üzeneteknél"}. {"Attribute 'jid' is not allowed here","A „jid” attribútum itt nem engedélyezett"}. {"Attribute 'node' is not allowed here","A „node” attribútum itt nem engedélyezett"}. {"August","augusztus"}. {"Automatic node creation is not enabled","Automatikus csomópont-létrehozás nincs engedélyezve"}. {"Backup Management","Biztonságimentés-kezelés"}. {"Backup of ~p","~p biztonsági mentése"}. {"Backup to File at ","Biztonsági mentés fájlba ekkor: "}. {"Backup","Biztonsági mentés"}. {"Bad format","Hibás formátum"}. {"Birthday","Születésnap"}. {"Both the username and the resource are required","A felhasználónév és az erőforrás is szükséges"}. {"Bytestream already activated","A bájtfolyam már be van kapcsolva"}. {"Cannot remove active list","Nem lehet eltávolítani az aktív listát"}. {"Cannot remove default list","Nem lehet eltávolítani az alapértelmezett listát"}. {"Change Password","Jelszó megváltoztatása"}. {"Change User Password","Felhasználó jelszavának megváltoztatása"}. {"Changing password is not allowed","A jelszó megváltoztatása nem engedélyezett"}. {"Changing role/affiliation is not allowed","A szerep vagy a hovatartozás megváltoztatása nem engedélyezett"}. {"Channel already exists","A csatorna már létezik"}. {"Channel does not exist","A csatorna nem létezik"}. {"Channels","Csatornák"}. {"Characters not allowed:","Nem engedélyezett karakterek:"}. {"Chatroom configuration modified","Csevegőszoba beállítása módosítva"}. {"Chatroom is created","Csevegőszoba létrehozva"}. {"Chatroom is destroyed","Csevegőszoba megszüntetve"}. {"Chatroom is started","Csevegőszoba elindítva"}. {"Chatroom is stopped","Csevegőszoba leállítva"}. {"Chatrooms","Csevegőszobák"}. {"Choose a username and password to register with this server","Válasszon felhasználónevet és jelszót a kiszolgálóra történő regisztráláshoz"}. {"Choose storage type of tables","Táblák tárolótípusának kiválasztása"}. {"Choose whether to approve this entity's subscription.","Annak kiválasztása, hogy elfogadja-e ennek a bejegyzésnek a feliratkozását."}. {"City","Település"}. {"Client acknowledged more stanzas than sent by server","Az ügyfél több stanzát nyugtázott, mint amennyit a kiszolgáló küldött"}. {"Commands","Parancsok"}. {"Conference room does not exist","A konferenciaszoba nem létezik"}. {"Configuration of room ~s","A(z) ~s szoba beállítása"}. {"Configuration","Beállítás"}. {"Connected Resources:","Kapcsolódott erőforrások:"}. {"Country","Ország"}. {"CPU Time:","Processzoridő:"}. {"Database failure","Adatbázishiba"}. {"Database Tables at ~p","Adatbázistáblák itt: ~p"}. {"Database Tables Configuration at ","Adatbázistáblák beállítása itt: "}. {"Database","Adatbázis"}. {"December","december"}. {"Default users as participants","Alapértelmezett felhasználók mint résztvevők"}. {"Delete content","Tartalom törlése"}. {"Delete message of the day on all hosts","Napi üzenet törlése az összes gépen"}. {"Delete message of the day","Napi üzenet törlése"}. {"Delete Selected","Kijelöltek törlése"}. {"Delete table","Tábla törlése"}. {"Delete User","Felhasználó törlése"}. {"Description:","Leírás:"}. {"Disc only copy","Csak lemez másolása"}. {"Dump Backup to Text File at ","Biztonsági mentés kiírása szövegfájlba itt: "}. {"Dump to Text File","Kiírás szövegfájlba"}. {"Edit Properties","Tulajdonságok szerkesztése"}. {"Either approve or decline the voice request.","Hagyja jóvá vagy utasítsa el a hangkérelmet."}. {"ejabberd HTTP Upload service","ejabberd HTTP feltöltési szolgáltatás"}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Multicast service","ejabberd üzenetszórási szolgáltatás"}. {"ejabberd Publish-Subscribe module","ejabberd publikálás-feliratkozás modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 bájtfolyam modul"}. {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","ejabberd webes adminisztráció"}. {"ejabberd","ejabberd"}. {"Elements","Elemek"}. {"Email","E-mail"}. {"Enable logging","Naplózás engedélyezése"}. {"Enabling push without 'node' attribute is not supported","A „node” attribútum nélküli felküldés engedélyezése nem támogatott"}. {"End User Session","Felhasználói munkamenet befejezése"}. {"Enter nickname you want to register","Adja meg a becenevet, amelyet regisztrálni szeretne"}. {"Enter path to backup file","Adja meg a biztonsági mentés fájl útvonalát"}. {"Enter path to jabberd14 spool dir","Adja meg a jabberd14 tárolókönyvtár útvonalát"}. {"Enter path to jabberd14 spool file","Adja meg a jabberd14 tárolófájl útvonalát"}. {"Enter path to text file","Adja meg a szövegfájl útvonalát"}. {"Enter the text you see","Írja be a látott szöveget"}. {"Error","Hiba"}. {"Export all tables as SQL queries to a file:","Összes tábla exportálása SQL-lekérdezésekként egy fájlba:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","A kiszolgálón lévő összes felhasználó adatainak exportálása PIEFXIS-fájlokba (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Egy gépen lévő felhasználók adatainak exportálása PIEFXIS-fájlokba (XEP-0227):"}. {"External component failure","Külső összetevő hiba"}. {"External component timeout","Külső összetevő időtúllépés"}. {"Failed to activate bytestream","Nem sikerült bekapcsolni a bájtfolyamot"}. {"Failed to extract JID from your voice request approval","Nem sikerült kinyerni a Jabber-azonosítót a hangkérelem jóváhagyásból"}. {"Failed to map delegated namespace to external component","Nem sikerült leképezni a delegált névteret külső összetevőre"}. {"Failed to parse HTTP response","Nem sikerült feldolgozni a HTTP választ"}. {"Family Name","Családnév"}. {"February","február"}. {"File larger than ~w bytes","A fájl nagyobb ~w bájtnál"}. {"Friday","péntek"}. {"From ~ts","Feladó: ~ts"}. {"From","Feladó"}. {"Full Name","Teljes név"}. {"Get Number of Online Users","Elérhető felhasználók számának lekérése"}. {"Get Number of Registered Users","Regisztrált felhasználók számának lekérése"}. {"Get Pending","Függőben lévő lekérése"}. {"Get User Last Login Time","Felhasználó legutolsó bejelentkezési idejének lekérése"}. {"Get User Password","Felhasználó jelszavának lekérése"}. {"Get User Statistics","Felhasználói statisztikák lekérése"}. {"Given Name","Keresztnév"}. {"Group","Csoport"}. {"Groups","Csoportok"}. {"has been banned","ki lett tiltva"}. {"has been kicked because of a system shutdown","ki lett rúgva egy rendszerleállítás miatt"}. {"has been kicked because of an affiliation change","ki lett rúgva egy hovatartozás megváltozása miatt"}. {"has been kicked because the room has been changed to members-only","ki lett rúgva, mert a szobát megváltoztatták csak tagok részére"}. {"has been kicked","ki lett rúgva"}. {"Host unknown","Gép ismeretlen"}. {"Host","Gép"}. {"HTTP File Upload","HTTP fájlfeltöltés"}. {"Idle connection","Tétlen kapcsolat"}. {"If you don't see the CAPTCHA image here, visit the web page.","Ha nem látja itt a CAPTCHA képet, akkor látogassa meg a weboldalt."}. {"Import Directory","Könyvtár importálása"}. {"Import File","Fájl importálása"}. {"Import user data from jabberd14 spool file:","Felhasználóadatok importálása jabberd14 tárolófájlból:"}. {"Import User from File at ","Felhasználó importálása fájlból itt: "}. {"Import users data from a PIEFXIS file (XEP-0227):","Felhasználók adatainak importálása PIEFXIS-fájlból (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Felhasználók adatainak importálása jabberd14 tárolókönyvtárból:"}. {"Import Users from Dir at ","Felhasználók importálása könyvtárból itt: "}. {"Import Users From jabberd14 Spool Files","Felhasználók importálása jabberd14 tárolófájlokból"}. {"Improper domain part of 'from' attribute","A „from” attribútum tartományrésze helytelen"}. {"Improper message type","Helytelen üzenettípus"}. {"Incoming s2s Connections:","Bejövő s2s kapcsolatok:"}. {"Incorrect CAPTCHA submit","Hibás CAPTCHA beküldés"}. {"Incorrect data form","Hibás adatűrlap"}. {"Incorrect password","Hibás jelszó"}. {"Incorrect value of 'action' attribute","Az „action” attribútum értéke hibás"}. {"Incorrect value of 'action' in data form","Az „action” értéke hibás az adatűrlapon"}. {"Incorrect value of 'path' in data form","A „path” értéke hibás az adatűrlapon"}. {"Insufficient privilege","Nincs elegendő jogosultság"}. {"Internal server error","Belső kiszolgálóhiba"}. {"Invalid 'from' attribute in forwarded message","Érvénytelen „from” attribútum a továbbított üzenetben"}. {"Invalid node name","Érvénytelen csomópontnév"}. {"Invalid 'previd' value","Érvénytelen „previd” érték"}. {"Invitations are not allowed in this conference","Meghívások nem engedélyezettek ebben a konferenciában"}. {"IP addresses","IP-címek"}. {"is now known as","mostantól úgy ismert mint"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Nem engedélyezett hibaüzeneteket küldeni a szobába. A résztvevő (~s) hibaüzenetet (~s) küldött, és ki lett rúgva a szobából"}. {"It is not allowed to send private messages of type \"groupchat\"","Nem engedélyezett „groupchat” típusú személyes üzeneteket küldeni"}. {"It is not allowed to send private messages to the conference","Nem engedélyezett személyes üzeneteket küldeni a konferenciába"}. {"It is not allowed to send private messages","Nem engedélyezett személyes üzeneteket küldeni"}. {"Jabber ID","Jabber-azonosító"}. {"January","január"}. {"JID normalization denied by service policy","A Jabber-azonosító normalizálása megtagadva a szolgáltatási irányelv miatt"}. {"JID normalization failed","A Jabber-azonosító normalizálása nem sikerült"}. {"joins the room","belépett a szobába"}. {"July","július"}. {"June","június"}. {"Last Activity","Utolsó tevékenység"}. {"Last login","Utolsó belépés"}. {"Last month","Múlt hónap"}. {"Last year","Múlt év"}. {"leaves the room","elhagyta a szobát"}. {"List of rooms","Szobák listája"}. {"Low level update script","Alacsony szintű frissítő parancsfájl"}. {"Make participants list public","Résztvevőlista nyilvánossá tétele"}. {"Make room CAPTCHA protected","Szoba CAPTCHA-védetté tétele"}. {"Make room members-only","Szoba beállítása csak tagoknak"}. {"Make room moderated","Szoba moderálttá tétele"}. {"Make room password protected","Szoba jelszóval védetté tétele"}. {"Make room persistent","Szoba állandóvá tétele"}. {"Make room public searchable","Szoba nyilvánosan kereshetővé tétele"}. {"Malformed username","Helytelenül formázott felhasználónév"}. {"MAM preference modification denied by service policy","MAM beállítások módosítása megtagadva a szolgáltatási irányelv miatt"}. {"March","március"}. {"Maximum Number of Occupants","Résztvevők legnagyobb száma"}. {"May","május"}. {"Membership is required to enter this room","Tagság szükséges a szobába lépéshez"}. {"Members:","Tagok:"}. {"Memory","Memória"}. {"Message body","Üzenettörzs"}. {"Message not found in forwarded payload","Nem található üzenet a továbbított adatokban"}. {"Messages from strangers are rejected","Idegenektől származó üzenetek vissza vannak utasítva"}. {"Middle Name","Középső név"}. {"Moderator privileges required","Moderátori jogosultságok szükségesek"}. {"Modified modules","Módosított modulok"}. {"Module failed to handle the query","A modul nem tudta kezelni a lekérdezést"}. {"Monday","hétfő"}. {"Multicast","Csoportcímzés"}. {"Multi-User Chat","Többfelhasználós csevegés"}. {"Name","Név"}. {"Name:","Név:"}. {"Neither 'jid' nor 'nick' attribute found","Sem a „jid”, sem a „nick” attribútum nem található"}. {"Neither 'role' nor 'affiliation' attribute found","Sem a „role”, sem az „affiliation” attribútum nem található"}. {"Never","Soha"}. {"New Password:","Új jelszó:"}. {"Nickname can't be empty","A becenév nem lehet üres"}. {"Nickname Registration at ","Becenév regisztrációja itt: "}. {"Nickname","Becenév"}. {"No address elements found","Nem találhatók cím elemek"}. {"No addresses element found","Nem található címek elem"}. {"No 'affiliation' attribute found","Nem található „affiliation” attribútum"}. {"No available resource found","Nem található elérhető erőforrás"}. {"No body provided for announce message","Nincs törzs megadva a közleményüzenethez"}. {"No child elements found","Nem találhatók gyermekelemek"}. {"No data form found","Nem található adatűrlap"}. {"No Data","Nincs adat"}. {"No features available","Nincsenek elérhető funkciók"}. {"No <forwarded/> element found","Nem található <forwarded/> elem"}. {"No hook has processed this command","Egyetlen horog sem dolgozta fel ezt a parancsot"}. {"No info about last activity found","Nem található információ a legutolsó tevékenységgel kapcsolatban"}. {"No 'item' element found","Nem található „item” elem"}. {"No items found in this query","Nem találhatók elemek ebben a lekérdezésben"}. {"No limit","Nincs korlát"}. {"No module is handling this query","Egyetlen modul sem kezeli ezt a lekérdezést"}. {"No node specified","Nincs csomópont megadva"}. {"No 'password' found in data form","Nem található „password” az adatűrlapon"}. {"No 'password' found in this query","Nem található „password” ebben a lekérdezésben"}. {"No 'path' found in data form","Nem található „path” az adatűrlapon"}. {"No pending subscriptions found","Nem találhatók függőben lévő feliratkozások"}. {"No privacy list with this name found","Nem található ilyen nevű adatvédelmi lista"}. {"No private data found in this query","Nem található személyes adat ebben a lekérdezésben"}. {"No running node found","Nem található futó csomópont"}. {"No services available","Nincsenek elérhető szolgáltatások"}. {"No statistics found for this item","Nem találhatók statisztikák ehhez az elemhez"}. {"No 'to' attribute found in the invitation","Nem található „to” attribútum a meghívásban"}. {"Node already exists","A csomópont már létezik"}. {"Node index not found","A csomópontindex nem található"}. {"Node not found","A csomópont nem található"}. {"Node ~p","~p csomópont"}. {"Nodeprep has failed","A csomópont-előkészítés sikertelen"}. {"Nodes","Csomópontok"}. {"None","Nincs"}. {"Not allowed","Nem engedélyezett"}. {"Not Found","Nem található"}. {"Not subscribed","Nincs feliratkozva"}. {"November","november"}. {"Number of online users","Elérhető felhasználók száma"}. {"Number of registered users","Regisztrált felhasználók száma"}. {"October","október"}. {"Offline Messages","Kapcsolat nélküli üzenetek"}. {"Offline Messages:","Kapcsolat nélküli üzenetek:"}. {"OK","Rendben"}. {"Old Password:","Régi jelszó:"}. {"Online Users","Elérhető felhasználók"}. {"Online Users:","Elérhető felhasználók:"}. {"Online","Elérhető"}. {"Only <enable/> or <disable/> tags are allowed","Csak az <enable/> vagy <disable/> címkék engedélyezettek"}. {"Only <list/> element is allowed in this query","Csak a <list/> elem engedélyezett ebben a lekérdezésben"}. {"Only members may query archives of this room","Csak tagok kérdezhetik le ennek a szobának az archívumát"}. {"Only moderators and participants are allowed to change the subject in this room","Csak moderátoroknak és résztvevőknek engedélyezett megváltoztatni a tárgyat ebben a szobában"}. {"Only moderators are allowed to change the subject in this room","Csak moderátoroknak engedélyezett megváltoztatni a tárgyat ebben a szobában"}. {"Only moderators can approve voice requests","Csak moderátorok hagyhatnak jóvá hangkérelmeket"}. {"Only occupants are allowed to send messages to the conference","Csak résztvevőknek engedélyezett üzeneteket küldeni a konferenciába"}. {"Only occupants are allowed to send queries to the conference","Csak résztvevőknek engedélyezett lekérdezéseket küldeni a konferenciába"}. {"Only service administrators are allowed to send service messages","Csak szolgáltatás-adminisztrátoroknak engedélyezett szolgáltatási üzeneteket küldeni"}. {"Organization Name","Szervezet neve"}. {"Organization Unit","Szervezeti egység"}. {"Outgoing s2s Connections","Kimenő s2s kapcsolatok"}. {"Outgoing s2s Connections:","Kimenő s2s kapcsolatok:"}. {"Owner privileges required","Tulajdonosi jogosultságok szükségesek"}. {"Packet relay is denied by service policy","Csomagátjátszás megtagadva a szolgáltatási irányelv miatt"}. {"Packet","Csomag"}. {"Password Verification","Jelszó ellenőrzése"}. {"Password Verification:","Jelszó ellenőrzése:"}. {"Password","Jelszó"}. {"Password:","Jelszó:"}. {"Path to Dir","Útvonal a könyvtárhoz"}. {"Path to File","Útvonal a fájlhoz"}. {"Pending","Függőben"}. {"Period: ","Időszak: "}. {"Ping query is incorrect","A lekérdezés pingelése hibás"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ne feledje, hogy ezek a beállítások csak a beépített Mnesia adatbázisról készítenek biztonsági mentést. Ha az ODBC modult használja, akkor az SQL adatbázisról is különálló biztonsági mentést kell készítenie."}. {"Please, wait for a while before sending new voice request","Várjon egy kicsit az új hangkérelem küldése előtt"}. {"Pong","Pong"}. {"Previous session not found","Az előző munkamenet nem található"}. {"Previous session PID has been killed","Az előző munkamenet folyamat-azonosítója ki lett lőve"}. {"Previous session PID has exited","Az előző munkamenet folyamat-azonosítója kilépett"}. {"Previous session PID is dead","Az előző munkamenet folyamat-azonosítója halott"}. {"Previous session timed out","Az előző munkamenet túllépte az időkorlátot"}. {"private, ","személyes, "}. {"Publish-Subscribe","Publikálás-feliratkozás"}. {"PubSub subscriber request","Publikálás-feliratkozás feliratkozási kérelem"}. {"Push record not found","Leküldési rekord nem található"}. {"Queries to the conference members are not allowed in this room","A konferenciatagok lekérdezései nem engedélyezettek ebben a szobában"}. {"Query to another users is forbidden","Egy másik felhasználó lekérdezése tiltva van"}. {"RAM and disc copy","RAM és lemezmásolás"}. {"RAM copy","RAM másolás"}. {"Really delete message of the day?","Valóban törli a napi üzenetet?"}. {"Recipient is not in the conference room","A címzett nincs a konferenciaszobában"}. {"Registered Users","Regisztrált felhasználók"}. {"Registered Users:","Regisztrált felhasználók:"}. {"Register","Regisztráció"}. {"Remote copy","Távoli másolás"}. {"Remove All Offline Messages","Összes kapcsolat nélküli üzenet eltávolítása"}. {"Remove User","Felhasználó eltávolítása"}. {"Remove","Eltávolítás"}. {"Replaced by new connection","Kicserélve egy új kapcsolattal"}. {"Request has timed out","A kérés túllépte az időkorlátot"}. {"Request is ignored","A kérés mellőzve lett"}. {"Resources","Erőforrások"}. {"Restart Service","Szolgáltatás újraindítása"}. {"Restart","Újraindítás"}. {"Restore Backup from File at ","Biztonsági mentés visszaállítása fájlból itt: "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Bináris biztonsági mentés visszaállítása az ejabberd következő újraindítása után (kevesebb memóriát igényel):"}. {"Restore binary backup immediately:","Bináris biztonsági mentés visszaállítása azonnal:"}. {"Restore plain text backup immediately:","Egyszerű szöveges biztonsági mentés visszaállítása azonnal:"}. {"Restore","Visszaállítás"}. {"Room Configuration","Szoba beállítása"}. {"Room creation is denied by service policy","Szobalétrehozás megtagadva a szolgáltatási irányelv miatt"}. {"Room description","Szoba leírása"}. {"Room Occupants","Szoba résztvevői"}. {"Room terminates","Szoba megszűnik"}. {"Room title","Szoba címe"}. {"Roster of ~ts","~ts névsora"}. {"Roster size","Névsor mérete"}. {"RPC Call Error","RPC hívási hiba"}. {"Running Nodes","Futó csomópontok"}. {"Saturday","szombat"}. {"Script check","Parancsfájl-ellenőrzés"}. {"Search Results for ","Keresési eredménye ennek: "}. {"Search users in ","Felhasználók keresése ebben: "}. {"Select All","Összes kijelölése"}. {"Send announcement to all online users on all hosts","Közlemény küldése az összes elérhető felhasználónak az összes gépen"}. {"Send announcement to all online users","Közlemény küldése az összes elérhető felhasználónak"}. {"Send announcement to all users on all hosts","Közlemény küldése az összes felhasználónak az összes gépen"}. {"Send announcement to all users","Közlemény küldése az összes felhasználónak"}. {"September","szeptember"}. {"Server:","Kiszolgáló:"}. {"Session state copying timed out","A munkamenet állapotának másolása túllépte az időkorlátot"}. {"Set message of the day and send to online users","Napi üzenet beállítása és küldés az elérhető felhasználóknak"}. {"Set message of the day on all hosts and send to online users","Napi üzenet beállítása az összes gépen és küldés az elérhető felhasználóknak"}. {"Shared Roster Groups","Megosztott névsorcsoportok"}. {"Show Integral Table","Integráltáblázat megjelenítése"}. {"Show Ordinary Table","Szokásos táblázat megjelenítése"}. {"Shut Down Service","Szolgáltatás leállítása"}. {"SOCKS5 Bytestreams","SOCKS5 bájtfolyamok"}. {"Statistics of ~p","~p statisztikái"}. {"Statistics","Statisztikák"}. {"Stop","Leállítás"}. {"Stopped Nodes","Leállított csomópontok"}. {"Storage Type","Tárolótípus"}. {"Store binary backup:","Bináris biztonsági mentés tárolása:"}. {"Store plain text backup:","Egyszerű szöveges biztonsági mentés tárolása:"}. {"Stream management is already enabled","A folyamkezelés már engedélyezve van"}. {"Stream management is not enabled","A folyamkezelés nincs engedélyezve"}. {"Subject","Tárgy"}. {"Submit","Elküldés"}. {"Submitted","Elküldve"}. {"Subscription","Feliratkozás"}. {"Subscriptions are not allowed","Feliratkozások nem engedélyezettek"}. {"Sunday","vasárnap"}. {"That nickname is already in use by another occupant","Ezt a becenevet már használja egy másik résztvevő"}. {"That nickname is registered by another person","Ezt a becenevet egy másik személy regisztrálta"}. {"The account already exists","A fiók már létezik"}. {"The CAPTCHA is valid.","A CAPTCHA érvényes."}. {"The CAPTCHA verification has failed","A CAPTCHA ellenőrzése nem sikerült"}. {"The captcha you entered is wrong","A beírt CAPTCHA hibás"}. {"The feature requested is not supported by the conference","A kért funkciót nem támogatja a konferencia"}. {"The password contains unacceptable characters","A jelszó elfogadhatatlan karaktereket tartalmaz"}. {"The password is too weak","A jelszó túl gyenge"}. {"the password is","a jelszó"}. {"The password was not changed","A jelszó nem lett megváltoztatva"}. {"The passwords are different","A jelszavak különböznek"}. {"The query is only allowed from local users","A lekérdezés csak helyi felhasználóktól engedélyezett"}. {"The query must not contain <item/> elements","A lekérdezés nem tartalmazhat <item/> elemeket"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","A stanzának csak egyetlen <active/> elemet, egyetlen <default/> elemet vagy egyetlen <list/> elemet KELL tartalmaznia"}. {"The username is not valid","A felhasználónév nem érvényes"}. {"There was an error creating the account: ","Hiba történt a fiók létrehozásakor: "}. {"There was an error deleting the account: ","Hiba történt a fiók törlésekor: "}. {"This room is not anonymous","Ez a szoba nem névtelen"}. {"This service can not process the address: ~s","Ez a szolgáltatás nem tudja feldolgozni a címet: ~s"}. {"Thursday","csütörtök"}. {"Time delay","Időkésleltetés"}. {"Timed out waiting for stream resumption","Időtúllépés a folyam újrakezdésére várakozásnál"}. {"Time","Idő"}. {"To register, visit ~s","Regisztráláshoz látogassa meg ezt az oldalt: ~s"}. {"To ~ts","Címzett: ~ts"}. {"To","Címzett"}. {"Token TTL","Token élettartama"}. {"Too many active bytestreams","Túl sok aktív bájtfolyam"}. {"Too many CAPTCHA requests","Túl sok CAPTCHA kérés"}. {"Too many child elements","Túl sok gyermekelem"}. {"Too many <item/> elements","Túl sok <item/> elem"}. {"Too many <list/> elements","Túl sok <list/> elem"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Túl sok (~p) sikertelen hitelesítés erről az IP-címről (~ts) A cím ~ts-kor lesz feloldva UTC szerint"}. {"Too many receiver fields were specified","Túl sok fogadómező lett meghatározva"}. {"Too many unacked stanzas","Túl sok nyugtázatlan stanza"}. {"Too many users in this conference","Túl sok felhasználó ebben a konferenciában"}. {"Total rooms","Szobák összesen"}. {"Traffic rate limit is exceeded","Forgalom sebességkorlátja elérve"}. {"Transactions Aborted:","Megszakított tranzakciók:"}. {"Transactions Committed:","Véglegesített tranzakciók:"}. {"Transactions Logged:","Naplózott tranzakciók:"}. {"Transactions Restarted:","Újraindított tranzakciók:"}. {"~ts's Offline Messages Queue","~ts kapcsolat nélküli üzeneteinek tárolója"}. {"Tuesday","kedd"}. {"Unable to generate a CAPTCHA","Nem lehet előállítani CAPTCHA-t"}. {"Unable to register route on existing local domain","Nem lehet útvonalat regisztrálni egy meglévő helyi tartományon"}. {"Unauthorized","Nem engedélyezett"}. {"Unexpected action","Váratlan művelet"}. {"Unexpected error condition: ~p","Váratlan hibafeltétel: ~p"}. {"Unregister","Regisztráció törlése"}. {"Unselect All","Összes kijelölésének megszüntetése"}. {"Unsupported <index/> element","Nem támogatott <index/> elem"}. {"Unsupported version","Nem támogatott verzió"}. {"Update message of the day (don't send)","Napi üzenet frissítése (ne küldje el)"}. {"Update message of the day on all hosts (don't send)","Napi üzenet frissítése az összes gépen (ne küldje el)"}. {"Update plan","Frissítési terv"}. {"Update ~p","~p frissítése"}. {"Update script","Frissítő parancsfájl"}. {"Update","Frissítés"}. {"Uptime:","Működési idő:"}. {"User already exists","A felhasználó már létezik"}. {"User (jid)","Felhasználó (JID)"}. {"User Management","Felhasználó-kezelés"}. {"User removed","Felhasználó eltávolítva"}. {"User session not found","Felhasználói munkamenet nem található"}. {"User session terminated","Felhasználói munkamenet befejeződött"}. {"User ~ts","~ts felhasználó"}. {"User","Felhasználó"}. {"Username:","Felhasználónév:"}. {"Users are not allowed to register accounts so quickly","A felhasználóknak nem engedélyezett fiókokat regisztrálni ilyen gyorsan"}. {"Users Last Activity","Felhasználók utolsó tevékenysége"}. {"Users","Felhasználók"}. {"Validate","Ellenőrzés"}. {"Value 'get' of 'type' attribute is not allowed","A „type” attribútum „get” értéke nem engedélyezett"}. {"Value of '~s' should be boolean","A(z) „~s” értéke csak logikai lehet"}. {"Value of '~s' should be datetime string","A(z) „~s” értéke csak dátum és idő karakterlánc lehet"}. {"Value of '~s' should be integer","A(z) „~s” értéke csak egész szám lehet"}. {"Value 'set' of 'type' attribute is not allowed","A „type” attribútum „set” értéke nem engedélyezett"}. {"vCard User Search","vCard felhasználó-keresés"}. {"Virtual Hosts","Virtuális gépek"}. {"Visitors are not allowed to change their nicknames in this room","A látogatóknak nem engedélyezett megváltoztatni a beceneveiket ebben a szobában"}. {"Visitors are not allowed to send messages to all occupants","A látogatóknak nem engedélyezett üzeneteket küldeni az összes résztvevőnek"}. {"Voice request","Hangkérelem"}. {"Voice requests are disabled in this conference","A hangkérelmek le vannak tiltva ebben a konferenciában"}. {"Wednesday","szerda"}. {"Wrong parameters in the web formulary","Hibás paraméterek a webes modelldokumentumban"}. {"Wrong xmlns","Hibás xmlns"}. {"You are being removed from the room because of a system shutdown","El lett távolítva a szobából egy rendszerleállítás miatt"}. {"You are not joined to the channel","Nincs csatlakozva a csatornához"}. {"You have been banned from this room","Ki lett tiltva ebből a szobából"}. {"You have joined too many conferences","Túl sok konferenciához csatlakozott"}. {"You must fill in field \"Nickname\" in the form","Ki kell töltenie a „becenév” mezőt az űrlapon"}. {"You need a client that supports x:data and CAPTCHA to register","Olyan programra van szüksége, amelynek x:data és CAPTCHA támogatása van a regisztráláshoz"}. {"You need a client that supports x:data to register the nickname","Olyan programra van szüksége, amelynek x:data támogatása van a becenév regisztráláshoz"}. {"You need an x:data capable client to search","Egy x:data támogatású programra van szüksége a kereséshez"}. {"Your active privacy list has denied the routing of this stanza.","Az aktív adatvédelmi listája megtagadta ennek a stanzának az útválasztását."}. {"Your contact offline message queue is full. The message has been discarded.","A partnere kapcsolat nélküli üzenettárolója megtelt. Az üzenet el lett dobva."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","A feliratkozási kérelme és/vagy ~s számára küldött üzenetei blokkolva lettek. A feliratkozási kérelmének feloldásához látogassa meg ezt az oldalt: ~s"}. {"You're not allowed to create nodes","Önnek nincs engedélye csomópontokat létrehozni"}. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/fr.msg���������������������������������������������������������������������0000644�0002322�0002322�00000104501�14154362354�016703� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Ajouter * à la fin du champ pour correspondre à la sous-chaîne)"}. {" has set the subject to: "," a défini le sujet sur : "}. {"# participants","# participants"}. {"A description of the node","Une description du nœud"}. {"A friendly name for the node","Un nom convivial pour le nœud"}. {"A password is required to enter this room","Un mot de passe est nécessaire pour accéder à ce salon"}. {"A Web Page","Une page Web"}. {"Accept","Accepter"}. {"Access denied by service policy","L'accès au service est refusé"}. {"Access model of authorize","Modèle d’accès de « autoriser »"}. {"Access model of open","Modèle d’accès de « ouvrir »"}. {"Access model of presence","Modèle d’accès de « présence »"}. {"Access model of roster","Modèle d’accès de « liste »"}. {"Access model of whitelist","Modèle d’accès de « liste blanche »"}. {"Access model","Modèle d’accès"}. {"Account doesn't exist","Le compte n’existe pas"}. {"Action on user","Action sur l'utilisateur"}. {"Add Jabber ID","Ajouter un Jabber ID"}. {"Add New","Ajouter"}. {"Add User","Ajouter un utilisateur"}. {"Administration of ","Administration de "}. {"Administration","Administration"}. {"Administrator privileges required","Les droits d'administrateur sont nécessaires"}. {"All activity","Toute activité"}. {"All Users","Tous les utilisateurs"}. {"Allow subscription","Autoriser l’abonnement"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Autoriser ce Jabber ID à s'abonner à ce nœud PubSub ?"}. {"Allow this person to register with the room?","Autoriser cette personne à enregistrer ce salon ?"}. {"Allow users to change the subject","Autoriser les utilisateurs à changer le sujet"}. {"Allow users to query other users","Autoriser les utilisateurs à envoyer des requêtes aux autres utilisateurs"}. {"Allow users to send invites","Autoriser les utilisateurs à envoyer des invitations"}. {"Allow users to send private messages","Autoriser les utilisateurs à envoyer des messages privés"}. {"Allow visitors to change nickname","Autoriser les visiteurs à changer de pseudo"}. {"Allow visitors to send private messages to","Autoriser les visiteurs à envoyer des messages privés"}. {"Allow visitors to send status text in presence updates","Autoriser les visiteurs à envoyer un message d'état avec leur présence"}. {"Allow visitors to send voice requests","Permettre aux visiteurs d'envoyer des demandes de 'voice'"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Un groupe LDAP associé qui définit l’adhésion à un salon ; cela devrait être un nom distingué LDAP selon la définition spécifique à l’implémentation ou au déploiement d’un groupe."}. {"Announcements","Annonces"}. {"Answer associated with a picture","Réponse associée à une image"}. {"Answer associated with a video","Réponse associée à une vidéo"}. {"Answer associated with speech","Réponse associée à un discours"}. {"Answer to a question","Réponse à une question"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","N’importe qui dans le groupe de la liste spécifiée peut s’abonner et récupérer les items"}. {"Anyone may associate leaf nodes with the collection","N’importe qui peut associer les feuilles avec la collection"}. {"Anyone may publish","N’importe qui peut publier"}. {"Anyone may subscribe and retrieve items","N’importe qui peut s’abonner et récupérer les items"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","N’importe qui avec un abonnement de présence peut s’abonner et récupérer les items"}. {"Anyone with Voice","N’importe qui avec Voice"}. {"Anyone","N’importe qui"}. {"April","Avril"}. {"Attribute 'channel' is required for this request","L’attribut « channel » est requis pour la requête"}. {"Attribute 'id' is mandatory for MIX messages","L’attribut « id » est obligatoire pour les messages MIX"}. {"Attribute 'jid' is not allowed here","L’attribut « jid » n’est pas autorisé ici"}. {"Attribute 'node' is not allowed here","L’attribut « node » n’est pas autorisé ici"}. {"Attribute 'to' of stanza that triggered challenge","L’attribut « to » de la strophe qui a déclenché le défi"}. {"August","Août"}. {"Automatic node creation is not enabled","La creation implicite de nœud n'est pas disponible"}. {"Backup Management","Gestion des sauvegardes"}. {"Backup of ~p","Sauvegarde de ~p"}. {"Backup to File at ","Sauvegarde fichier sur "}. {"Backup","Sauvegarde"}. {"Bad format","Mauvais format"}. {"Birthday","Date d'anniversaire"}. {"Both the username and the resource are required","Le nom d'utilisateur et sa ressource sont nécessaires"}. {"Bytestream already activated","Le flux SOCKS5 est déjà activé"}. {"Cannot remove active list","La liste active ne peut être supprimée"}. {"Cannot remove default list","La liste par défaut ne peut être supprimée"}. {"CAPTCHA web page","Page web de CAPTCHA"}. {"Challenge ID","Identifiant du défi"}. {"Change Password","Modifier le mot de passe"}. {"Change User Password","Changer le mot de passe de l'utilisateur"}. {"Changing password is not allowed","La modification du mot de passe n'est pas autorisée"}. {"Changing role/affiliation is not allowed","La modification role/affiliation n'est pas autorisée"}. {"Channel already exists","Ce canal existe déjà"}. {"Channel does not exist","Le canal n’existe pas"}. {"Channels","Canaux"}. {"Characters not allowed:","Caractères non autorisés :"}. {"Chatroom configuration modified","Configuration du salon modifiée"}. {"Chatroom is created","Le salon de discussion est créé"}. {"Chatroom is destroyed","Le salon de discussion est détruit"}. {"Chatroom is started","Le salon de discussion a démarré"}. {"Chatroom is stopped","Le salon de discussion est stoppé"}. {"Chatrooms","Salons de discussion"}. {"Choose a username and password to register with this server","Choisissez un nom d'utilisateur et un mot de passe pour ce serveur"}. {"Choose storage type of tables","Choisissez un type de stockage pour les tables"}. {"Choose whether to approve this entity's subscription.","Choisissez d'approuver ou non l'abonnement de cette entité."}. {"City","Ville"}. {"Client acknowledged more stanzas than sent by server","Le client accuse réception de plus de strophes que ce qui est envoyé par le serveur"}. {"Commands","Commandes"}. {"Conference room does not exist","Le salon de discussion n'existe pas"}. {"Configuration of room ~s","Configuration pour le salon ~s"}. {"Configuration","Configuration"}. {"Connected Resources:","Ressources connectées :"}. {"Contact Addresses (normally, room owner or owners)","Adresses de contact (normalement les administrateurs du salon)"}. {"Country","Pays"}. {"CPU Time:","Temps CPU :"}. {"Current Discussion Topic","Sujet de discussion courant"}. {"Database failure","Échec sur la base de données"}. {"Database Tables at ~p","Tables de base de données sur ~p"}. {"Database Tables Configuration at ","Configuration des tables de base de données sur "}. {"Database","Base de données"}. {"December","Décembre"}. {"Default users as participants","Les utilisateurs sont participant par défaut"}. {"Delete content","Supprimer le contenu"}. {"Delete message of the day on all hosts","Supprimer le message du jour sur tous les domaines"}. {"Delete message of the day","Supprimer le message du jour"}. {"Delete Selected","Suppression des éléments sélectionnés"}. {"Delete table","Supprimer la table"}. {"Delete User","Supprimer l'utilisateur"}. {"Deliver event notifications","Envoyer les notifications d'événement"}. {"Deliver payloads with event notifications","Inclure le contenu du message avec la notification"}. {"Description:","Description :"}. {"Disc only copy","Copie sur disque uniquement"}. {"'Displayed groups' not added (they do not exist!): ","« Groupes affichés » non ajoutés (ils n’existent pas !) : "}. {"Displayed:","Affichés :"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Ne révélez votre mot de passe à personne, pas même aux administrateurs du serveur XMPP."}. {"Dump Backup to Text File at ","Enregistrer la sauvegarde dans un fichier texte sur "}. {"Dump to Text File","Sauvegarder dans un fichier texte"}. {"Duplicated groups are not allowed by RFC6121","Les groupes dupliqués ne sont pas autorisés par la RFC6121"}. {"Dynamically specify a replyto of the item publisher","Spécifie dynamiquement un « réponse à » de l’item de l’éditeur"}. {"Edit Properties","Modifier les propriétés"}. {"Either approve or decline the voice request.","Accepter ou refuser la demande de voix."}. {"ejabberd MUC module","Module MUC ejabberd"}. {"ejabberd Multicast service","Service de Multidiffusion d'ejabberd"}. {"ejabberd Publish-Subscribe module","Module Publish-Subscribe d'ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams per ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Console Web d'administration de ejabberd"}. {"Elements","Éléments"}. {"Email Address","Adresse courriel"}. {"Email","Courriel"}. {"Enable logging","Activer l'archivage"}. {"Enable message archiving","Activer l'archivage de messages"}. {"Enabling push without 'node' attribute is not supported","L'activation push ne peut se faire sans l'attribut 'node'"}. {"End User Session","Terminer la session de l'utilisateur"}. {"Enter nickname you want to register","Entrez le pseudo que vous souhaitez enregistrer"}. {"Enter path to backup file","Entrez le chemin vers le fichier de sauvegarde"}. {"Enter path to jabberd14 spool dir","Entrez le chemin vers le répertoire spool de Jabberd 1.4"}. {"Enter path to jabberd14 spool file","Entrez le chemin vers le fichier spool de Jabberd 1.4"}. {"Enter path to text file","Entrez le chemin vers le fichier texte"}. {"Enter the text you see","Tapez le texte que vous voyez"}. {"Error","Erreur"}. {"Exclude Jabber IDs from CAPTCHA challenge","Exempter des Jabberd IDs du test CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exporter toutes les tables vers un fichier SQL :"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exporter les données de tous les utilisateurs du serveur vers un fichier PIEFXIS (XEP-0227) :"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exporter les données utilisateurs d'un hôte vers un fichier PIEFXIS (XEP-0227) :"}. {"External component failure","Erreur de composant externe"}. {"External component timeout","Dépassement de delai du composant externe"}. {"Failed to activate bytestream","Échec d'activation de bytestream"}. {"Failed to extract JID from your voice request approval","Échec d'extraction du JID dans la requête de voix"}. {"Failed to map delegated namespace to external component","Échec d'association d'espace de nom vers un composant externe"}. {"Failed to parse HTTP response","Échec de lecture de la réponse HTTP"}. {"Failed to process option '~s'","Échec de traitement de l'option '~s'"}. {"Family Name","Nom de famille"}. {"FAQ Entry","Entrée FAQ"}. {"February","Février"}. {"File larger than ~w bytes","Taille de fichier suppérieur à ~w octets"}. {"Fill in the form to search for any matching XMPP User","Complétez le formulaire pour rechercher un utilisateur XMPP correspondant"}. {"Friday","Vendredi"}. {"From","De"}. {"Full Name","Nom complet"}. {"Get Number of Online Users","Récupérer le nombre d'utilisateurs en ligne"}. {"Get Number of Registered Users","Récupérer le nombre d'utilisateurs enregistrés"}. {"Get User Last Login Time","Récupérer la dernière date de connexion de l'utilisateur"}. {"Get User Password","Récupérer le mot de passe de l'utilisateur"}. {"Get User Statistics","Récupérer les statistiques de l'utilisateur"}. {"Given Name","Nom"}. {"Grant voice to this person?","Accorder le droit de parole à cet utilisateur ?"}. {"Group","Groupe"}. {"Groups that will be displayed to the members","Groupes qui seront affichés aux membres"}. {"Groups","Groupes"}. {"has been banned","a été banni"}. {"has been kicked because of a system shutdown","a été éjecté en raison de l'arrêt du système"}. {"has been kicked because of an affiliation change","a été éjecté à cause d'un changement d'autorisation"}. {"has been kicked because the room has been changed to members-only","a été éjecté car la salle est désormais réservée aux membres"}. {"has been kicked","a été expulsé"}. {"Host unknown","Serveur inconnu"}. {"Host","Serveur"}. {"HTTP File Upload","Téléversement de fichier HTTP"}. {"If you don't see the CAPTCHA image here, visit the web page.","SI vous ne voyez pas l'image CAPTCHA ici, visitez la page web."}. {"Import Directory","Importer un répertoire"}. {"Import File","Importer un fichier"}. {"Import user data from jabberd14 spool file:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4 :"}. {"Import User from File at ","Importer un utilisateur depuis le fichier sur "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importer les données utilisateurs à partir d'un fichier PIEFXIS (XEP-0227) :"}. {"Import users data from jabberd14 spool directory:","Importer des utilisateurs depuis un fichier spool Jabberd 1.4 :"}. {"Import Users from Dir at ","Importer des utilisateurs depuis le répertoire sur "}. {"Import Users From jabberd14 Spool Files","Importer des utilisateurs depuis un fichier spool Jabberd 1.4"}. {"Improper domain part of 'from' attribute","Le domaine de l'attribut 'from' est incorrect"}. {"Improper message type","Mauvais type de message"}. {"Incoming s2s Connections:","Connexions s2s entrantes :"}. {"Incorrect CAPTCHA submit","Entrée CAPTCHA incorrecte"}. {"Incorrect data form","Formulaire incorrect"}. {"Incorrect password","Mot de passe incorrect"}. {"Incorrect value of 'action' attribute","Valeur de l'attribut 'action' incorrecte"}. {"Incorrect value of 'action' in data form","Valeur de l'attribut 'action' incorrecte dans le formulaire"}. {"Incorrect value of 'path' in data form","Valeur de l'attribut 'path' incorrecte dans le formulaire"}. {"Insufficient privilege","Droits insuffisants"}. {"Internal server error","Erreur interne du serveur"}. {"Invalid 'from' attribute in forwarded message","L'attribut 'from' du message transféré est incorrect"}. {"Invalid node name","Nom de nœud invalide"}. {"Invalid 'previd' value","Valeur 'previd' invalide"}. {"Invitations are not allowed in this conference","Les invitations ne sont pas autorisées dans ce salon"}. {"IP addresses","Adresses IP"}. {"is now known as","est maintenant connu comme"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","L'envoyer de messages d'erreur au salon n'est pas autorisé. Le participant (~s) à envoyé un message d'erreur (~s) et à été expulsé du salon"}. {"It is not allowed to send private messages of type \"groupchat\"","Il n'est pas permis d'envoyer des messages privés de type \"groupchat\""}. {"It is not allowed to send private messages to the conference","Il n'est pas permis d'envoyer des messages privés à la conférence"}. {"It is not allowed to send private messages","L'envoi de messages privés n'est pas autorisé"}. {"Jabber ID","Jabber ID"}. {"January","Janvier"}. {"joins the room","rejoint le salon"}. {"July","Juillet"}. {"June","Juin"}. {"Just created","Vient d'être créé"}. {"Label:","Étiquette :"}. {"Last Activity","Dernière activité"}. {"Last login","Dernière connexion"}. {"Last month","Dernier mois"}. {"Last year","Dernière année"}. {"leaves the room","quitte le salon"}. {"List of rooms","Liste des salons"}. {"Low level update script","Script de mise à jour de bas-niveau"}. {"Make participants list public","Rendre la liste des participants publique"}. {"Make room CAPTCHA protected","Protéger le salon par un CAPTCHA"}. {"Make room members-only","Réserver le salon aux membres uniquement"}. {"Make room moderated","Rendre le salon modéré"}. {"Make room password protected","Protéger le salon par mot de passe"}. {"Make room persistent","Rendre le salon persistant"}. {"Make room public searchable","Rendre le salon public"}. {"Malformed username","Nom d'utilisateur invalide"}. {"March","Mars"}. {"Max payload size in bytes","Taille maximum pour le contenu du message en octet"}. {"Maximum file size","Taille maximale du fichier"}. {"Maximum Number of History Messages Returned by Room","Nombre maximal de messages d'historique renvoyés par salle"}. {"Maximum number of items to persist","Nombre maximal d'éléments à conserver"}. {"Maximum Number of Occupants","Nombre maximal d'occupants"}. {"May","Mai"}. {"Membership is required to enter this room","Vous devez être membre pour accèder à ce salon"}. {"Members:","Membres :"}. {"Memory","Mémoire"}. {"Message body","Corps du message"}. {"Message not found in forwarded payload","Message non trouvé dans l'enveloppe transférée"}. {"Messages from strangers are rejected","Les messages d'étrangers sont rejetés"}. {"Messages of type headline","Messages de type titre"}. {"Messages of type normal","Messages de type normal"}. {"Middle Name","Autre nom"}. {"Minimum interval between voice requests (in seconds)","Intervalle minimum entre les demandes de 'voice' (en secondes)"}. {"Moderator privileges required","Les droits de modérateur sont nécessaires"}. {"Modified modules","Modules mis à jour"}. {"Module failed to handle the query","Échec de traitement de la demande"}. {"Monday","Lundi"}. {"Multicast","Multidiffusion"}. {"Multi-User Chat","Discussion de groupe"}. {"Name","Nom"}. {"Name:","Nom :"}. {"Natural Language for Room Discussions","Langue naturelle pour les discussions en salle"}. {"Natural-Language Room Name","Nom de la salle en langue naturelle"}. {"Neither 'jid' nor 'nick' attribute found","Attribut 'jid' ou 'nick' absent"}. {"Neither 'role' nor 'affiliation' attribute found","Attribut 'role' ou 'affiliation' absent"}. {"Never","Jamais"}. {"New Password:","Nouveau mot de passe :"}. {"Nickname Registration at ","Enregistrement d'un pseudo sur "}. {"Nickname ~s does not exist in the room","Le pseudo ~s n'existe pas dans ce salon"}. {"Nickname","Pseudo"}. {"No 'affiliation' attribute found","Attribut 'affiliation' absent"}. {"No available resource found","Aucune ressource disponible"}. {"No body provided for announce message","Pas de corps de message pour l'annonce"}. {"No data form found","Formulaire non trouvé"}. {"No Data","Aucune information disponible"}. {"No features available","Aucune fonctionalité disponible"}. {"No hook has processed this command","Aucun gestionnaire n'a pris en charge cette commande"}. {"No info about last activity found","Aucune activité précédente trouvée"}. {"No 'item' element found","Aucun élément 'item' trouvé"}. {"No items found in this query","Aucun item trouvé dans cette requête"}. {"No limit","Pas de limite"}. {"No module is handling this query","Aucun module ne supporte cette requête"}. {"No node specified","Nœud non spécifié"}. {"No 'password' found in data form","Entrée 'password' absente du formulaire"}. {"No 'password' found in this query","L'élément 'password' est absent de la requête"}. {"No 'path' found in data form","Entrée 'path' absente du formulaire"}. {"No pending subscriptions found","Aucune demande d'abonnement trouvée"}. {"No privacy list with this name found","Liste non trouvée"}. {"No private data found in this query","Aucune donnée privée trouvée dans cette requête"}. {"No running node found","Nœud non trouvé"}. {"No services available","Aucun service disponible"}. {"No statistics found for this item","Pas de statistiques"}. {"No 'to' attribute found in the invitation","L'élément 'to' est absent de l'invitation"}. {"Node already exists","Ce nœud existe déjà"}. {"Node ID","Identifiant du nœud"}. {"Node index not found","Index de nœud non trouvé"}. {"Node not found","Nœud non trouvé"}. {"Node ~p","Nœud ~p"}. {"Nodeprep has failed","Échec de formattage"}. {"Nodes","Nœuds"}. {"None","Aucun"}. {"Not Found","Nœud non trouvé"}. {"Not subscribed","Pas abonné"}. {"Notify subscribers when items are removed from the node","Avertir les abonnés lorsque des éléments sont supprimés sur le nœud"}. {"Notify subscribers when the node configuration changes","Avertir les abonnés lorsque la configuration du nœud change"}. {"Notify subscribers when the node is deleted","Avertir les abonnés lorsque le nœud est supprimé"}. {"November","Novembre"}. {"Number of occupants","Nombre d'occupants"}. {"Number of online users","Nombre d'utilisateurs en ligne"}. {"Number of registered users","Nombre d'utilisateurs enregistrés"}. {"October","Octobre"}. {"Offline Messages","Messages en attente"}. {"Offline Messages:","Messages hors ligne :"}. {"OK","OK"}. {"Old Password:","Ancien mot de passe :"}. {"Online Users:","Utilisateurs connectés :"}. {"Online Users","Utilisateurs en ligne"}. {"Online","En ligne"}. {"Only admins can see this","Seuls les administrateurs peuvent voir cela"}. {"Only deliver notifications to available users","Envoyer les notifications uniquement aux utilisateurs disponibles"}. {"Only <enable/> or <disable/> tags are allowed","Seul le tag <enable/> ou <disable/> est autorisé"}. {"Only <list/> element is allowed in this query","Seul l'élément <list/> est autorisé dans cette requête"}. {"Only members may query archives of this room","Seuls les membres peuvent accéder aux archives de ce salon"}. {"Only moderators and participants are allowed to change the subject in this room","Seuls les modérateurs et les participants peuvent changer le sujet dans ce salon"}. {"Only moderators are allowed to change the subject in this room","Seuls les modérateurs peuvent changer le sujet dans ce salon"}. {"Only moderators can approve voice requests","Seuls les modérateurs peuvent accépter les requêtes voix"}. {"Only occupants are allowed to send messages to the conference","Seuls les occupants peuvent envoyer des messages à la conférence"}. {"Only occupants are allowed to send queries to the conference","Seuls les occupants sont autorisés à envoyer des requêtes à la conférence"}. {"Only publishers may publish","Seuls les éditeurs peuvent publier"}. {"Only service administrators are allowed to send service messages","Seuls les administrateurs du service sont autoriser à envoyer des messages de service"}. {"Organization Name","Nom de l'organisation"}. {"Organization Unit","Unité de l'organisation"}. {"Outgoing s2s Connections","Connexions s2s sortantes"}. {"Outgoing s2s Connections:","Connexions s2s sortantes :"}. {"Owner privileges required","Les droits de propriétaire sont nécessaires"}. {"Packet","Paquet"}. {"Participant","Participant"}. {"Password Verification","Vérification du mot de passe"}. {"Password Verification:","Vérification du mot de passe :"}. {"Password","Mot de passe"}. {"Password:","Mot de passe :"}. {"Path to Dir","Chemin vers le répertoire"}. {"Path to File","Chemin vers le fichier"}. {"Pending","En suspens"}. {"Period: ","Période : "}. {"Persist items to storage","Stockage persistant des éléments"}. {"Ping query is incorrect","Requête ping incorrecte"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ces options sauvegardent uniquement la base de données interne Mnesia. Si vous utilisez le module ODBC vous devez sauvegarde votre base SQL séparément."}. {"Please, wait for a while before sending new voice request","Attendez un moment avant de re-lancer une requête de voix"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Rendre le Jabber ID réel visible pour"}. {"private, ","privé"}. {"Publish-Subscribe","Publication-Abonnement"}. {"PubSub subscriber request","Demande d'abonnement PubSub"}. {"Purge all items when the relevant publisher goes offline","Purger tous les items lorsque publieur est hors-ligne"}. {"Queries to the conference members are not allowed in this room","Les requêtes sur les membres de la conférence ne sont pas autorisé dans ce salon"}. {"Query to another users is forbidden","Requête vers un autre utilisateur interdite"}. {"RAM and disc copy","Copie en mémoire vive (RAM) et sur disque"}. {"RAM copy","Copie en mémoire vive (RAM)"}. {"Really delete message of the day?","Confirmer la suppression du message du jour ?"}. {"Recipient is not in the conference room","Le destinataire n'est pas dans la conférence"}. {"Registered Users","Utilisateurs enregistrés"}. {"Registered Users:","Utilisateurs enregistrés :"}. {"Register","Enregistrer"}. {"Remote copy","Copie distante"}. {"Remove All Offline Messages","Effacer tous les messages hors ligne"}. {"Remove User","Supprimer l'utilisateur"}. {"Remove","Supprimer"}. {"Replaced by new connection","Remplacé par une nouvelle connexion"}. {"Request has timed out","La demande a expiré"}. {"Request is ignored","La demande est ignorée"}. {"Requested role","Rôle demandé"}. {"Resources","Ressources"}. {"Restart Service","Redémarrer le service"}. {"Restart","Redémarrer"}. {"Restore Backup from File at ","Restaurer la sauvegarde depuis le fichier sur "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restauration de la sauvegarde binaire après redémarrage (nécessite moins de mémoire) :"}. {"Restore binary backup immediately:","Restauration immédiate d'une sauvegarde binaire :"}. {"Restore plain text backup immediately:","Restauration immédiate d'une sauvegarde texte :"}. {"Restore","Restauration"}. {"Room Configuration","Configuration du salon"}. {"Room creation is denied by service policy","La création de salons est interdite par le service"}. {"Room description","Description du salon"}. {"Room Occupants","Occupants du salon"}. {"Room title","Titre du salon"}. {"Roster groups allowed to subscribe","Groupes de liste de contact autorisés à s'abonner"}. {"Roster size","Taille de la liste de contacts"}. {"RPC Call Error","Erreur d'appel RPC"}. {"Running Nodes","Nœuds actifs"}. {"Saturday","Samedi"}. {"Script check","Validation du script"}. {"Search Results for ","Résultats de recherche pour "}. {"Search users in ","Rechercher des utilisateurs "}. {"Send announcement to all online users on all hosts","Envoyer l'annonce à tous les utilisateurs en ligne sur tous les serveurs"}. {"Send announcement to all online users","Envoyer l'annonce à tous les utilisateurs en ligne"}. {"Send announcement to all users on all hosts","Envoyer une annonce à tous les utilisateurs de tous les domaines"}. {"Send announcement to all users","Envoyer l'annonce à tous les utilisateurs"}. {"September","Septembre"}. {"Server:","Serveur :"}. {"Set message of the day and send to online users","Définir le message du jour et l'envoyer aux utilisateurs en ligne"}. {"Set message of the day on all hosts and send to online users","Définir le message du jour pour tous domaines et l'envoyer aux utilisateurs en ligne"}. {"Shared Roster Groups","Groupes de liste de contacts partagée"}. {"Show Integral Table","Montrer la table intégralement"}. {"Show Ordinary Table","Montrer la table ordinaire"}. {"Shut Down Service","Arrêter le service"}. {"Specify the access model","Définir le modèle d'accès"}. {"Specify the event message type","Définir le type de message d'événement"}. {"Specify the publisher model","Définir le modèle de publication"}. {"Statistics of ~p","Statistiques de ~p"}. {"Statistics","Statistiques"}. {"Stop","Arrêter"}. {"Stopped Nodes","Nœuds arrêtés"}. {"Storage Type","Type de stockage"}. {"Store binary backup:","Sauvegarde binaire :"}. {"Store plain text backup:","Sauvegarde texte :"}. {"Subject","Sujet"}. {"Submit","Soumettre"}. {"Submitted","Soumis"}. {"Subscriber Address","Adresse de l'abonné"}. {"Subscription","Abonnement"}. {"Subscriptions are not allowed","Les abonnement ne sont pas autorisés"}. {"Sunday","Dimanche"}. {"That nickname is already in use by another occupant","Le pseudo est déjà utilisé par un autre occupant"}. {"That nickname is registered by another person","Le pseudo est enregistré par une autre personne"}. {"The CAPTCHA is valid.","Le CAPTCHA est valide"}. {"The CAPTCHA verification has failed","La vérification du CAPTCHA a échoué"}. {"The collections with which a node is affiliated","Les collections avec lesquelle un nœud est affilié"}. {"The default language of the node","La langue par défaut du nœud"}. {"The feature requested is not supported by the conference","La demande de fonctionalité n'est pas supportée par la conférence"}. {"The name of the node","Le nom du nœud"}. {"The password contains unacceptable characters","Le mot de passe contient des caractères non-acceptables"}. {"The password is too weak","Le mot de passe est trop faible"}. {"the password is","le mot de passe est"}. {"The query is only allowed from local users","La requête n'est autorisé qu'aux utilisateurs locaux"}. {"The query must not contain <item/> elements","La requête ne doit pas contenir d'élément <item/>"}. {"There was an error creating the account: ","Il y a eu une erreur en créant le compte : "}. {"There was an error deleting the account: ","Il y a eu une erreur en effaçant le compte : "}. {"This room is not anonymous","Ce salon n'est pas anonyme"}. {"Thursday","Jeudi"}. {"Time delay","Délais"}. {"Time","Heure"}. {"To register, visit ~s","Pour vous enregistrer, visitez ~s"}. {"To","A"}. {"Token TTL","Jeton TTL"}. {"Too many active bytestreams","Trop de flux SOCKS5 actifs"}. {"Too many CAPTCHA requests","Trop de requêtes CAPTCHA"}. {"Too many <item/> elements","Trop d'éléments <item/>"}. {"Too many <list/> elements","Trop d'éléments <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Trop (~p) d'authentification ont échoué pour cette adresse IP (~s). L'adresse sera débloquée à ~s UTC"}. {"Too many unacked stanzas","Trop de stanzas sans accusé de réception (ack)"}. {"Too many users in this conference","Trop d'utilisateurs dans cette conférence"}. {"Total rooms","Nombre de salons"}. {"Traffic rate limit is exceeded","La limite de trafic a été dépassée"}. {"Transactions Aborted:","Transactions annulées :"}. {"Transactions Committed:","Transactions commitées :"}. {"Transactions Logged:","Transactions journalisées :"}. {"Transactions Restarted:","Transactions redémarrées :"}. {"Tuesday","Mardi"}. {"Unable to generate a CAPTCHA","Impossible de générer le CAPTCHA"}. {"Unable to register route on existing local domain","Impossible d'enregistrer la route sur un domaine locale existant"}. {"Unauthorized","Non autorisé"}. {"Unexpected action","Action inattendu"}. {"Unregister","Désinscrire"}. {"Unsupported <index/> element","Elément <index/> non supporté"}. {"Update message of the day (don't send)","Mise à jour du message du jour (pas d'envoi)"}. {"Update message of the day on all hosts (don't send)","Mettre à jour le message du jour sur tous les domaines (ne pas envoyer)"}. {"Update plan","Plan de mise à jour"}. {"Update ~p","Mise à jour de ~p"}. {"Update script","Script de mise à jour"}. {"Update","Mettre à jour"}. {"Uptime:","Temps depuis le démarrage :"}. {"User already exists","L'utilisateur existe déjà"}. {"User JID","JID de l'utilisateur "}. {"User (jid)","Utilisateur (jid)"}. {"User Management","Gestion des utilisateurs"}. {"User session not found","Session utilisateur non trouvée"}. {"User session terminated","Session utilisateur terminée"}. {"Username:","Nom d'utilisateur :"}. {"Users are not allowed to register accounts so quickly","Les utilisateurs ne sont pas autorisés à enregistrer des comptes si rapidement"}. {"Users Last Activity","Dernière activité des utilisateurs"}. {"Users","Utilisateurs"}. {"User","Utilisateur"}. {"Validate","Valider"}. {"Value 'get' of 'type' attribute is not allowed","La valeur de l'attribut 'type' ne peut être 'get'"}. {"Value of '~s' should be boolean","La valeur de '~s' ne peut être booléen"}. {"Value of '~s' should be datetime string","La valeur de '~s' doit être une chaine datetime"}. {"Value of '~s' should be integer","La valeur de '~s' doit être un entier"}. {"Value 'set' of 'type' attribute is not allowed","La valeur de l'attribut 'type' ne peut être 'set'"}. {"vCard User Search","Recherche dans l'annnuaire"}. {"Virtual Hosts","Serveurs virtuels"}. {"Visitors are not allowed to change their nicknames in this room","Les visiteurs ne sont pas autorisés à changer de pseudo dans ce salon"}. {"Visitors are not allowed to send messages to all occupants","Les visiteurs ne sont pas autorisés à envoyer des messages à tout les occupants"}. {"Voice request","Demande de voix"}. {"Voice requests are disabled in this conference","Les demandes de voix sont désactivées dans cette conférence"}. {"Wednesday","Mercredi"}. {"When a new subscription is processed","Quand un nouvel abonnement est traité"}. {"When to send the last published item","A quel moment envoyer le dernier élément publié"}. {"Whether to allow subscriptions","Autoriser l'abonnement ?"}. {"You have been banned from this room","Vous avez été exclus de ce salon"}. {"You have joined too many conferences","Vous avec rejoint trop de conférences"}. {"You must fill in field \"Nickname\" in the form","Vous devez préciser le champ \"pseudo\" dans le formulaire"}. {"You need a client that supports x:data and CAPTCHA to register","Vous avez besoin d'un client prenant en charge x:data et CAPTCHA pour enregistrer un pseudo"}. {"You need a client that supports x:data to register the nickname","Vous avez besoin d'un client prenant en charge x:data pour enregistrer un pseudo"}. {"You need an x:data capable client to search","Vous avez besoin d'un client supportant x:data pour faire une recherche"}. {"Your active privacy list has denied the routing of this stanza.","Votre règle de flitrage active a empêché le routage de ce stanza."}. {"Your contact offline message queue is full. The message has been discarded.","La file d'attente de message de votre contact est pleine. Votre message a été détruit."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Vos messages pour ~s sont bloqués. Pour les débloquer, veuillez visiter ~s"}. {"You're not allowed to create nodes","Vous n'êtes pas autorisé à créer des nœuds"}. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/gl.msg���������������������������������������������������������������������0000644�0002322�0002322�00000070204�14154362354�016700� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," puxo o asunto: "}. {"A friendly name for the node","Un nome sinxelo para o nodo"}. {"A password is required to enter this room","Necesítase contrasinal para entrar nesta sala"}. {"Accept","Aceptar"}. {"Access denied by service policy","Acceso denegado pola política do servizo"}. {"Action on user","Acción no usuario"}. {"Add Jabber ID","Engadir ID Jabber"}. {"Add New","Engadir novo"}. {"Add User","Engadir usuario"}. {"Administration of ","Administración de "}. {"Administration","Administración"}. {"Administrator privileges required","Necesítase privilexios de administrador"}. {"All activity","Toda a actividade"}. {"All Users","Todos os usuarios"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Desexas permitir a este JabberID que se subscriba a este nodo PubSub?"}. {"Allow users to change the subject","Permitir aos usuarios cambiar o asunto"}. {"Allow users to query other users","Permitir aos usuarios consultar a outros usuarios"}. {"Allow users to send invites","Permitir aos usuarios enviar invitacións"}. {"Allow users to send private messages","Permitir aos usuarios enviar mensaxes privadas"}. {"Allow visitors to change nickname","Permitir aos visitantes cambiarse o alcume"}. {"Allow visitors to send private messages to","Permitir aos visitantes enviar mensaxes privadas a"}. {"Allow visitors to send status text in presence updates","Permitir aos visitantes enviar texto de estado nas actualizacións depresenza"}. {"Allow visitors to send voice requests","Permitir aos visitantes enviar peticións de voz"}. {"Announcements","Anuncios"}. {"April","Abril"}. {"August","Agosto"}. {"Automatic node creation is not enabled","A creación automática de nodos non está habilitada"}. {"Backup Management","Xestión de copia de seguridade"}. {"Backup of ~p","Copia de seguridade de ~p"}. {"Backup to File at ","Copia de seguridade de arquivos en "}. {"Backup","Copia de seguridade"}. {"Bad format","Mal formato"}. {"Birthday","Aniversario"}. {"Both the username and the resource are required","Tanto o nome de usuario como o recurso son necesarios"}. {"Bytestream already activated","Bytestream xa está activado"}. {"Cannot remove active list","Non se pode eliminar a lista activa"}. {"Cannot remove default list","Non se pode eliminar a lista predeterminada"}. {"CAPTCHA web page","CAPTCHA páxina Web"}. {"Change Password","Cambiar contrasinal"}. {"Change User Password","Cambiar contrasinal de usuario"}. {"Changing password is not allowed","Non se permite cambiar o contrasinal"}. {"Changing role/affiliation is not allowed","O cambio de rol/afiliación non está permitido"}. {"Characters not allowed:","Caracteres non permitidos:"}. {"Chatroom configuration modified","Configuración da sala modificada"}. {"Chatroom is created","Creouse a sala"}. {"Chatroom is destroyed","Destruíuse a sala"}. {"Chatroom is started","Iniciouse a sala"}. {"Chatroom is stopped","Detívose a sala"}. {"Chatrooms","Salas de charla"}. {"Choose a username and password to register with this server","Escolle un nome de usuario e contrasinal para rexistrarche neste servidor"}. {"Choose storage type of tables","Selecciona tipo de almacenamento das táboas"}. {"Choose whether to approve this entity's subscription.","Decidir se aprobar a subscripción desta entidade."}. {"City","Cidade"}. {"Commands","Comandos"}. {"Conference room does not exist","A sala de conferencias non existe"}. {"Configuration of room ~s","Configuración para a sala ~s"}. {"Configuration","Configuración"}. {"Connected Resources:","Recursos conectados:"}. {"Country","País"}. {"CPU Time:","Tempo da CPU:"}. {"Database failure","Erro na base de datos"}. {"Database Tables at ~p","Táboas da base de datos en ~p"}. {"Database Tables Configuration at ","Configuración de táboas da base de datos en "}. {"Database","Base de datos"}. {"December","Decembro"}. {"Default users as participants","Os usuarios son participantes por defecto"}. {"Delete message of the day on all hosts","Borrar a mensaxe do día en todos os dominios"}. {"Delete message of the day","Borrar mensaxe do dia"}. {"Delete Selected","Eliminar os seleccionados"}. {"Delete User","Borrar usuario"}. {"Deliver event notifications","Entregar notificacións de eventos"}. {"Deliver payloads with event notifications","Enviar payloads xunto coas notificacións de eventos"}. {"Description:","Descrición:"}. {"Disc only copy","Copia en disco soamente"}. {"Dump Backup to Text File at ","Exporta copia de seguridade a ficheiro de texto en "}. {"Dump to Text File","Exportar a ficheiro de texto"}. {"Edit Properties","Editar Propiedades"}. {"Either approve or decline the voice request.","Aproba ou rexeita a petición de voz."}. {"ejabberd MUC module","Módulo de MUC para ejabberd"}. {"ejabberd Multicast service","Servizo Multicast de ejabberd"}. {"ejabberd Publish-Subscribe module","Módulo de Publicar-Subscribir de ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Módulo SOCKS5 Bytestreams para ejabberd"}. {"ejabberd vCard module","Módulo vCard para ejabberd"}. {"ejabberd Web Admin","ejabberd Administrador Web"}. {"Elements","Elementos"}. {"Email","Email"}. {"Enable logging","Gardar históricos"}. {"Enable message archiving","Activar o almacenamento de mensaxes"}. {"Enabling push without 'node' attribute is not supported","Non se admite a activación do empuxe sen o atributo 'nodo'"}. {"End User Session","Pechar sesión de usuario"}. {"Enter nickname you want to register","Introduce o alcume que queiras rexistrar"}. {"Enter path to backup file","Introduce ruta ao ficheiro de copia de seguridade"}. {"Enter path to jabberd14 spool dir","Introduce a ruta ao directorio de jabberd14 spools"}. {"Enter path to jabberd14 spool file","Introduce ruta ao ficheiro jabberd14 spool"}. {"Enter path to text file","Introduce ruta ao ficheiro de texto"}. {"Enter the text you see","Introduza o texto que ves"}. {"Error","Erro"}. {"Exclude Jabber IDs from CAPTCHA challenge","Excluír Jabber IDs das probas de CAPTCHA"}. {"Export all tables as SQL queries to a file:","Exportar todas as táboas a un ficheiro SQL:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportar datos de todos os usuarios do servidor a ficheros PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportar datos dos usuarios dun dominio a ficheiros PIEFXIS (XEP-0227):"}. {"External component failure","Fallo de compoñente externo"}. {"External component timeout","Paso o tempo de espera do compoñente externo"}. {"Failed to activate bytestream","Fallo ao activar bytestream"}. {"Failed to extract JID from your voice request approval","Fallo ao extraer o Jabber ID da túa aprobación de petición de voz"}. {"Failed to map delegated namespace to external component","O mapeo de espazo de nomes delegado fallou ao compoñente externo"}. {"Failed to parse HTTP response","Non se puido analizar a resposta HTTP"}. {"Failed to process option '~s'","Fallo ao procesar a opción '~s'"}. {"Family Name","Apelido"}. {"February","Febreiro"}. {"File larger than ~w bytes","O ficheiro é maior que ~w bytes"}. {"Friday","Venres"}. {"From","De"}. {"Full Name","Nome completo"}. {"Get Number of Online Users","Ver número de usuarios conectados"}. {"Get Number of Registered Users","Ver número de usuarios rexistrados"}. {"Get User Last Login Time","Ver data da última conexión de usuario"}. {"Get User Password","Ver contrasinal de usuario"}. {"Get User Statistics","Ver estatísticas de usuario"}. {"Given Name","Nome"}. {"Grant voice to this person?","¿Conceder voz a esta persoa?"}. {"Group","Grupo"}. {"Groups","Grupos"}. {"has been banned","foi bloqueado"}. {"has been kicked because of a system shutdown","foi expulsado porque o sistema vaise a deter"}. {"has been kicked because of an affiliation change","foi expulsado debido a un cambio de afiliación"}. {"has been kicked because the room has been changed to members-only","foi expulsado, porque a sala cambiouse a só-membros"}. {"has been kicked","foi expulsado"}. {"Host unknown","Dominio descoñecido"}. {"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si non ves a imaxe CAPTCHA aquí, visita a páxina web."}. {"Import Directory","Importar directorio"}. {"Import File","Importar ficheiro"}. {"Import user data from jabberd14 spool file:","Importar usuario de ficheiro spool de jabberd14:"}. {"Import User from File at ","Importa usuario desde ficheiro en "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importar usuarios en un fichero PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importar usuarios do directorio spool de jabberd14:"}. {"Import Users from Dir at ","Importar usuarios desde o directorio en "}. {"Import Users From jabberd14 Spool Files","Importar usuarios de ficheiros spool de jabberd-1.4"}. {"Improper domain part of 'from' attribute","Parte de dominio impropio no atributo 'from'"}. {"Improper message type","Tipo de mensaxe incorrecta"}. {"Incoming s2s Connections:","Conexións S2S saíntes:"}. {"Incorrect CAPTCHA submit","O CAPTCHA proporcionado é incorrecto"}. {"Incorrect data form","Formulario de datos incorrecto"}. {"Incorrect password","Contrasinal incorrecta"}. {"Incorrect value of 'action' attribute","Valor incorrecto do atributo 'action'"}. {"Incorrect value of 'action' in data form","Valor incorrecto de 'action' no formulario de datos"}. {"Incorrect value of 'path' in data form","Valor incorrecto de 'path' no formulario de datos"}. {"Insufficient privilege","Privilexio insuficiente"}. {"Invalid 'from' attribute in forwarded message","Atributo 'from'' non é válido na mensaxe reenviada"}. {"Invitations are not allowed in this conference","As invitacións non están permitidas nesta sala"}. {"IP addresses","Direccións IP"}. {"is now known as","agora coñécese como"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Non está permitido enviar mensaxes de erro á sala. Este participante (~s) enviou unha mensaxe de erro (~s) e foi expulsado da sala"}. {"It is not allowed to send private messages of type \"groupchat\"","Non está permitido enviar mensaxes privadas do tipo \"groupchat\""}. {"It is not allowed to send private messages to the conference","Impedir o envio de mensaxes privadas á sala"}. {"It is not allowed to send private messages","Non está permitido enviar mensaxes privadas"}. {"Jabber ID","Jabber ID"}. {"January","Xaneiro"}. {"joins the room","entra na sala"}. {"July","Xullo"}. {"June","Xuño"}. {"Last Activity","Última actividade"}. {"Last login","Última conexión"}. {"Last month","Último mes"}. {"Last year","Último ano"}. {"leaves the room","sae da sala"}. {"List of rooms","Lista de salas"}. {"Low level update script","Script de actualización a baixo nivel"}. {"Make participants list public","A lista de participantes é pública"}. {"Make room CAPTCHA protected","Protexer a sala con CAPTCHA"}. {"Make room members-only","Sala só para membros"}. {"Make room moderated","Facer sala moderada"}. {"Make room password protected","Protexer a sala con contrasinal"}. {"Make room persistent","Sala permanente"}. {"Make room public searchable","Sala publicamente visible"}. {"Malformed username","Nome de usuario mal formado"}. {"March","Marzo"}. {"Max # of items to persist","Máximo # de elementos que persisten"}. {"Max payload size in bytes","Máximo tamaño do payload en bytes"}. {"Maximum Number of Occupants","Número máximo de ocupantes"}. {"May","Maio"}. {"Membership is required to enter this room","Necesitas ser membro desta sala para poder entrar"}. {"Members:","Membros:"}. {"Memory","Memoria"}. {"Message body","Corpo da mensaxe"}. {"Message not found in forwarded payload","Mensaxe non atopada no contido reenviado"}. {"Middle Name","Segundo nome"}. {"Minimum interval between voice requests (in seconds)","Intervalo mínimo entre peticións de voz (en segundos)"}. {"Moderator privileges required","Necesítase privilexios de moderador"}. {"Moderator","Moderator"}. {"Modified modules","Módulos Modificados"}. {"Module failed to handle the query","O módulo non puido xestionar a consulta"}. {"Monday","Luns"}. {"Multicast","Multicast"}. {"Multi-User Chat","Salas de Charla"}. {"Name","Nome"}. {"Name:","Nome:"}. {"Neither 'jid' nor 'nick' attribute found","Non se atopou o atributo 'jid' nin 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Non se atopou o atributo 'role' nin 'affiliation'"}. {"Never","Nunca"}. {"New Password:","Novo contrasinal:"}. {"Nickname Registration at ","Rexistro do alcume en "}. {"Nickname ~s does not exist in the room","O alcume ~s non existe na sala"}. {"Nickname","Alcume"}. {"No 'affiliation' attribute found","Non se atopou o atributo de 'affiliation'"}. {"No available resource found","Non se atopou ningún recurso"}. {"No body provided for announce message","Non se proporcionou corpo de mensaxe para o anuncio"}. {"No data form found","Non se atopou formulario de datos"}. {"No Data","Sen datos"}. {"No features available","Non hai características dispoñibles"}. {"No hook has processed this command","Ningún evento procesou este comando"}. {"No info about last activity found","Non se atopou información sobre a última actividade"}. {"No 'item' element found","Non se atopou o elemento 'item'"}. {"No items found in this query","Non se atoparon elementos nesta consulta"}. {"No limit","Sen límite"}. {"No module is handling this query","Ningún módulo manexa esta consulta"}. {"No node specified","Non se especificou nodo"}. {"No 'password' found in data form","Non se atopou 'password' no formulario de datos"}. {"No 'password' found in this query","Non se atopou 'password' nesta solicitude"}. {"No 'path' found in data form","Non se atopou 'path' neste formulario de datos"}. {"No pending subscriptions found","Non se atoparon subscricións pendentes"}. {"No privacy list with this name found","Non se atopou ningunha lista de privacidade con este nome"}. {"No private data found in this query","Non se atopou ningún elemento de datos privado nesta solicitude"}. {"No running node found","Non se atoparon nodos activos"}. {"No services available","Non hai servizos dispoñibles"}. {"No statistics found for this item","Non se atopou ningunha estatística para este elemento"}. {"No 'to' attribute found in the invitation","O atributo 'to' non se atopou na invitación"}. {"Node already exists","O nodo xa existe"}. {"Node ID","Nodo ID"}. {"Node index not found","Non se atopou índice de nodo"}. {"Node not found","Nodo non atopado"}. {"Node ~p","Nodo ~p"}. {"Nodeprep has failed","Nodeprep fallou"}. {"Nodes","Nodos"}. {"None","Ningún"}. {"Not Found","Non atopado"}. {"Not subscribed","Non subscrito"}. {"Notify subscribers when items are removed from the node","Notificar subscriptores cando os elementos bórranse do nodo"}. {"Notify subscribers when the node configuration changes","Notificar subscriptores cando cambia a configuración do nodo"}. {"Notify subscribers when the node is deleted","Notificar subscriptores cando o nodo bórrase"}. {"November","Novembro"}. {"Number of occupants","Número de ocupantes"}. {"Number of online users","Número de usuarios conectados"}. {"Number of registered users","Número de usuarios rexistrados"}. {"October","Outubro"}. {"Offline Messages","Mensaxes diferidas"}. {"Offline Messages:","Mensaxes sen conexión:"}. {"OK","Aceptar"}. {"Old Password:","Contrasinal anterior:"}. {"Online Users","Usuarios conectados"}. {"Online Users:","Usuarios conectados:"}. {"Online","Conectado"}. {"Only deliver notifications to available users","Só enviar notificacións aos usuarios dispoñibles"}. {"Only <enable/> or <disable/> tags are allowed","Só se permiten etiquetas <enable/> ou <disable/>"}. {"Only <list/> element is allowed in this query","Só se admite o elemento <list/> nesta consulta"}. {"Only members may query archives of this room","Só membros poden consultar o arquivo de mensaxes da sala"}. {"Only moderators and participants are allowed to change the subject in this room","Só os moderadores e os participantes se lles permite cambiar o tema nesta sala"}. {"Only moderators are allowed to change the subject in this room","Só os moderadores están autorizados a cambiar o tema nesta sala"}. {"Only moderators can approve voice requests","Só os moderadores poden aprobar peticións de voz"}. {"Only occupants are allowed to send messages to the conference","Só os ocupantes poden enviar mensaxes á sala"}. {"Only occupants are allowed to send queries to the conference","Só os ocupantes poden enviar solicitudes á sala"}. {"Only service administrators are allowed to send service messages","Só os administradores do servizo teñen permiso para enviar mensaxes de servizo"}. {"Organization Name","Nome da organización"}. {"Organization Unit","Unidade da organización"}. {"Outgoing s2s Connections","Conexións S2S saíntes"}. {"Outgoing s2s Connections:","Conexións S2S saíntes:"}. {"Owner privileges required","Requírense privilexios de propietario da sala"}. {"Packet","Paquete"}. {"Participant","Participante"}. {"Password Verification","Verificación da contrasinal"}. {"Password Verification:","Verificación da Contrasinal:"}. {"Password","Contrasinal"}. {"Password:","Contrasinal:"}. {"Path to Dir","Ruta ao directorio"}. {"Path to File","Ruta ao ficheiro"}. {"Pending","Pendente"}. {"Period: ","Periodo: "}. {"Persist items to storage","Persistir elementos ao almacenar"}. {"Ping query is incorrect","A solicitude de Ping é incorrecta"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Ten en conta que estas opcións só farán copia de seguridade da base de datos Mnesia. Se está a utilizar o módulo de ODBC, tamén necesita unha copia de seguridade da súa base de datos SQL por separado."}. {"Please, wait for a while before sending new voice request","Por favor, espera un pouco antes de enviar outra petición de voz"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Os Jabber ID reais poden velos"}. {"private, ","privado, "}. {"Publish-Subscribe","Publicar-Subscribir"}. {"PubSub subscriber request","Petición de subscriptor de PubSub"}. {"Purge all items when the relevant publisher goes offline","Purgar todos os elementos cando o editor correspondente desconéctase"}. {"Queries to the conference members are not allowed in this room","Nesta sala non se permiten solicitudes aos membros da sala"}. {"Query to another users is forbidden","É prohibido enviar solicitudes a outros usuarios"}. {"RAM and disc copy","Copia en RAM e disco"}. {"RAM copy","Copia en RAM"}. {"Really delete message of the day?","¿Está seguro que quere borrar a mensaxe do dia?"}. {"Recipient is not in the conference room","O receptor non está na sala de conferencia"}. {"Registered Users","Usuarios rexistrados"}. {"Registered Users:","Usuarios rexistrados:"}. {"Register","Rexistrar"}. {"Remote copy","Copia remota"}. {"Remove All Offline Messages","Borrar Todas as Mensaxes Sen conexión"}. {"Remove User","Eliminar usuario"}. {"Remove","Borrar"}. {"Replaced by new connection","Substituído por unha nova conexión"}. {"Resources","Recursos"}. {"Restart Service","Reiniciar o servizo"}. {"Restart","Reiniciar"}. {"Restore Backup from File at ","Restaura copia de seguridade desde o ficheiro en "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Restaurar copia de seguridade binaria no seguinte reinicio de ejabberd (require menos memoria):"}. {"Restore binary backup immediately:","Restaurar inmediatamente copia de seguridade binaria:"}. {"Restore plain text backup immediately:","Restaurar copias de seguridade de texto plano inmediatamente:"}. {"Restore","Restaurar"}. {"Roles for which Presence is Broadcasted","Roles para os que si se difunde a súa Presenza"}. {"Room Configuration","Configuración da Sala"}. {"Room creation is denied by service policy","Denegar crear a sala por política do servizo"}. {"Room description","Descrición da sala"}. {"Room Occupants","Ocupantes da sala"}. {"Room title","Título da sala"}. {"Roster groups allowed to subscribe","Lista de grupos autorizados a subscribir"}. {"Roster size","Tamaño da lista de contactos"}. {"RPC Call Error","Erro na chamada RPC"}. {"Running Nodes","Nodos funcionando"}. {"Saturday","Sábado"}. {"Script check","Comprobación de script"}. {"Search Results for ","Buscar resultados por "}. {"Search users in ","Buscar usuarios en "}. {"Send announcement to all online users on all hosts","Enviar anuncio a todos os usuarios conectados en todos os dominios"}. {"Send announcement to all online users","Enviar anuncio a todos os usuarios conectados"}. {"Send announcement to all users on all hosts","Enviar anuncio a todos os usuarios en todos os dominios"}. {"Send announcement to all users","Enviar anuncio a todos os usuarios"}. {"September","Setembro"}. {"Server:","Servidor:"}. {"Set message of the day and send to online users","Pór mensaxe do dia e enviar a todos os usuarios conectados"}. {"Set message of the day on all hosts and send to online users","Pór mensaxe do día en todos os dominios e enviar aos usuarios conectados"}. {"Shared Roster Groups","Grupos Compartidos"}. {"Show Integral Table","Mostrar Táboa Integral"}. {"Show Ordinary Table","Mostrar Táboa Ordinaria"}. {"Shut Down Service","Deter o servizo"}. {"Specify the access model","Especifica o modelo de acceso"}. {"Specify the event message type","Especifica o tipo da mensaxe de evento"}. {"Specify the publisher model","Especificar o modelo do publicante"}. {"Statistics of ~p","Estatísticas de ~p"}. {"Statistics","Estatísticas"}. {"Stop","Deter"}. {"Stopped Nodes","Nodos detidos"}. {"Storage Type","Tipo de almacenamento"}. {"Store binary backup:","Gardar copia de seguridade binaria:"}. {"Store plain text backup:","Gardar copia de seguridade en texto plano:"}. {"Subject","Asunto"}. {"Submit","Enviar"}. {"Submitted","Enviado"}. {"Subscriber Address","Dirección do subscriptor"}. {"Subscriptions are not allowed","Non se permiten subscricións"}. {"Subscription","Subscripción"}. {"Sunday","Domingo"}. {"That nickname is already in use by another occupant","Ese alcume xa está a ser usado por outro ocupante"}. {"That nickname is registered by another person","O alcume xa está rexistrado por outra persoa"}. {"The CAPTCHA is valid.","O CAPTCHA é válido."}. {"The CAPTCHA verification has failed","A verificación de CAPTCHA fallou"}. {"The collections with which a node is affiliated","As coleccións coas que un nodo está afiliado"}. {"The feature requested is not supported by the conference","A sala de conferencias non admite a función solicitada"}. {"The password contains unacceptable characters","O contrasinal contén caracteres inaceptables"}. {"The password is too weak","O contrasinal é demasiado débil"}. {"the password is","a contrasinal é"}. {"The query is only allowed from local users","A solicitude só se permite para usuarios locais"}. {"The query must not contain <item/> elements","A solicitude non debe conter elementos <item/>"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","A estroa DEBEN conter un elemento <active/>, un elemento <default/> ou un elemento <list/>"}. {"There was an error creating the account: ","Produciuse un erro ao crear a conta: "}. {"There was an error deleting the account: ","Produciuse un erro ao eliminar a conta: "}. {"This room is not anonymous","Sala non anónima"}. {"Thursday","Xoves"}. {"Time delay","Atraso temporal"}. {"Time","Data"}. {"To register, visit ~s","Para rexistrarse, visita ~s"}. {"Token TTL","Token TTL"}. {"Too many active bytestreams","Demasiados bytestreams activos"}. {"Too many CAPTCHA requests","Demasiadas solicitudes CAPTCHA"}. {"Too many <item/> elements","Demasiados elementos <item/>"}. {"Too many <list/> elements","Demasiados elementos <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Demasiados (~p) fallou autenticaciones desde esta dirección IP (~s). A dirección será desbloqueada as ~s UTC"}. {"Too many unacked stanzas","Demasiadas mensaxes sen recoñecer recibilos"}. {"Too many users in this conference","Demasiados usuarios nesta sala"}. {"To","Para"}. {"Total rooms","Salas totais"}. {"Traffic rate limit is exceeded","Hase exedido o límite de tráfico"}. {"Transactions Aborted:","Transaccións abortadas:"}. {"Transactions Committed:","Transaccións finalizadas:"}. {"Transactions Logged:","Transaccións rexistradas:"}. {"Transactions Restarted:","Transaccións reiniciadas:"}. {"Tuesday","Martes"}. {"Unable to generate a CAPTCHA","No se pudo generar un CAPTCHA"}. {"Unable to register route on existing local domain","Non se pode rexistrar a ruta no dominio local existente"}. {"Unauthorized","Non autorizado"}. {"Unexpected action","Acción inesperada"}. {"Unregister","Eliminar rexistro"}. {"Unsupported <index/> element","Elemento <index/> non soportado"}. {"Update message of the day (don't send)","Actualizar mensaxe do dia, pero non envialo"}. {"Update message of the day on all hosts (don't send)","Actualizar a mensaxe do día en todos os dominos (pero non envialo)"}. {"Update ~p","Actualizar ~p"}. {"Update plan","Plan de actualización"}. {"Update script","Script de actualización"}. {"Update","Actualizar"}. {"Uptime:","Tempo desde o inicio:"}. {"User already exists","O usuario xa existe"}. {"User JID","Jabber ID do usuario"}. {"User (jid)","Usuario (jid)"}. {"User Management","Administración de usuarios"}. {"User session not found","Sesión de usuario non atopada"}. {"User session terminated","Sesión de usuario completada"}. {"Username:","Nome de usuario:"}. {"Users are not allowed to register accounts so quickly","Os usuarios non están autorizados a rexistrar contas con tanta rapidez"}. {"Users Last Activity","Última actividade dos usuarios"}. {"Users","Usuarios"}. {"User","Usuario"}. {"Validate","Validar"}. {"Value 'get' of 'type' attribute is not allowed","O valor \"get\" do atributo 'type' non está permitido"}. {"Value of '~s' should be boolean","O valor de '~s' debería ser booleano"}. {"Value of '~s' should be datetime string","O valor de '~s' debería ser unha data"}. {"Value of '~s' should be integer","O valor de '~s' debería ser un enteiro"}. {"Value 'set' of 'type' attribute is not allowed","O valor \"set\" do atributo 'type' non está permitido"}. {"vCard User Search","vCard busqueda de usuario"}. {"Virtual Hosts","Hosts Virtuais"}. {"Visitors are not allowed to change their nicknames in this room","Os visitantes non teñen permitido cambiar os seus alcumes nesta sala"}. {"Visitors are not allowed to send messages to all occupants","Os visitantes non poden enviar mensaxes a todos os ocupantes"}. {"Visitor","Visitante"}. {"Voice request","Petición de voz"}. {"Voice requests are disabled in this conference","As peticións de voz están desactivadas nesta sala"}. {"Wednesday","Mércores"}. {"When to send the last published item","Cando enviar o último elemento publicado"}. {"Whether to allow subscriptions","Permitir subscripciones"}. {"You have been banned from this room","Fuches bloqueado nesta sala"}. {"You have joined too many conferences","Entrou en demasiadas salas de conferencia"}. {"You must fill in field \"Nickname\" in the form","Debes encher o campo \"Alcumo\" no formulario"}. {"You need a client that supports x:data and CAPTCHA to register","Necesitas un cliente con soporte de x:data e CAPTCHA para rexistrarche"}. {"You need a client that supports x:data to register the nickname","Necesitas un cliente con soporte de x:data para poder rexistrar o alcume"}. {"You need an x:data capable client to search","Necesitas un cliente con soporte de x:data para poder buscar"}. {"Your active privacy list has denied the routing of this stanza.","A súa lista de privacidade activa negou o encaminamiento desta estrofa."}. {"Your contact offline message queue is full. The message has been discarded.","A túa cola de mensaxes diferidas de contactos está chea. A mensaxe descartouse."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","As súas mensaxes a ~s encóntranse bloqueadas. Para desbloquear, visite ~s"}. {"You're not allowed to create nodes","Non tes permiso para crear nodos"}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/pl.msg���������������������������������������������������������������������0000644�0002322�0002322�00000070203�14154362354�016710� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," zmienił temat na: "}. {"A friendly name for the node","Przyjazna nazwa węzła"}. {"A password is required to enter this room","Aby wejść do pokoju wymagane jest hasło"}. {"Accept","Zaakceptuj"}. {"Access denied by service policy","Dostęp zabroniony zgodnie z zasadami usługi"}. {"Action on user","Wykonaj na użytkowniku"}. {"Add Jabber ID","Dodaj Jabber ID"}. {"Add New","Dodaj nowe"}. {"Add User","Dodaj użytkownika"}. {"Administration of ","Zarządzanie "}. {"Administration","Administracja"}. {"Administrator privileges required","Wymagane uprawnienia administratora"}. {"All activity","Cała aktywność"}. {"All Users","Wszyscy użytkownicy"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Pozwól temu Jabber ID na zapisanie się do tego węzła PubSub"}. {"Allow users to change the subject","Pozwól użytkownikom zmieniać temat"}. {"Allow users to query other users","Pozwól użytkownikom pobierać informacje o innych użytkownikach"}. {"Allow users to send invites","Pozwól użytkownikom wysyłać zaproszenia"}. {"Allow users to send private messages","Pozwól użytkownikom wysyłać prywatne wiadomości"}. {"Allow visitors to change nickname","Pozwól uczestnikom na zmianę nicka"}. {"Allow visitors to send private messages to","Pozwól użytkownikom wysyłać prywatne wiadomości"}. {"Allow visitors to send status text in presence updates","Pozwól uczestnikom na wysyłanie statusów opisowych"}. {"Allow visitors to send voice requests","Pozwól użytkownikom wysyłać zaproszenia"}. {"Announcements","Powiadomienia"}. {"April","Kwiecień"}. {"August","Sierpień"}. {"Automatic node creation is not enabled","Automatyczne tworzenie węzłów nie zostało włączone"}. {"Backup Management","Zarządzanie kopiami zapasowymi"}. {"Backup of ~p","Kopia zapasowa ~p"}. {"Backup to File at ","Zapisz kopię w pliku na "}. {"Backup","Kopia zapasowa"}. {"Bad format","Błędny format"}. {"Birthday","Data urodzenia"}. {"Both the username and the resource are required","Wymagana jest zarówno nazwa użytkownika jak i zasób"}. {"Bytestream already activated","Strumień danych został już aktywowany"}. {"Cannot remove active list","Nie można usunąć aktywnej listy"}. {"Cannot remove default list","Nie można usunąć domyślnej listy"}. {"CAPTCHA web page","Strona internetowa CAPTCHA"}. {"Change Password","Zmień hasło"}. {"Change User Password","Zmień hasło użytkownika"}. {"Changing password is not allowed","Zmiana hasła jest niedopuszczalna"}. {"Changing role/affiliation is not allowed","Zmiana roli jest niedopuszczalna"}. {"Characters not allowed:","Te znaki są niedozwolone:"}. {"Chatroom configuration modified","Konfiguracja pokoju zmodyfikowana"}. {"Chatroom is created","Pokój został stworzony"}. {"Chatroom is destroyed","Pokój został usunięty"}. {"Chatroom is started","Pokój został uruchomiony"}. {"Chatroom is stopped","Pokój został zatrzymany"}. {"Chatrooms","Pokoje rozmów"}. {"Choose a username and password to register with this server","Wybierz nazwę użytkownika i hasło aby zarejestrować się na tym serwerze"}. {"Choose storage type of tables","Wybierz typ bazy dla tablel"}. {"Choose whether to approve this entity's subscription.","Wybierz, czy akceptować subskrypcję tej jednostki"}. {"City","Miasto"}. {"Commands","Polecenia"}. {"Conference room does not exist","Pokój konferencyjny nie istnieje"}. {"Configuration of room ~s","Konfiguracja pokoju ~s"}. {"Configuration","Konfiguracja"}. {"Connected Resources:","Zasoby zalogowane:"}. {"Country","Państwo"}. {"CPU Time:","Czas CPU:"}. {"Database failure","Błąd bazy danych"}. {"Database Tables at ~p","Tabele bazy na ~p"}. {"Database Tables Configuration at ","Konfiguracja tabel bazy na "}. {"Database","Baza danych"}. {"December","Grudzień"}. {"Default users as participants","Domyślni użytkownicy jako uczestnicy"}. {"Delete message of the day on all hosts","Usuń wiadomość dnia ze wszystkich hostów"}. {"Delete message of the day","Usuń wiadomość dnia"}. {"Delete Selected","Usuń zaznaczone"}. {"Delete User","Usuń użytkownika"}. {"Deliver event notifications","Dostarczaj powiadomienia o zdarzeniach"}. {"Deliver payloads with event notifications","Dostarczaj zawartość publikacji wraz z powiadomieniami o zdarzeniach"}. {"Description:","Opis:"}. {"Disc only copy","Kopia tylko na dysku"}. {"Dump Backup to Text File at ","Zapisz kopię zapasową w pliku tekstowym na "}. {"Dump to Text File","Wykonaj kopie do pliku tekstowego"}. {"Edit Properties","Edytuj właściwości"}. {"Either approve or decline the voice request.","Zatwierdź lub odrzuć żądanie głosowe."}. {"ejabberd MUC module","Moduł MUC"}. {"ejabberd Multicast service","Serwis multicast ejabbera"}. {"ejabberd Publish-Subscribe module","Moduł Publish-Subscribe"}. {"ejabberd SOCKS5 Bytestreams module","Moduł SOCKS5 Bytestreams"}. {"ejabberd vCard module","Moduł vCard ejabberd"}. {"ejabberd Web Admin","ejabberd: Panel Administracyjny"}. {"Elements","Elementy"}. {"Email","Email"}. {"Enable logging","Włącz logowanie"}. {"Enable message archiving","Włącz archiwizowanie rozmów"}. {"Enabling push without 'node' attribute is not supported","Aktywacja 'push' bez węzła jest nie dostępna"}. {"End User Session","Zakończ sesję uzytkownika"}. {"Enter nickname you want to register","Wprowadz nazwę użytkownika którego chcesz zarejestrować"}. {"Enter path to backup file","Wprowadź scieżkę do pliku kopii zapasowej"}. {"Enter path to jabberd14 spool dir","Wprowadź ścieżkę do roboczego katalogu serwera jabberd14"}. {"Enter path to jabberd14 spool file","Wprowadź ścieżkę do roboczego pliku serwera jabberd14"}. {"Enter path to text file","Wprowadź scieżkę do pliku tekstowego"}. {"Enter the text you see","Przepisz tekst z obrazka"}. {"Error","Błąd"}. {"Exclude Jabber IDs from CAPTCHA challenge","Pomiń Jabber ID z żądania CAPTCHA"}. {"Export all tables as SQL queries to a file:","Wyeksportuj wszystkie tabele jako zapytania SQL do pliku:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Eksportuj dane wszystkich użytkowników serwera do plików w formacie PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Eksportuj dane użytkowników z hosta do plików w formacie PIEFXIS (XEP-0227):"}. {"External component failure","Błąd zewnętrznego komponentu"}. {"External component timeout","Upłynął limit czasu zewnętrznego komponentu"}. {"Failed to activate bytestream","Nie udało się aktywować strumienia danych"}. {"Failed to extract JID from your voice request approval","Nie udało się wydobyć JID-u z twojego żądania"}. {"Failed to map delegated namespace to external component","Nie udało się znaleźć zewnętrznego komponentu na podstawie nazwy"}. {"Failed to parse HTTP response","Nie udało się zanalizować odpowiedzi HTTP"}. {"Failed to process option '~s'","Nie udało się przetworzyć opcji '~s'"}. {"Family Name","Nazwisko"}. {"February","Luty"}. {"File larger than ~w bytes","Plik jest większy niż ~w bajtów"}. {"Friday","Piątek"}. {"From","Od"}. {"Full Name","Pełna nazwa"}. {"Get Number of Online Users","Pokaż liczbę zalogowanych użytkowników"}. {"Get Number of Registered Users","Pokaż liczbę zarejestrowanych użytkowników"}. {"Get User Last Login Time","Pokaż czas ostatniego zalogowania uzytkownika"}. {"Get User Password","Pobierz hasło użytkownika"}. {"Get User Statistics","Pobierz statystyki użytkownika"}. {"Given Name","Imię"}. {"Grant voice to this person?","Udzielić głosu tej osobie?"}. {"Group","Grupa"}. {"Groups","Grupy"}. {"has been banned","został wykluczony"}. {"has been kicked because of a system shutdown","został wyrzucony z powodu wyłączenia systemu"}. {"has been kicked because of an affiliation change","został wyrzucony z powodu zmiany przynależności"}. {"has been kicked because the room has been changed to members-only","został wyrzucony z powodu zmiany pokoju na \"Tylko dla Członków\""}. {"has been kicked","został wyrzucony"}. {"Host unknown","Nieznany host"}. {"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Jeśli nie widzisz obrazka CAPTCHA, odwiedź stronę internetową."}. {"Import Directory","Importuj katalog"}. {"Import File","Importuj plik"}. {"Import user data from jabberd14 spool file:","Importuj dane użytkownika z pliku roboczego serwera jabberd14:"}. {"Import User from File at ","Importuj użytkownika z pliku na "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importuj dane użytkowników z pliku w formacie PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importuj użytkowników z katalogu roboczego serwera jabberd14:"}. {"Import Users from Dir at ","Importuj użytkowników z katalogu na "}. {"Import Users From jabberd14 Spool Files","Importuj użytkowników z plików roboczych serwera jabberd14"}. {"Improper domain part of 'from' attribute","Nieprawidłowa domena atrybutu 'from'"}. {"Improper message type","Nieprawidłowy typ wiadomości"}. {"Incoming s2s Connections:","Przychodzące połączenia s2s:"}. {"Incorrect CAPTCHA submit","Nieprawidłowa odpowiedz dla CAPTCHA"}. {"Incorrect data form","Nieprawidłowe dane w formatce"}. {"Incorrect password","Nieprawidłowe hasło"}. {"Incorrect value of 'action' attribute","Nieprawidłowe dane atrybutu 'action'"}. {"Incorrect value of 'action' in data form","Nieprawidłowe dane atrybutu 'action'"}. {"Incorrect value of 'path' in data form","Nieprawidłowe dane atrybutu 'path'"}. {"Insufficient privilege","Niewystarczające uprawnienia"}. {"Invalid 'from' attribute in forwarded message","Nieprawidłowy atrybut 'from' w przesyłanej dalej wiadomości"}. {"Invitations are not allowed in this conference","Zaproszenia są wyłączone w tym pokoju"}. {"IP addresses","Adresy IP"}. {"is now known as","jest teraz znany jako"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Użytkownik nie może wysyłać wiadomości o błędach do pokoju. Użytkownik (~s) wysłał błąd (~s) i został wyrzucony z pokoju"}. {"It is not allowed to send private messages of type \"groupchat\"","Nie można wysyłać prywatnych wiadomości typu \"groupchat\""}. {"It is not allowed to send private messages to the conference","Nie wolno wysyłac prywatnych wiadomości na konferencję"}. {"It is not allowed to send private messages","Wysyłanie prywatnych wiadomości jest zabronione"}. {"Jabber ID","Jabber ID"}. {"January","Styczeń"}. {"joins the room","dołącza do pokoju"}. {"July","Lipiec"}. {"June","Czerwiec"}. {"Last Activity","Ostatnia aktywność"}. {"Last login","Ostatnie logowanie"}. {"Last month","Miniony miesiąc"}. {"Last year","Miniony rok"}. {"leaves the room","opuszcza pokój"}. {"List of rooms","Lista pokoi"}. {"Low level update script","Skrypt aktualizacji niskiego poziomu"}. {"Make participants list public","Upublicznij listę uczestników"}. {"Make room CAPTCHA protected","Pokój zabezpieczony captchą"}. {"Make room members-only","Pokój tylko dla członków"}. {"Make room moderated","Pokój moderowany"}. {"Make room password protected","Pokój zabezpieczony hasłem"}. {"Make room persistent","Utwórz pokój na stałe"}. {"Make room public searchable","Pozwól wyszukiwać pokój"}. {"Malformed username","Nieprawidłowa nazwa użytkownika"}. {"March","Marzec"}. {"Max # of items to persist","Maksymalna liczba przechowywanych przedmiotów"}. {"Max payload size in bytes","Maksymalna wielkość powiadomienia w bajtach"}. {"Maximum Number of Occupants","Maksymalna liczba uczestników"}. {"May","Maj"}. {"Members:","Członkowie:"}. {"Membership is required to enter this room","Musisz być na liście członków tego pokoju aby do niego wejść"}. {"Memory","Pamięć"}. {"Message body","Treść wiadomości"}. {"Message not found in forwarded payload","Nie znaleziona wiadomości w przesyłanych dalej danych"}. {"Middle Name","Drugie imię"}. {"Minimum interval between voice requests (in seconds)","Minimalny odstęp między żądaniami głosowymi (w sekundach)"}. {"Moderator privileges required","Wymagane uprawnienia moderatora"}. {"Moderator","Moderatorzy"}. {"Modified modules","Zmodyfikowane moduły"}. {"Module failed to handle the query","Moduł nie był wstanie przetworzyć zapytania"}. {"Monday","Poniedziałek"}. {"Multicast","Multicast"}. {"Multi-User Chat","Wieloosobowa rozmowa"}. {"Name","Imię"}. {"Name:","Nazwa:"}. {"Neither 'jid' nor 'nick' attribute found","Brak zarówno atrybutu 'jid' jak i 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Brak zarówno atrybutu 'role' jak i 'affiliation'"}. {"Never","Nigdy"}. {"New Password:","Nowe hasło:"}. {"Nickname Registration at ","Rejestracja nazwy użytkownika na "}. {"Nickname","Nazwa użytkownika"}. {"No 'affiliation' attribute found","Brak wartości dla 'access'"}. {"No available resource found","Brak dostępnych zasobów"}. {"No body provided for announce message","Brak treści powiadomienia"}. {"No data form found","Brak danych dla formatki"}. {"No Data","Brak danych"}. {"No features available","Brak dostępnych funkcji"}. {"No hook has processed this command","Żadna funkcja nie przetworzyła tej komendy"}. {"No info about last activity found","Nie znaleziono informacji o ostatniej aktywności"}. {"No 'item' element found","Brak wartości dla 'item'"}. {"No items found in this query","Nie znaleziono żadnych pozycji w tym zapytaniu"}. {"No limit","Bez limitu"}. {"No module is handling this query","Żaden moduł nie obsługuje tego zapytania"}. {"No node specified","Nie podano węzła"}. {"No 'password' found in data form","Brak wartości dla 'password'"}. {"No 'password' found in this query","Brak wartości dla 'password'"}. {"No 'path' found in data form","Brak wartości dla 'path'"}. {"No pending subscriptions found","Nie ma żadnych oczekujących subskrypcji"}. {"No privacy list with this name found","Nie znaleziona żadnych list prywatności z tą nazwą"}. {"No private data found in this query","Nie znaleziono danych prywatnych w tym zapytaniu"}. {"No running node found","Brak uruchomionych węzłów"}. {"No services available","Usługa nie jest dostępna"}. {"No statistics found for this item","Nie znaleziono statystyk dla tego elementu"}. {"No 'to' attribute found in the invitation","Brak wartości dla 'to' w zaproszeniu"}. {"Node already exists","Węzeł już istnieje"}. {"Node ID","ID węzła"}. {"Node index not found","Indeks węzła już istnieje"}. {"Node not found","Węzeł nie został znaleziony"}. {"Node ~p","Węzeł ~p"}. {"Nodeprep has failed","Weryfikacja nazwy nie powiodła się"}. {"Nodes","Węzły"}. {"None","Brak"}. {"Not Found","Nie znaleziono"}. {"Not subscribed","Nie zasubskrybowano"}. {"Notify subscribers when items are removed from the node","Informuj subskrybentów o usunięciu elementów węzła"}. {"Notify subscribers when the node configuration changes","Informuj subskrybentów o zmianach konfiguracji węzła"}. {"Notify subscribers when the node is deleted","Informuj subskrybentów o usunięciu węzła"}. {"November","Listopad"}. {"Number of occupants","Liczba uczestników"}. {"Number of online users","Liczba zalogowanych użytkowników"}. {"Number of registered users","Liczba zarejestrowanych użytkowników"}. {"October","Październik"}. {"Offline Messages","Wiadomości offline"}. {"Offline Messages:","Wiadomości offline:"}. {"OK","OK"}. {"Old Password:","Stare hasło:"}. {"Online Users","Użytkownicy zalogowani"}. {"Online Users:","Użytkownicy zalogowani:"}. {"Online","Dostępny"}. {"Only deliver notifications to available users","Dostarczaj powiadomienia tylko dostępnym użytkownikom"}. {"Only <enable/> or <disable/> tags are allowed","Dozwolone są wyłącznie elementy <enable/> lub <disable/>"}. {"Only <list/> element is allowed in this query","Wyłącznie elementy <item/> są dozwolone w tym zapytaniu"}. {"Only members may query archives of this room","Tylko moderatorzy mogą przeglądać archiwa tego pokoju"}. {"Only moderators and participants are allowed to change the subject in this room","Tylko moderatorzy i uczestnicy mogą zmienić temat tego pokoju"}. {"Only moderators are allowed to change the subject in this room","Tylko moderatorzy mogą zmienić temat tego pokoju"}. {"Only moderators can approve voice requests","Tylko moderatorzy mogą zatwierdzać żądania głosowe"}. {"Only occupants are allowed to send messages to the conference","Tylko uczestnicy mogą wysyłać wiadomości na konferencję"}. {"Only occupants are allowed to send queries to the conference","Tylko uczestnicy mogą wysyłać zapytania do konferencji"}. {"Only service administrators are allowed to send service messages","Tylko administratorzy mogą wysyłać wiadomości"}. {"Organization Name","Nazwa organizacji"}. {"Organization Unit","Dział"}. {"Outgoing s2s Connections","Wychodzące połączenia s2s"}. {"Outgoing s2s Connections:","Wychodzące połączenia s2s:"}. {"Owner privileges required","Wymagane uprawnienia właściciela"}. {"Packet","Pakiet"}. {"Participant","Uczestnicy"}. {"Password Verification","Weryfikacja hasła"}. {"Password Verification:","Weryfikacja hasła:"}. {"Password","Hasło"}. {"Password:","Hasło:"}. {"Path to Dir","Ścieżka do katalogu"}. {"Path to File","Scieżka do pliku"}. {"Pending","Oczekuje"}. {"Period: ","Przedział czasu: "}. {"Persist items to storage","Przechowuj na stałe dane PubSub"}. {"Ping query is incorrect","Żądanie 'ping' nie jest prawidłowe"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Te opcje kopii zapasowych dotyczą tylko wbudowanej bazy danych typu Mnesia. Jeśli korzystasz z modułu ODBC, musisz wykonać kopie bazy we własnym zakresie."}. {"Please, wait for a while before sending new voice request","Proszę poczekać chwile, zanim wyślesz nowe żądanie głosowe"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Prawdziwe Jabber ID widoczne dla"}. {"private, ","prywatny, "}. {"Publish-Subscribe","PubSub"}. {"PubSub subscriber request","Żądanie subskrybcji PubSub"}. {"Purge all items when the relevant publisher goes offline","Usuń wszystkie elementy w momencie kiedy publikujący rozłączy się"}. {"Queries to the conference members are not allowed in this room","Informacje o członkach konferencji nie są dostępne w tym pokoju"}. {"Query to another users is forbidden","Zapytanie do innych użytkowników nie są dozwolone"}. {"RAM and disc copy","Kopia na dysku i w pamięci RAM"}. {"RAM copy","Kopia w pamięci RAM"}. {"Really delete message of the day?","Na pewno usunąć wiadomość dnia?"}. {"Recipient is not in the conference room","Odbiorcy nie ma w pokoju"}. {"Registered Users","Użytkownicy zarejestrowani"}. {"Registered Users:","Użytkownicy zarejestrowani:"}. {"Register","Zarejestruj"}. {"Remote copy","Kopia zdalna"}. {"Remove All Offline Messages","Usuń wszystkie wiadomości typu 'Offline'"}. {"Remove User","Usuń użytkownika"}. {"Remove","Usuń"}. {"Replaced by new connection","Połączenie zostało zastąpione"}. {"Resources","Zasoby"}. {"Restart Service","Restart usługi"}. {"Restart","Uruchom ponownie"}. {"Restore Backup from File at ","Odtwórz bazę danych z kopii zapasowej na "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Odtwórz kopię binarną podczas następnego uruchomienia ejabberd (wymaga mniej zasobów):"}. {"Restore binary backup immediately:","Natychmiast odtwórz kopię binarną:"}. {"Restore plain text backup immediately:","Natychmiast odtwórz kopię z postaci tekstowej:"}. {"Restore","Przywróć z kopii"}. {"Roles for which Presence is Broadcasted","Role dla których wysyłane są statusy"}. {"Room Configuration","Konfiguracja pokoju"}. {"Room creation is denied by service policy","Zasady serwera zabraniają tworzyć nowe pokoje"}. {"Room description","Opis pokoju"}. {"Room Occupants","Lista uczestników"}. {"Room title","Tytuł pokoju"}. {"Roster groups allowed to subscribe","Grupy kontaktów uprawnione do subskrypcji"}. {"Roster size","Rozmiar listy kontaktów"}. {"RPC Call Error","Błąd żądania RPC"}. {"Running Nodes","Uruchomione węzły"}. {"Saturday","Sobota"}. {"Script check","Sprawdź skrypt"}. {"Search Results for ","Wyniki wyszukiwania dla "}. {"Search users in ","Wyszukaj użytkowników w "}. {"Send announcement to all online users on all hosts","Wyślij powiadomienie do wszystkich zalogowanych użytkowników na wszystkich hostach"}. {"Send announcement to all online users","Wyślij powiadomienie do wszystkich zalogowanych użytkowników"}. {"Send announcement to all users on all hosts","Wyślij powiadomienie do wszystkich użytkowników na wszystkich hostach"}. {"Send announcement to all users","Wyślij powiadomienie do wszystkich użytkowników"}. {"September","Wrzesień"}. {"Server:","Serwer:"}. {"Set message of the day and send to online users","Wyślij wiadomość dnia do wszystkich zalogowanych użytkowników"}. {"Set message of the day on all hosts and send to online users","Ustaw wiadomość dnia dla wszystkich hostów i wyślij do zalogowanych uzytkowników"}. {"Shared Roster Groups","Wspólne grupy kontaktów"}. {"Show Integral Table","Pokaż tabelę całkowitą"}. {"Show Ordinary Table","Pokaż zwykłą tabelę"}. {"Shut Down Service","Wyłącz usługę"}. {"Specify the access model","Określ model dostępu"}. {"Specify the event message type","Określ typ wiadomości"}. {"Specify the publisher model","Określ model publikującego"}. {"Statistics of ~p","Statystyki ~p"}. {"Statistics","Statystyki"}. {"Stopped Nodes","Zatrzymane węzły"}. {"Stop","Zatrzymaj"}. {"Storage Type","Typ bazy"}. {"Store binary backup:","Zachowaj kopię binarną:"}. {"Store plain text backup:","Zachowaj kopię w postaci tekstowej:"}. {"Subject","Temat"}. {"Submitted","Wprowadzone"}. {"Submit","Wyślij"}. {"Subscriber Address","Adres subskrybenta"}. {"Subscriptions are not allowed","Subskrypcje nie są dozwolone"}. {"Subscription","Subskrypcja"}. {"Sunday","Niedziela"}. {"That nickname is already in use by another occupant","Ta nazwa użytkownika jest używana przez kogoś innego"}. {"That nickname is registered by another person","Ta nazwa użytkownika jest już zarejestrowana przez inną osobę"}. {"The CAPTCHA is valid.","Captcha jest poprawna."}. {"The CAPTCHA verification has failed","Weryfikacja CAPTCHA nie powiodła się"}. {"The collections with which a node is affiliated","Grupy, do których należy węzeł"}. {"The feature requested is not supported by the conference","Żądana czynność nie jest obsługiwana przez konferencje"}. {"The password contains unacceptable characters","Hasło zawiera niedopuszczalne znaki"}. {"The password is too weak","Hasło nie jest wystarczająco trudne"}. {"the password is","hasło to:"}. {"The query is only allowed from local users","To żądanie jest dopuszczalne wyłącznie dla lokalnych użytkowników"}. {"The query must not contain <item/> elements","Żądanie nie może zawierać elementów <item/>"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Żądanie może zawierać wyłącznie jeden z elementów <active/>, <default/> lub <list/>"}. {"There was an error creating the account: ","Wystąpił błąd podczas tworzenia konta: "}. {"There was an error deleting the account: ","Podczas usuwania konta wystąpił błąd: "}. {"This room is not anonymous","Ten pokój nie jest anonimowy"}. {"Thursday","Czwartek"}. {"Time delay","Opóźnienie"}. {"Time","Czas"}. {"To register, visit ~s","Żeby się zarejestrować odwiedź ~s"}. {"To","Do"}. {"Token TTL","Limit czasu tokenu"}. {"Too many active bytestreams","Zbyt wiele strumieni danych"}. {"Too many CAPTCHA requests","Za dużo żądań CAPTCHA"}. {"Too many <item/> elements","Zbyt wiele elementów <item/>"}. {"Too many <list/> elements","Zbyt wiele elementów <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Zbyt wiele (~p) nieudanych prób logowanie z tego adresu IP (~s). Ten adres zostanie odblokowany o ~s UTC"}. {"Too many unacked stanzas","Zbyt wiele niepotwierdzonych pakietów"}. {"Too many users in this conference","Zbyt wielu użytkowników konferencji"}. {"Total rooms","Wszystkich pokoi"}. {"Traffic rate limit is exceeded","Limit transferu przekroczony"}. {"Transactions Aborted:","Transakcje anulowane:"}. {"Transactions Committed:","Transakcje zakończone:"}. {"Transactions Logged:","Transakcje zalogowane:"}. {"Transactions Restarted:","Transakcje uruchomione ponownie:"}. {"Tuesday","Wtorek"}. {"Unable to generate a CAPTCHA","Nie można wygenerować CAPTCHA"}. {"Unable to register route on existing local domain","Nie można zarejestrować trasy dla lokalnej domeny"}. {"Unauthorized","Nie autoryzowano"}. {"Unexpected action","Nieoczekiwana akcja"}. {"Unregister","Wyrejestruj"}. {"Unsupported <index/> element","Nieobsługiwany element <index/>"}. {"Update message of the day (don't send)","Aktualizuj wiadomość dnia (bez wysyłania)"}. {"Update message of the day on all hosts (don't send)","Aktualizuj wiadomość dnia na wszystkich hostach (bez wysyłania)"}. {"Update plan","Plan aktualizacji"}. {"Update ~p","Uaktualnij ~p"}. {"Update script","Skrypt aktualizacji"}. {"Update","Aktualizuj"}. {"Uptime:","Czas pracy:"}. {"User already exists","Użytkownik już istnieje"}. {"User JID","Użytkownik "}. {"User (jid)","Użytkownik (jid)"}. {"User Management","Zarządzanie użytkownikami"}. {"User session not found","Sesja użytkownika nie została znaleziona"}. {"User session terminated","Sesja użytkownika została zakończona"}. {"Username:","Nazwa użytkownika:"}. {"Users are not allowed to register accounts so quickly","Użytkowncy nie mogą tak szybko rejestrować nowych kont"}. {"Users Last Activity","Ostatnia aktywność użytkowników"}. {"Users","Użytkownicy"}. {"User","Użytkownik"}. {"Validate","Potwierdź"}. {"Value 'get' of 'type' attribute is not allowed","Wartość 'get' dla atrybutu 'type' jest niedozwolona"}. {"Value of '~s' should be boolean","Wartość '~s' powinna być typu logicznego"}. {"Value of '~s' should be datetime string","Wartość '~s' powinna być typu daty"}. {"Value of '~s' should be integer","Wartość '~s' powinna być liczbą"}. {"Value 'set' of 'type' attribute is not allowed","Wartość 'set' dla atrybutu 'type' jest niedozwolona"}. {"vCard User Search","Wyszukiwanie vCard użytkowników"}. {"Virtual Hosts","Wirtualne Hosty"}. {"Visitor","Odwiedzający"}. {"Visitors are not allowed to change their nicknames in this room","Uczestnicy tego pokoju nie mogą zmieniać swoich nicków"}. {"Visitors are not allowed to send messages to all occupants","Odwiedzający nie mogą wysyłać wiadomości do wszystkich obecnych"}. {"Voice requests are disabled in this conference","Głosowe żądania są wyłączone w tym pokoju"}. {"Voice request","Żądanie głosowe"}. {"Wednesday","Środa"}. {"When to send the last published item","Kiedy wysłać ostatnio opublikowaną rzecz"}. {"Whether to allow subscriptions","Czy pozwolić na subskrypcje"}. {"You have been banned from this room","Zostałeś wykluczony z tego pokoju"}. {"You have joined too many conferences","Dołączyłeś do zbyt wielu konferencji"}. {"You must fill in field \"Nickname\" in the form","Musisz wypełnić pole \"Nazwa użytkownika\" w formularzu"}. {"You need a client that supports x:data and CAPTCHA to register","Potrzebujesz klienta obsługującego x:data aby zarejestrować nick"}. {"You need a client that supports x:data to register the nickname","Potrzebujesz klienta obsługującego x:data aby zarejestrować nick"}. {"You need an x:data capable client to search","Potrzebujesz klienta obsługującego x:data aby wyszukiwać"}. {"Your active privacy list has denied the routing of this stanza.","Aktualna lista prywatności zabrania przesyłania tej stanzy."}. {"Your contact offline message queue is full. The message has been discarded.","Kolejka wiadomości offline adresata jest pełna. Wiadomość została odrzucona."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Twoje wiadomości do ~s są blokowane. Aby je odblokować, odwiedź ~s"}. {"You're not allowed to create nodes","Nie masz uprawnień do tworzenia węzłów"}. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/nl.msg���������������������������������������������������������������������0000644�0002322�0002322�00000051677�14154362354�016724� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," veranderde het onderwerp in: "}. {"A friendly name for the node","Bijnaam voor deze knoop"}. {"A password is required to enter this room","U hebt een wachtwoord nodig om deze chatruimte te kunnen betreden"}. {"Access denied by service policy","De toegang werd geweigerd door het beleid van deze dienst"}. {"Action on user","Actie op gebruiker"}. {"Add Jabber ID","Jabber ID toevoegen"}. {"Add New","Toevoegen"}. {"Add User","Gebruiker toevoegen"}. {"Administration of ","Beheer van "}. {"Administration","Beheer"}. {"Administrator privileges required","U hebt beheerdersprivileges nodig"}. {"All activity","Alle activiteit"}. {"All Users","Alle gebruikers"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Deze gebruiker toestaan te abonneren op deze pubsub node?"}. {"Allow users to change the subject","Sta gebruikers toe het onderwerp te veranderen"}. {"Allow users to query other users","Gebruikers mogen naar andere gebruikers verzoeken verzenden"}. {"Allow users to send invites","Gebruikers mogen uitnodigingen verzenden"}. {"Allow users to send private messages","Gebruikers mogen privéberichten verzenden"}. {"Allow visitors to change nickname","Sta bezoekers toe hun naam te veranderen"}. {"Allow visitors to send private messages to","Gebruikers mogen privéberichten verzenden aan"}. {"Allow visitors to send status text in presence updates","Sta bezoekers toe hun statusbericht in te stellen"}. {"Allow visitors to send voice requests","Gebruikers mogen stemaanvragen verzenden"}. {"Announcements","Mededelingen"}. {"April","April"}. {"August","Augustus"}. {"Backup Management","Backup"}. {"Backup of ~p","Backup maken van ~p"}. {"Backup to File at ","Binaire backup maken op "}. {"Backup","Backup"}. {"Bad format","Verkeerd formaat"}. {"Birthday","Geboortedatum"}. {"CAPTCHA web page","CAPTCHA webpagina."}. {"Change Password","Wachtwoord wijzigen"}. {"Change User Password","Verander Gebruikerswachtwoord"}. {"Characters not allowed:","Niet-toegestane karakters:"}. {"Chatroom configuration modified","De instellingen van de chatruimte werden veranderd"}. {"Chatroom is created","Gespreksruimte gecreëerd"}. {"Chatroom is destroyed","Gespreksruimte vernietigd"}. {"Chatroom is started","Gespreksruimte gestart"}. {"Chatroom is stopped","Gespreksruimte gestopt"}. {"Chatrooms","Groepsgesprekken"}. {"Choose a username and password to register with this server","Kies een gebruikersnaam en een wachtwoord om u te registreren op deze server"}. {"Choose storage type of tables","Opslagmethode voor tabellen kiezen"}. {"Choose whether to approve this entity's subscription.","Beslis of dit verzoek tot abonneren zal worden goedgekeurd"}. {"City","Plaats"}. {"Commands","Commando's"}. {"Conference room does not exist","De chatruimte bestaat niet"}. {"Configuration of room ~s","Instellingen van chatruimte ~s"}. {"Configuration","Instellingen"}. {"Connected Resources:","Verbonden bronnen:"}. {"Country","Land"}. {"CPU Time:","Processortijd:"}. {"Database Tables at ~p","Databasetabellen van ~p"}. {"Database Tables Configuration at ","Instellingen van databasetabellen op "}. {"Database","Database"}. {"December","December"}. {"Default users as participants","Gebruikers standaard instellen als deelnemers"}. {"Delete message of the day on all hosts","Verwijder bericht-van-de-dag op alle hosts"}. {"Delete message of the day","Bericht van de dag verwijderen"}. {"Delete Selected","Geselecteerde verwijderen"}. {"Delete User","Verwijder Gebruiker"}. {"Deliver event notifications","Gebeurtenisbevestigingen Sturen"}. {"Deliver payloads with event notifications","Berichten bezorgen samen met gebeurtenisnotificaties"}. {"Description:","Beschrijving:"}. {"Disc only copy","Harde schijf"}. {"Dump Backup to Text File at ","Backup naar een tekstbestand schrijven op "}. {"Dump to Text File","Backup naar een tekstbestand schrijven"}. {"Edit Properties","Eigenschappen bewerken"}. {"Either approve or decline the voice request.","Keur stemaanvraag goed of af."}. {"ejabberd MUC module","ejabberd's MUC module"}. {"ejabberd Multicast service","ejabberd Multicast service"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe module"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. {"ejabberd vCard module","ejabberd's vCard-module"}. {"ejabberd Web Admin","ejabberd Webbeheer"}. {"Elements","Elementen"}. {"Email","E-mail"}. {"Enable logging","Logs aanzetten"}. {"Enable message archiving","Zet bericht-archivering aan"}. {"End User Session","Verwijder Gebruikers-sessie"}. {"Enter nickname you want to register","Voer de bijnaam in die u wilt registreren"}. {"Enter path to backup file","Voer pad naar backupbestand in"}. {"Enter path to jabberd14 spool dir","Voer pad naar jabberd14-spool-directory in"}. {"Enter path to jabberd14 spool file","Voer pad naar jabberd14-spool-bestand in"}. {"Enter path to text file","Voer pad naar backupbestand in"}. {"Enter the text you see","Voer de getoonde tekst in"}. {"Error","Fout"}. {"Exclude Jabber IDs from CAPTCHA challenge","Geen CAPTCHA test voor Jabber IDs"}. {"Export all tables as SQL queries to a file:","Exporteer alle tabellen als SQL-queries naar een bestand:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exporteer data van alle gebruikers in de server naar PIEFXIS-bestanden (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exporteer data van alle gebruikers van een host naar PIEXFIS-bestanden (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Er kon geen JID worden ontleend uit deze stemaanvraag"}. {"Family Name","Achternaam"}. {"February","Februari"}. {"Friday","Vrijdag"}. {"From","Van"}. {"Full Name","Volledige naam"}. {"Get Number of Online Users","Aantal Aanwezige Gebruikers Opvragen"}. {"Get Number of Registered Users","Aantal Geregistreerde Gebruikers Opvragen"}. {"Get User Last Login Time","Tijd van Laatste Aanmelding Opvragen"}. {"Get User Password","Gebruikerswachtwoord Opvragen"}. {"Get User Statistics","Gebruikers-statistieken Opvragen"}. {"Grant voice to this person?","Stemaanvraag honoreren voor deze persoon?"}. {"Group","Groep"}. {"Groups","Groepen"}. {"has been banned","is verbannen"}. {"has been kicked because of a system shutdown","is weggestuurd omdat het systeem gestopt wordt"}. {"has been kicked because of an affiliation change","is weggestuurd vanwege een affiliatieverandering"}. {"has been kicked because the room has been changed to members-only","is weggestuurd omdat de chatruimte vanaf heden alleen toegankelijk is voor leden"}. {"has been kicked","is weggestuurd"}. {"Host","Host"}. {"If you don't see the CAPTCHA image here, visit the web page.","Als U het CAPTCHA-plaatje niet ziet, bezoek dan de webpagina."}. {"Import Directory","Directory importeren"}. {"Import File","Bestand importeren"}. {"Import user data from jabberd14 spool file:","Importeer gebruikersdata via spool-bestanden van jabberd14"}. {"Import User from File at ","Importeer gebruiker via bestand op "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importeer gebruikersdata van een PIEFXIS-bestand (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importeer gebruikersdata via spool-bestanden van jabberd14"}. {"Import Users from Dir at ","Gebruikers importeren vanaf directory op "}. {"Import Users From jabberd14 Spool Files","Importeer gebruikers via spool-bestanden van jabberd14"}. {"Improper message type","Onjuist berichttype"}. {"Incorrect password","Foutief wachtwoord"}. {"IP addresses","IP-adres"}. {"is now known as","heet nu"}. {"It is not allowed to send private messages of type \"groupchat\"","Er mogen geen privéberichten van het type \"groupchat\" worden verzonden"}. {"It is not allowed to send private messages to the conference","Er mogen geen privéberichten naar de chatruimte worden verzonden"}. {"It is not allowed to send private messages","Het is niet toegestaan priveberichten te sturen"}. {"Jabber ID","Jabber ID"}. {"January","Januari"}. {"joins the room","betrad de chatruimte"}. {"July","Juli"}. {"June","Juni"}. {"Last Activity","Laatste activiteit"}. {"Last login","Laatste Aanmelding"}. {"Last month","Afgelopen maand"}. {"Last year","Afgelopen jaar"}. {"leaves the room","verliet de chatruimte"}. {"List of rooms","Lijst van groepsgesprekken"}. {"Low level update script","Lowlevel script voor de opwaardering"}. {"Make participants list public","Deelnemerslijst publiek maken"}. {"Make room CAPTCHA protected","Chatruimte beveiligen met een geautomatiseerde Turing test"}. {"Make room members-only","Chatruimte enkel toegankelijk maken voor leden"}. {"Make room moderated","Chatruimte gemodereerd maken"}. {"Make room password protected","Chatruimte beveiligen met een wachtwoord"}. {"Make room persistent","Chatruimte blijvend maken"}. {"Make room public searchable","Chatruimte doorzoekbaar maken"}. {"March","Maart"}. {"Max # of items to persist","Maximum aantal in het geheugen te bewaren items"}. {"Max payload size in bytes","Maximumgrootte van bericht in bytes"}. {"Maximum Number of Occupants","Maximum aantal aanwezigen"}. {"May","Mei"}. {"Members:","Groepsleden:"}. {"Membership is required to enter this room","U moet lid zijn om deze chatruimte te kunnen betreden"}. {"Memory","Geheugen"}. {"Message body","Bericht"}. {"Middle Name","Tussennaam"}. {"Minimum interval between voice requests (in seconds)","Minimale interval tussen stemaanvragen (in seconden)"}. {"Moderator privileges required","U hebt moderatorprivileges nodig"}. {"Modified modules","Gewijzigde modules"}. {"Monday","Maandag"}. {"Multicast","Multicast"}. {"Multi-User Chat","Groepschat"}. {"Name","Naam"}. {"Name:","Naam:"}. {"Never","Nooit"}. {"New Password:","Nieuw Wachtwoord:"}. {"Nickname Registration at ","Registratie van een bijnaam op "}. {"Nickname ~s does not exist in the room","De bijnaam ~s bestaat niet in deze chatruimte"}. {"Nickname","Bijnaam"}. {"No body provided for announce message","De mededeling bevat geen bericht"}. {"No Data","Geen gegevens"}. {"No limit","Geen limiet"}. {"Node ID","Node ID"}. {"Node not found","Node niet gevonden"}. {"Node ~p","Node ~p"}. {"Nodes","Nodes"}. {"None","Geen"}. {"Not Found","Niet gevonden"}. {"Notify subscribers when items are removed from the node","Abonnees informeren wanneer items verwijderd worden uit de node"}. {"Notify subscribers when the node configuration changes","Abonnees informeren wanneer de instellingen van de node veranderen"}. {"Notify subscribers when the node is deleted","Abonnees informeren wanneer de node verwijderd word"}. {"November","November"}. {"Number of occupants","Aantal aanwezigen"}. {"Number of online users","Aantal Aanwezige Gebruikers"}. {"Number of registered users","Aantal Geregistreerde Gebruikers"}. {"October","Oktober"}. {"Offline Messages","Offline berichten"}. {"Offline Messages:","Offline berichten:"}. {"OK","OK"}. {"Old Password:","Oud Wachtwoord:"}. {"Online Users","Online gebruikers"}. {"Online Users:","Online gebruikers:"}. {"Online","Online"}. {"Only deliver notifications to available users","Notificaties alleen verzenden naar online gebruikers"}. {"Only moderators and participants are allowed to change the subject in this room","Alleen moderators en deelnemers mogen het onderwerp van deze chatruimte veranderen"}. {"Only moderators are allowed to change the subject in this room","Alleen moderators mogen het onderwerp van deze chatruimte veranderen"}. {"Only moderators can approve voice requests","Alleen moderators kunnen stemaanvragen goedkeuren"}. {"Only occupants are allowed to send messages to the conference","Alleen aanwezigen mogen berichten naar de chatruimte verzenden"}. {"Only occupants are allowed to send queries to the conference","Alleen aanwezigen mogen verzoeken verzenden naar de chatruimte"}. {"Only service administrators are allowed to send service messages","Alleen beheerders van deze dienst mogen mededelingen verzenden naar alle chatruimtes"}. {"Organization Name","Organisatie"}. {"Organization Unit","Afdeling"}. {"Outgoing s2s Connections","Uitgaande s2s-verbindingen"}. {"Outgoing s2s Connections:","Uitgaande s2s-verbindingen:"}. {"Owner privileges required","U hebt eigenaarsprivileges nodig"}. {"Packet","Pakket"}. {"Password Verification","Wachtwoord Bevestiging"}. {"Password Verification:","Wachtwoord Bevestiging:"}. {"Password","Wachtwoord"}. {"Password:","Wachtwoord:"}. {"Path to Dir","Pad naar directory"}. {"Path to File","Pad naar bestand"}. {"Pending","Bezig"}. {"Period: ","Periode: "}. {"Persist items to storage","Items in het geheugen bewaren"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Merk op dat volgende opties enkel backups maken van de ingebouwde database Mnesia. Als U de ODBC module gebruikt dan moeten daarvan afzonderlijke backups gemaakt worden."}. {"Please, wait for a while before sending new voice request","Wacht s.v.p. met het maken van een nieuwe stemaanvraag."}. {"Pong","Pong"}. {"Present real Jabber IDs to","Jabber ID's kunnen achterhaald worden door"}. {"private, ","privé, "}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","PubSub abonnee verzoek"}. {"Purge all items when the relevant publisher goes offline","Verwijder alle items wanneer de gerelateerde publiceerder offline gaat"}. {"Queries to the conference members are not allowed in this room","Er mogen geen verzoeken verzenden worden naar deelnemers in deze chatruimte"}. {"RAM and disc copy","RAM en harde schijf"}. {"RAM copy","RAM"}. {"Really delete message of the day?","Wilt u het bericht van de dag verwijderen?"}. {"Recipient is not in the conference room","De ontvanger is niet in de chatruimte"}. {"Registered Users","Geregistreerde gebruikers"}. {"Registered Users:","Geregistreerde gebruikers:"}. {"Register","Registreer"}. {"Remote copy","Op andere nodes in de cluster"}. {"Remove All Offline Messages","Verwijder alle offline berichten"}. {"Remove User","Gebruiker verwijderen"}. {"Remove","Verwijderen"}. {"Replaced by new connection","Vervangen door een nieuwe verbinding"}. {"Resources","Bronnen"}. {"Restart Service","Herstart Service"}. {"Restart","Herstarten"}. {"Restore Backup from File at ","Binaire backup direct herstellen op "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Binaire backup herstellen na herstart van ejabberd (vereist minder geheugen):"}. {"Restore binary backup immediately:","Binaire backup direct herstellen:"}. {"Restore plain text backup immediately:","Backup in een tekstbestand direct herstellen:"}. {"Restore","Binaire backup direct herstellen"}. {"Room Configuration","Instellingen van de chatruimte"}. {"Room creation is denied by service policy","De aanmaak van de chatruimte is verhinderd door de instellingen van deze server"}. {"Room description","Beschrijving"}. {"Room Occupants","Aantal aanwezigen"}. {"Room title","Naam van de chatruimte"}. {"Roster groups allowed to subscribe","Contactlijst-groepen die mogen abonneren"}. {"Roster size","Contactlijst Groote"}. {"RPC Call Error","RPC-oproepfout"}. {"Running Nodes","Draaiende nodes"}. {"Saturday","Zaterdag"}. {"Script check","Controle van script"}. {"Search Results for ","Zoekresultaten voor "}. {"Search users in ","Gebruikers zoeken in "}. {"Send announcement to all online users on all hosts","Mededeling verzenden naar alle online gebruikers op alle virtuele hosts"}. {"Send announcement to all online users","Mededeling verzenden naar alle online gebruikers"}. {"Send announcement to all users on all hosts","Stuur aankondiging aan alle gebruikers op alle hosts"}. {"Send announcement to all users","Mededeling verzenden naar alle gebruikers"}. {"September","September"}. {"Server:","Server:"}. {"Set message of the day and send to online users","Bericht van de dag instellen en verzenden naar online gebruikers"}. {"Set message of the day on all hosts and send to online users","Stel bericht-van-de-dag in op alle hosts en stuur naar aanwezige gebruikers"}. {"Shared Roster Groups","Gedeelde rostergroepen"}. {"Show Integral Table","Volledige tabel laten zien"}. {"Show Ordinary Table","Deel van tabel laten zien"}. {"Shut Down Service","Stop Service"}. {"Specify the access model","Geef toegangsmodel"}. {"Specify the event message type","Geef type van eventbericht"}. {"Specify the publisher model","Publicatietype opgeven"}. {"Statistics of ~p","Statistieken van ~p"}. {"Statistics","Statistieken"}. {"Stopped Nodes","Gestopte nodes"}. {"Stop","Stoppen"}. {"Storage Type","Opslagmethode"}. {"Store binary backup:","Binaire backup maken:"}. {"Store plain text backup:","Backup naar een tekstbestand schrijven:"}. {"Subject","Onderwerp"}. {"Submitted","Verzonden"}. {"Submit","Verzenden"}. {"Subscriber Address","Abonnee Adres"}. {"Subscription","Inschrijving"}. {"Sunday","Zondag"}. {"That nickname is already in use by another occupant","Deze bijnaam is al in gebruik door een andere aanwezige"}. {"That nickname is registered by another person","Deze bijnaam is al geregistreerd door iemand anders"}. {"The CAPTCHA is valid.","De geautomatiseerde Turing-test is geslaagd."}. {"The CAPTCHA verification has failed","De CAPTCHA-verificatie is mislukt"}. {"The collections with which a node is affiliated","De collecties waar een node mee is gerelateerd"}. {"The password is too weak","Het wachtwoord is te zwak"}. {"the password is","het wachtwoord is"}. {"There was an error creating the account: ","Er was een fout bij het creeern van de account:"}. {"There was an error deleting the account: ","Er was een fout bij het verwijderen van de account."}. {"This room is not anonymous","Deze chatruimte is niet anoniem"}. {"Thursday","Donderdag"}. {"Time delay","Vertraging"}. {"Time","Tijd"}. {"To","Aan"}. {"Too many CAPTCHA requests","Te veel CAPTCHA-aanvragen"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Te veel (~p) mislukte authenticatie-pogingen van dit IP-adres (~s). Dit adres zal worden gedeblokkeerd om ~s UTC"}. {"Too many unacked stanzas","Te veel niet-bevestigde stanzas"}. {"Total rooms","Aantal groepsgesprekken"}. {"Traffic rate limit is exceeded","Dataverkeerslimiet overschreden"}. {"Transactions Aborted:","Afgebroken transacties:"}. {"Transactions Committed:","Bevestigde transacties:"}. {"Transactions Logged:","Gelogde transacties:"}. {"Transactions Restarted:","Herstarte transacties:"}. {"Tuesday","Dinsdag"}. {"Unable to generate a CAPTCHA","Het generen van een CAPTCHA is mislukt"}. {"Unauthorized","Niet geautoriseerd"}. {"Unregister","Opheffen"}. {"Update message of the day (don't send)","Bericht van de dag bijwerken (niet verzenden)"}. {"Update message of the day on all hosts (don't send)","Verander bericht-van-de-dag op alle hosts (niet versturen)"}. {"Update plan","Plan voor de opwaardering"}. {"Update ~p","Opwaarderen van ~p"}. {"Update script","Script voor de opwaardering"}. {"Update","Bijwerken"}. {"Uptime:","Uptime:"}. {"User JID","JID Gebruiker"}. {"User Management","Gebruikersbeheer"}. {"User","Gebruiker"}. {"Username:","Gebruikersnaam:"}. {"Users are not allowed to register accounts so quickly","Het is gebruikers niet toegestaan zo snel achter elkaar te registreren"}. {"Users Last Activity","Laatste activiteit van gebruikers"}. {"Users","Gebruikers"}. {"Validate","Bevestigen"}. {"vCard User Search","Gebruikers zoeken"}. {"Virtual Hosts","Virtuele hosts"}. {"Visitors are not allowed to change their nicknames in this room","Het is bezoekers niet toegestaan hun naam te veranderen in dit kanaal"}. {"Visitors are not allowed to send messages to all occupants","Bezoekers mogen geen berichten verzenden naar alle aanwezigen"}. {"Voice requests are disabled in this conference","Stemaanvragen zijn uitgeschakeld voor deze chatruimte"}. {"Voice request","Stemaanvraag"}. {"Wednesday","Woensdag"}. {"When to send the last published item","Wanneer het laatst gepubliceerde item verzonden moet worden"}. {"Whether to allow subscriptions","Abonnementsaanvraag toestaan"}. {"You have been banned from this room","U werd verbannen uit deze chatruimte"}. {"You must fill in field \"Nickname\" in the form","U moet het veld \"bijnaam\" invullen"}. {"You need a client that supports x:data and CAPTCHA to register","U hebt een client nodig die x:data en CAPTCHA ondersteunt om een bijnaam te registreren"}. {"You need a client that supports x:data to register the nickname","U hebt een client nodig die x:data ondersteunt om een bijnaam te registreren"}. {"You need an x:data capable client to search","U hebt een client nodig die x:data ondersteunt om te zoeken"}. {"Your active privacy list has denied the routing of this stanza.","Uw actieve privacy-lijst verbied het routeren van dit stanza."}. {"Your contact offline message queue is full. The message has been discarded.","Te veel offline berichten voor dit contactpersoon. Het bericht is niet opgeslagen."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Uw berichten aan ~s worden geblokkeerd. Om ze te deblokkeren, ga naar ~s"}. �����������������������������������������������������������������ejabberd-21.12/priv/msgs/he.msg���������������������������������������������������������������������0000644�0002322�0002322�00000067234�14154362354�016703� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," הגדיר/ה את הנושא אל: "}. {"A friendly name for the node","שם ידידותי עבור הצומת"}. {"A password is required to enter this room","נדרשת סיסמה כדי להיכנס אל חדר זה"}. {"Accept","קבל"}. {"Access denied by service policy","גישה נדחתה על ידי פוליסת שירות"}. {"Action on user","פעולה על משתמש"}. {"Add Jabber ID","הוסף מזהה Jabber"}. {"Add New","הוסף חדש"}. {"Add User","הוסף משתמש"}. {"Administration of ","ניהול של "}. {"Administration","הנהלה"}. {"Administrator privileges required","נדרשות הרשאות מנהל"}. {"All activity","כל פעילות"}. {"All Users","כל המשתמשים"}. {"Allow this Jabber ID to subscribe to this pubsub node?","להתיר למזהה Jabber זה להירשם לצומת PubSub זה?"}. {"Allow users to change the subject","התר למשתמשים לשנות את הנושא"}. {"Allow users to query other users","התר למשתמשים לתשאל משתמשים אחרים"}. {"Allow users to send invites","התר למשתמשים לשלוח הזמנות"}. {"Allow users to send private messages","התר למשתמשים לשלוח הודעות פרטיות"}. {"Allow visitors to change nickname","התר למבקרים לשנות שם כינוי"}. {"Allow visitors to send private messages to","התר למבקרים לשלוח הודעות פרטיות אל"}. {"Allow visitors to send status text in presence updates","התר למבקרים לשלוח טקסט מצב בתוך עדכוני נוכחות"}. {"Allow visitors to send voice requests","התר למבקרים לשלוח בקשות ביטוי"}. {"Announcements","בשורות"}. {"April","אפריל"}. {"August","אוגוסט"}. {"Automatic node creation is not enabled","יצירה אוטומטית של צומת אינה מאופשרת"}. {"Backup Management","ניהול גיבוי"}. {"Backup of ~p","גיבוי של ~p"}. {"Backup to File at ","גבה לקובץ אצל "}. {"Backup","גיבוי"}. {"Bad format","פורמט רע"}. {"Birthday","יום הולדת"}. {"Cannot remove active list","לא ניתן להסיר רשימה פעילה"}. {"Cannot remove default list","לא ניתן להסיר רשימה שגרתית"}. {"CAPTCHA web page","עמוד רשת CAPTCHA"}. {"Change Password","שנה סיסמה"}. {"Change User Password","שנה סיסמת משתמש"}. {"Changing password is not allowed","שינוי סיסמה אינו מותר"}. {"Changing role/affiliation is not allowed","שינוי תפקיד/שיוך אינו מותר"}. {"Characters not allowed:","תווים לא מורשים:"}. {"Chatroom configuration modified","תצורת חדר שיחה שונתה"}. {"Chatroom is created","חדר שיחה נוצר כעת"}. {"Chatroom is destroyed","חדר שיחה הינו הרוס"}. {"Chatroom is started","חדר שיחה מותחל כעת"}. {"Chatroom is stopped","חדר שיחה הינו מופסק"}. {"Chatrooms","חדרי שיחה"}. {"Choose a username and password to register with this server","בחר שם משתמש וסיסמה כדי להירשם בעזרת שרת זה"}. {"Choose storage type of tables","בחר טיפוס אחסון של טבלאות"}. {"Choose whether to approve this entity's subscription.","בחר האם לאשר את ההרשמה של ישות זו."}. {"City","עיר"}. {"Commands","פקודות"}. {"Conference room does not exist","חדר ועידה לא קיים"}. {"Configuration of room ~s","תצורת חדר ~s"}. {"Configuration","תצורה"}. {"Connected Resources:","משאבים מחוברים:"}. {"Country","ארץ"}. {"CPU Time:","זמן מחשב (CPU):"}. {"Database failure","כשל מסד נתונים"}. {"Database Tables at ~p","טבלאות מסד נתונים אצל ~p"}. {"Database Tables Configuration at ","תצורת טבלאות מסד נתונים אצל "}. {"Database","מסד נתונים"}. {"December","דצמבר"}. {"Default users as participants","משתמשים שגרתיים כמשתתפים"}. {"Delete message of the day on all hosts","מחק את בשורת היום בכל המארחים"}. {"Delete message of the day","מחק את בשורת היום"}. {"Delete Selected","מחק נבחרות"}. {"Delete User","מחק משתמש"}. {"Deliver event notifications","מסור התראות אירוע"}. {"Deliver payloads with event notifications","מסור מטעני ייעוד (מטע״ד) יחד עם התראות אירוע"}. {"Description:","תיאור:"}. {"Disc only copy","העתק של תקליטור בלבד"}. {"Dump Backup to Text File at ","השלך גיבוי לקובץ טקסט אצל "}. {"Dump to Text File","השלך לקובץ טקסט"}. {"Edit Properties","ערוך מאפיינים"}. {"Either approve or decline the voice request.","אשר או דחה בקשת ביטוי."}. {"ejabberd MUC module","מודול MUC של ejabberd"}. {"ejabberd Multicast service","שירות שידור מרובב של ejabberd"}. {"ejabberd Publish-Subscribe module","מודול Publish-Subscribe של ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","מודול SOCKS5 Bytestreams של ejabberd"}. {"ejabberd vCard module","מודול vCard של ejabberd"}. {"ejabberd Web Admin","מנהל רשת ejabberd"}. {"Elements","אלמנטים"}. {"Email","דוא״ל"}. {"Enable logging","אפשר רישום פעילות"}. {"Enable message archiving","אפשר אחסון הודעות"}. {"End User Session","סיים סשן משתמש"}. {"Enter nickname you want to register","הזן שם כינוי אשר ברצונך לרשום"}. {"Enter path to backup file","הזן נתיב לקובץ גיבוי"}. {"Enter path to jabberd14 spool dir","הזן נתיב למדור סליל (spool dir) של jabberd14"}. {"Enter path to jabberd14 spool file","הזן נתיב לקובץ סליל (spool file) של jabberd14"}. {"Enter path to text file","הזן נתיב לקובץ טקסט"}. {"Enter the text you see","הזן את הכיתוב שאתה רואה"}. {"Error","שגיאה"}. {"Exclude Jabber IDs from CAPTCHA challenge","הוצא כתובות Jabber מתוך אתגר CAPTCHA"}. {"Export all tables as SQL queries to a file:","יצא את כל הטבלאות בתור שאילתות SQL לתוך קובץ:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","יצא מידע של כל המשתמשים שבתוך שרת זה לתוך קבצי PIEFXIS ‏(XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","יצא מידע של כל המשתמשים שבתוך מארח לתוך קבצי PIEFXIS ‏(XEP-0227):"}. {"Failed to activate bytestream","נכשל להפעיל bytestream"}. {"Failed to extract JID from your voice request approval","נכשל לחלץ JID מתוך אישור בקשת הביטוי שלך"}. {"Failed to parse HTTP response","נכשל לפענח תגובת HTTP"}. {"Failed to process option '~s'","נכשל לעבד אפשרות '~s'"}. {"Family Name","שם משפחה"}. {"February","פברואר"}. {"File larger than ~w bytes","קובץ גדול יותר משיעור של ~w בייטים"}. {"Friday","יום שישי"}. {"From","מאת"}. {"Full Name","שם מלא"}. {"Get Number of Online Users","השג מספר של משתמשים מקוונים"}. {"Get Number of Registered Users","השג מספר של משתמשים רשומים"}. {"Get User Last Login Time","השג זמן כניסה אחרון של משתמש"}. {"Get User Password","השג סיסמת משתמש"}. {"Get User Statistics","השג סטטיסטיקת משתמש"}. {"Given Name","שם פרטי"}. {"Grant voice to this person?","להעניק ביטוי לאישיות זו?"}. {"Groups","קבוצות"}. {"Group","קבוצה"}. {"has been banned","נאסר/ה"}. {"has been kicked because of a system shutdown","נבעט/ה משום כיבוי מערכת"}. {"has been kicked because of an affiliation change","נבעט/ה משום שינוי סינוף"}. {"has been kicked because the room has been changed to members-only","נבעט/ה משום שהחדר שונה אל חברים-בלבד"}. {"has been kicked","נבעט/ה"}. {"Host unknown","מארח לא ידוע"}. {"Host","מארח"}. {"If you don't see the CAPTCHA image here, visit the web page.","אם אינך רואה תמונת CAPTCHA כאן, בקר בעמוד רשת."}. {"Import Directory","ייבוא מדור"}. {"Import File","ייבוא קובץ"}. {"Import user data from jabberd14 spool file:","יבא נתוני משתמש מתוך קובץ סליל (spool file) של jabberd14:"}. {"Import User from File at ","ייבוא משתמש מתוך קובץ אצל "}. {"Import users data from a PIEFXIS file (XEP-0227):","יבא מידע משתמשים מתוך קובץ PIEFXIS ‏(XEP-0227):"}. {"Import users data from jabberd14 spool directory:","יבא נתוני משתמשים מתוך מדור סליל (spool directory) של jabberd14:"}. {"Import Users from Dir at ","ייבוא משתמשים מתוך מדור אצל "}. {"Import Users From jabberd14 Spool Files","יבא משתמשים מתוך קבצי סליל (Spool Files) של jabberd14"}. {"Improper message type","טיפוס הודעה לא מתאים"}. {"Incoming s2s Connections:","חיבורי s2s נכנסים:"}. {"Incorrect CAPTCHA submit","נשלחה CAPTCHA שגויה"}. {"Incorrect data form","טופס מידע לא תקין"}. {"Incorrect password","מילת מעבר שגויה"}. {"Insufficient privilege","הרשאה לא מספיקה"}. {"Invitations are not allowed in this conference","הזמנות אינן מותרות בועידה זו"}. {"IP addresses","כתובות IP"}. {"is now known as","ידועה כעת בכינוי"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","אין זה מותר לשלוח הודעות שגיאה לחדר. משתתף זה (~s) שלח הודעת שגיאה (~s) ונבעט מתוך החדר"}. {"It is not allowed to send private messages of type \"groupchat\"","אין זה מותר לשלוח הודעות פרטיות מן טיפוס \"groupchat\""}. {"It is not allowed to send private messages to the conference","אין זה מותר לשלוח הודעות פרטיות לועידה"}. {"It is not allowed to send private messages","אין זה מותר לשלוח הודעות פרטיות"}. {"Jabber ID","מזהה Jabber"}. {"January","ינואר"}. {"joins the room","נכנס/ת אל החדר"}. {"July","יולי"}. {"June","יוני"}. {"Last Activity","פעילות אחרונה"}. {"Last login","כניסה אחרונה"}. {"Last month","חודש אחרון"}. {"Last year","שנה אחרונה"}. {"leaves the room","עוזב/ת את החדר"}. {"List of rooms","רשימה של חדרים"}. {"Low level update script","תסריט עדכון Low level"}. {"Make participants list public","הפוך רשימת משתתפים לפומבית"}. {"Make room CAPTCHA protected","הפוך חדר לחדר מוגן CAPTCHA"}. {"Make room members-only","הפוך חדר לחדר עבור חברים-בלבד"}. {"Make room moderated","הפוך חדר לחדר מבוקר"}. {"Make room password protected","הפוך חדר לחדר מוגן במילת מעבר"}. {"Make room persistent","הפוך חדר לחדר קבוע"}. {"Make room public searchable","הפוך חדר לחדר שנתון לחיפוש פומבי"}. {"Malformed username","שם משתמש פגום"}. {"March","מרץ"}. {"Max # of items to persist","מספר מרבי של פריטים לקיבוע"}. {"Max payload size in bytes","גודל מרבי של מטען ייעוד (payload) ביחידות מידה של byte"}. {"Maximum Number of Occupants","מספר מרבי של נוכחים"}. {"May","מאי"}. {"Membership is required to enter this room","נדרשת חברות כדי להיכנס אל חדר זה"}. {"Members:","חברים:"}. {"Memory","זיכרון"}. {"Message body","גוף הודעה"}. {"Middle Name","שם אמצעי"}. {"Minimum interval between voice requests (in seconds)","תדירות מינימלית בין בקשות ביטוי (בשניות)"}. {"Moderator privileges required","נדרשות הרשאות אחראי"}. {"Moderator","אחראי"}. {"Modified modules","מודולים שהותאמו"}. {"Module failed to handle the query","מודול נכשל לטפל בשאילתא"}. {"Monday","יום שני"}. {"Multicast","שידור מרובב"}. {"Multi-User Chat","שיחה מרובת משתמשים"}. {"Name","שם"}. {"Name:","שם:"}. {"Never","אף פעם"}. {"New Password:","סיסמה חדשה:"}. {"Nickname Registration at ","רישום שם כינוי אצל "}. {"Nickname","שם כינוי"}. {"No available resource found","לא נמצא משאב זמין"}. {"No body provided for announce message","לא סופק גוף עבור הודעת בשורה"}. {"No Data","אין מידע"}. {"No features available","אין תכונות זמינות"}. {"No items found in this query","לא נמצאו פריטים בתוך שאילתא זו"}. {"No limit","ללא הגבלה"}. {"No module is handling this query","אין מודול אשר מטפל בשאילתא זו"}. {"No node specified","לא צויין צומת"}. {"No pending subscriptions found","לא נמצאו הרשמות ממתינות"}. {"No privacy list with this name found","לא נמצאה רשימת פרטיות בשם זה"}. {"No private data found in this query","לא נמצא מידע פרטי בתוך שאילתא זו"}. {"No running node found","לא נמצא צומת מורץ"}. {"No services available","אין שירות זמין"}. {"No statistics found for this item","לא נמצאה סטטיסטיקה לגבי פריט זה"}. {"Node already exists","צומת כבר קיים"}. {"Node ID","מזהה צומת (NID)"}. {"Node index not found","מפתח צומת לא נמצא"}. {"Node not found","צומת לא נמצא"}. {"Node ~p","צומת ~p"}. {"Nodeprep has failed","‏Nodeprep נכשל"}. {"Nodes","צמתים"}. {"None","אין"}. {"Not Found","לא נמצא"}. {"Not subscribed","לא רשום"}. {"Notify subscribers when items are removed from the node","הודע מנויים כאשר פריטים מוסרים מתוך הצומת"}. {"Notify subscribers when the node configuration changes","הודע מנויים כאשר תצורת הצומת משתנה"}. {"Notify subscribers when the node is deleted","הודע מנויים כאשר הצומת נמחק"}. {"November","נובמבר"}. {"Number of occupants","מספר של נוכחים"}. {"Number of online users","מספר של משתמשים מקוונים"}. {"Number of registered users","מספר של משתמשים רשומים"}. {"October","אוקטובר"}. {"Offline Messages","הודעות לא מקוונות"}. {"Offline Messages:","הודעות לא מקוונות:"}. {"OK","אישור"}. {"Old Password:","סיסמה ישנה:"}. {"Online Users","משתמשים מקוונים"}. {"Online Users:","משתמשים מקוונים:"}. {"Online","מקוון"}. {"Only deliver notifications to available users","מסור התראות למשתמשים זמינים בלבד"}. {"Only <enable/> or <disable/> tags are allowed","רק תגיות <enable/> או <disable/> הינן מורשות"}. {"Only members may query archives of this room","רק חברים רשאים לתשאל ארכיונים של חדר זה"}. {"Only moderators and participants are allowed to change the subject in this room","רק אחראים ומשתתפים רשאים לשנות את הנושא בחדר זה"}. {"Only moderators are allowed to change the subject in this room","רק אחראים רשאים לשנות את הנושא בחדר זה"}. {"Only moderators can approve voice requests","רק אחראים יכולים לאשר בקשות ביטוי"}. {"Only occupants are allowed to send messages to the conference","רק נוכחים רשאים לשלוח הודעות אל הועידה"}. {"Only occupants are allowed to send queries to the conference","רק נוכחים רשאים לשלוח שאילתות אל הועידה"}. {"Only service administrators are allowed to send service messages","רק מנהלי שירות רשאים לשלוח הודעות שירות"}. {"Organization Name","שם ארגון"}. {"Organization Unit","יחידת איגוד"}. {"Outgoing s2s Connections","חיבורי s2s יוצאים"}. {"Outgoing s2s Connections:","חיבורי s2s יוצאים:"}. {"Owner privileges required","נדרשות הרשאות בעלים"}. {"Packet","חבילת מידע"}. {"Participant","משתתף"}. {"Password Verification","אימות סיסמה"}. {"Password Verification:","אימות סיסמה:"}. {"Password","סיסמה"}. {"Password:","סיסמה:"}. {"Path to Dir","נתיב למדור"}. {"Path to File","נתיב לקובץ"}. {"Pending","ממתינות"}. {"Period: ","משך זמן: "}. {"Persist items to storage","פריטים קבועים לאחסון"}. {"Ping query is incorrect","שאילתת פינג הינה שגויה"}. {"Ping","פינג"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","אנא שים לב כי אפשרויות אלו יגבו את מסד הנתונים המובנה Mnesia בלבד. אם הינך עושה שימוש במודול ODBC, עליך גם לגבות את מסד הנתונים SQL אשר מצוי ברשותך בנפרד."}. {"Please, wait for a while before sending new voice request","אנא, המתן לזמן מה לפני שליחת בקשת ביטוי חדשה"}. {"Pong","פונג"}. {"Present real Jabber IDs to","הצג כתובות Jabber ממשיות"}. {"private, ","פרטי, "}. {"Publish-Subscribe","‫Publish-Subscribe"}. {"PubSub subscriber request","בקשת מנוי PubSub"}. {"Purge all items when the relevant publisher goes offline","טהר את כל הפריטים כאשר המפרסם הרלוונטי הופך לבלתי מקוון"}. {"Queries to the conference members are not allowed in this room","שאילתות אל חברי הועידה אינן מותרות בחדר זה"}. {"RAM and disc copy","העתק RAM וגם תקליטור"}. {"RAM copy","העתק RAM"}. {"Really delete message of the day?","באמת למחוק את בשורת היום?"}. {"Recipient is not in the conference room","מקבל אינו מצוי בחדר הועידה"}. {"Registered Users","משתמשים רשומים"}. {"Registered Users:","משתמשים רשומים:"}. {"Register","הרשם"}. {"Remote copy","העתק מרוחק"}. {"Remove All Offline Messages","הסר את כל ההודעות הלא מקוונות"}. {"Remove User","הסר משתמש"}. {"Remove","הסר"}. {"Replaced by new connection","הוחלף בחיבור חדש"}. {"Resources","משאבים"}. {"Restart Service","אתחל שירות"}. {"Restart","אתחל"}. {"Restore Backup from File at ","שחזר גיבוי מתוך קובץ אצל "}. {"Restore binary backup after next ejabberd restart (requires less memory):","שחזר גיבוי בינארי לאחר האתחול הבא של ejabberd (מצריך פחות זיכרון):"}. {"Restore binary backup immediately:","שחזר גיבוי בינארי לאלתר:"}. {"Restore plain text backup immediately:","שחזר גיבוי טקסט גלוי (plain text) לאלתר:"}. {"Restore","שחזר"}. {"Roles for which Presence is Broadcasted","תפקידים להם נוכחות הינה משודרת"}. {"Room Configuration","תצורת חדר"}. {"Room creation is denied by service policy","יצירת חדר נדחתה על ידי פוליסת שירות"}. {"Room description","תיאור חדר"}. {"Room Occupants","נוכחי חדר"}. {"Room title","כותרת חדר"}. {"Roster groups allowed to subscribe","קבוצות רשימה מורשות להירשם"}. {"Roster size","גודל רשימה"}. {"RPC Call Error","שגיאת קריאת RPC"}. {"Running Nodes","צמתים מורצים"}. {"Saturday","יום שבת"}. {"Script check","בדיקת תסריט"}. {"Search Results for ","תוצאות חיפוש עבור "}. {"Search users in ","חיפוש משתמשים אצל "}. {"Send announcement to all online users on all hosts","שלח בשורה לכל המשתמשים המקוונים בכל המארחים"}. {"Send announcement to all online users","שלח בשורה לכל המשתמשים המקוונים"}. {"Send announcement to all users on all hosts","שלח בשורה לכל המשתמשים בכל המארחים"}. {"Send announcement to all users","שלח בשורה לכל המשתמשים"}. {"September","ספטמבר"}. {"Server:","שרת:"}. {"Set message of the day and send to online users","קבע את בשורת היום ושלח למשתמשים מקוונים"}. {"Set message of the day on all hosts and send to online users","קבע את בשורת היום בכל המארחים ושלח למשתמשים מקוונים"}. {"Shared Roster Groups","קבוצות רשימה משותפות"}. {"Show Integral Table","הצג טבלה אינטגרלית"}. {"Show Ordinary Table","הצג טבלה רגילה"}. {"Shut Down Service","כבה שירות"}. {"Specify the access model","ציין מודל גישה"}. {"Specify the event message type","ציין טיפוס הודעת אירוע"}. {"Specify the publisher model","ציין מודל פרסום"}. {"Statistics of ~p","סטטיסטיקות של ~p"}. {"Statistics","סטטיסטיקה"}. {"Stopped Nodes","צמתים שנפסקו"}. {"Stop","הפסק"}. {"Storage Type","טיפוס אחסון"}. {"Store binary backup:","אחסן גיבוי בינארי:"}. {"Store plain text backup:","אחסן גיבוי טקסט גלוי (plain text):"}. {"Subject","נושא"}. {"Submitted","נשלח"}. {"Submit","שלח"}. {"Subscriber Address","כתובת מנוי"}. {"Subscriptions are not allowed","הרשמות אינן מורשות"}. {"Subscription","הרשמה"}. {"Sunday","יום ראשון"}. {"That nickname is already in use by another occupant","שם כינוי זה כבר מצוי בשימוש על ידי נוכח אחר"}. {"That nickname is registered by another person","שם כינוי זה הינו רשום על ידי מישהו אחר"}. {"The CAPTCHA is valid.","‏CAPTCHA הינה תקפה."}. {"The CAPTCHA verification has failed","אימות CAPTCHA נכשל"}. {"The collections with which a node is affiliated","האוספים עמם צומת מסונף"}. {"The password is too weak","הסיסמה חלשה מדי"}. {"the password is","הסיסמה היא"}. {"The type of node data, usually specified by the namespace of the payload (if any)","סוג מידע ממסר, לרוב מצוין לפי מרחב־שמות של מטען הייעוד (אם בכלל)"}. {"There was an error creating the account: ","אירעה שגיאה ביצירת החשבון: "}. {"There was an error deleting the account: ","אירעה שגיאה במחיקת החשבון: "}. {"This room is not anonymous","חדר זה אינו אנונימי"}. {"Thursday","יום חמישי"}. {"Time delay","זמן שיהוי"}. {"Time","זמן"}. {"To register, visit ~s","כדי להירשם, בקרו ~s"}. {"Token TTL","סימן TTL"}. {"Too many active bytestreams","יותר מדי יחידות bytestream פעילות"}. {"Too many CAPTCHA requests","יותר מדי בקשות CAPTCHA"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","יותר מדי (~p) אימותים כושלים מתוך כתובת IP זו (~s). הכתובת תורשה לקבל גישה בשעה ~s UTC"}. {"Too many unacked stanzas","יותר מדי סטנזות בלי אישורי קבלה"}. {"Too many users in this conference","יותר מדי משתמשים בועידה זו"}. {"Total rooms","חדרים סה״כ"}. {"To","לכבוד"}. {"Traffic rate limit is exceeded","מגבלת שיעור תעבורה נחצתה"}. {"Transactions Aborted:","טרנזקציות שבוטלו:"}. {"Transactions Committed:","טרנזקציות שבוצעו:"}. {"Transactions Logged:","טרנזקציות שנרשמו:"}. {"Transactions Restarted:","טרנזקציות שהותחלו מחדש:"}. {"Tuesday","יום שלישי"}. {"Unable to generate a CAPTCHA","אין אפשרות להפיק CAPTCHA"}. {"Unauthorized","לא מורשה"}. {"Unexpected action","פעולה לא צפויה"}. {"Unregister","בטל רישום"}. {"Update message of the day (don't send)","עדכן את בשורת היום (אל תשלח)"}. {"Update message of the day on all hosts (don't send)","עדכן את בשורת היום בכל המארחים (אל תשלח)"}. {"Update plan","תכנית עדכון"}. {"Update ~p","עדכון ~p"}. {"Update script","תסריט עדכון"}. {"Update","עדכן"}. {"Uptime:","זמן פעילות:"}. {"User already exists","משתמש כבר קיים"}. {"User JID","‏JID משתמש"}. {"User (jid)","משתמש (jid)"}. {"User Management","ניהול משתמשים"}. {"User session not found","סשן משתמש לא נמצא"}. {"User session terminated","סשן משתמש הסתיים"}. {"Username:","שם משתמש:"}. {"Users are not allowed to register accounts so quickly","משתמשים אינם מורשים לרשום חשבונות כל כך במהירות"}. {"Users Last Activity","פעילות משתמשים אחרונה"}. {"Users","משתמשים"}. {"User","משתמש"}. {"Validate","הענק תוקף"}. {"Value of '~s' should be boolean","ערך של '~s' צריך להיות boolean"}. {"Value of '~s' should be datetime string","ערך של '~s' צריך להיות מחרוזת datetime"}. {"Value of '~s' should be integer","ערך של '~s' צריך להיות integer"}. {"vCard User Search","חיפוש משתמש vCard"}. {"Virtual Hosts","מארחים מדומים"}. {"Visitors are not allowed to change their nicknames in this room","מבקרים אינם מורשים לשנות את שמות הכינויים שלהם בחדר זה"}. {"Visitors are not allowed to send messages to all occupants","מבקרים אינם מורשים לשלוח הודעות אל כל הנוכחים"}. {"Visitor","מבקר"}. {"Voice requests are disabled in this conference","בקשות ביטוי מנוטרלות בועידה זו"}. {"Voice request","בקשת ביטוי"}. {"Wednesday","יום רביעי"}. {"When to send the last published item","מתי לשלוח את הפריט המפורסם האחרון"}. {"Whether to allow subscriptions","האם להתיר הרשמות"}. {"You have been banned from this room","נאסרת מן חדר זה"}. {"You have joined too many conferences","הצטרפת ליותר מדי ועידות"}. {"You must fill in field \"Nickname\" in the form","עליך למלא את השדה \"שם כינוי\" בתוך התבנית"}. {"You need a client that supports x:data and CAPTCHA to register","עליך להשתמש בלקוח אשר תומך x:data וגם CAPTCHA כדי להירשם"}. {"You need a client that supports x:data to register the nickname","עליך להשתמש בלקוח אשר תומך x:data כדי לרשום את השם כינוי"}. {"You need an x:data capable client to search","עליך להשתמש בלקוח אשר מסוגל להבין x:data כדי לחפש"}. {"Your active privacy list has denied the routing of this stanza.","רשימת הפרטיות הפעילה שלך אסרה את הניתוב של סטנזה זו."}. {"Your contact offline message queue is full. The message has been discarded.","תור הודעות קשר לא מקוונות הינו מלא. ההודעה סולקה."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","ההודעות שלך לערוץ ~s הינן חסומות. כדי לבטל את חסימתן, בקר בכתובת ~s"}. {"You're not allowed to create nodes","אינך מורשה ליצור צמתים"}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/wa.msg���������������������������������������������������������������������0000644�0002322�0002322�00000055243�14154362354�016713� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," a candjî l' tite a: "}. {"A friendly name for the node","On no uzeu-ahessåve pol nuk"}. {"A password is required to enter this room","I fåt dner on scret po poleur intrer dins cisse såle ci"}. {"Accept","Accepter"}. {"Access denied by service policy","L' accès a stî rfuzé pal politike do siervice"}. {"Action on user","Accion so l' uzeu"}. {"Add Jabber ID","Radjouter èn ID Jabber"}. {"Add New","Radjouter"}. {"Add User","Radjouter èn uzeu"}. {"Administration of ","Manaedjaedje di "}. {"Administration","Manaedjaedje"}. {"Administrator privileges required","I fåt des priviledjes di manaedjeu"}. {"All activity","Dispoy todi"}. {"All Users","Tos les uzeus"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Permete ki ci Jabber ID ci si poye abouner a ç' nuk eplaidaedje-abounmint ci?"}. {"Allow users to change the subject","Les uzeus polèt candjî l' tite"}. {"Allow users to query other users","Les uzeus polèt cweri ls ôtes uzeus"}. {"Allow users to send invites","Les uzeus polèt evoyî priyaedjes"}. {"Allow users to send private messages","Les uzeus polèt evoyî des messaedjes privés"}. {"Allow visitors to change nickname","Permete ki les viziteus candjexhe leus metous nos"}. {"Allow visitors to send private messages to","Les uzeus polèt evoyî des messaedjes privés"}. {"Allow visitors to send status text in presence updates","Permete ki les viziteus evoyexhe des tecse d' estat dins leus messaedjes di prezince"}. {"Allow visitors to send voice requests","Les uzeus polèt evoyî des dmandes di vwès"}. {"Announcements","Anonces"}. {"April","avri"}. {"August","awousse"}. {"Backup Management","Manaedjaedje des copeyes di såvrité"}. {"Backup of ~p","Copeye di såvrité po ~p"}. {"Backup to File at ","Fé ene copeye di såvrité dins on fitchî so "}. {"Backup","Copeye di såvrité"}. {"Bad format","Mwais fôrmat"}. {"Birthday","Date d' askepiaedje"}. {"CAPTCHA web page","Pådje web CAPTCHA"}. {"Change Password","Candjî l' sicret"}. {"Change User Password","Candjî l' sicret d' l' uzeu"}. {"Characters not allowed:","Caracteres nén permetous:"}. {"Chatroom configuration modified","L' apontiaedje del såle di berdelaedje a candjî"}. {"Chatroom is created","Li såle di berdelaedje est ahivêye"}. {"Chatroom is destroyed","Li såle di berdelaedje est distrûte"}. {"Chatroom is started","Li såle di berdelaedje est enondêye"}. {"Chatroom is stopped","Li såle di berdelaedje est ahotêye"}. {"Chatrooms","Såles di berdelaedje"}. {"Choose a username and password to register with this server","Tchoezixhoz on no d' uzeu eyet on scret po vs edjîstrer so ç' sierveu ci"}. {"Choose storage type of tables","Tchoezi l' sôre di wårdaedje po les tåves"}. {"Choose whether to approve this entity's subscription.","Tchoezi s' i fåt aprover ou nén l' abounmint di ciste intité."}. {"City","Veye"}. {"Commands","Comandes"}. {"Conference room does not exist","Li såle di conferince n' egzistêye nén"}. {"Configuration of room ~s","Apontiaedje del såle ~s"}. {"Configuration","Apontiaedjes"}. {"Connected Resources:","Raloyî avou les rsoûces:"}. {"Country","Payis"}. {"CPU Time:","Tins CPU:"}. {"Database Tables at ~p","Tåves del båze di dnêyes so ~p"}. {"Database Tables Configuration at ","Apontiaedje des tåves del båze di dnêyes so "}. {"Database","Båze di dnêyes"}. {"December","decimbe"}. {"Default users as participants","Les uzeus sont des pårticipants come prémetowe dujhance"}. {"Delete message of the day on all hosts","Disfacer l' messaedje do djoû so tos les lodjoes"}. {"Delete message of the day","Disfacer l' messaedje do djoû"}. {"Delete Selected","Disfacer les elemints tchoezis"}. {"Delete User","Disfacer èn uzeu"}. {"Deliver event notifications","Evoyî des notifiaedjes d' evenmints"}. {"Deliver payloads with event notifications","Evoyî l' contnou avou les notifiaedjes d' evenmints"}. {"Description:","Discrijhaedje:"}. {"Disc only copy","Copeye seulmint sol deure plake"}. {"Dump Backup to Text File at ","Copeye di såvritè viè on fitchî tecse so "}. {"Dump to Text File","Schaper en on fitchî tecse"}. {"Edit Properties","Candjî les prôpietés"}. {"Either approve or decline the voice request.","Aprover oudonbén rifuzer li dmande di vwès."}. {"ejabberd MUC module","Module MUC (såles di berdelaedje) po ejabberd"}. {"ejabberd Multicast service","siervice multicast d' ejabberd"}. {"ejabberd Publish-Subscribe module","Module d' eplaidaedje-abounmint po ejabberd"}. {"ejabberd SOCKS5 Bytestreams module","Module SOCKS5 Bytestreams po ejabberd"}. {"ejabberd vCard module","Module vCard ejabberd"}. {"ejabberd Web Admin","Manaedjeu waibe ejabberd"}. {"Elements","Elemints"}. {"Email","Emile"}. {"Enable logging","Mete en alaedje li djournå"}. {"Enable message archiving","Mete en alaedje l' årtchivaedje des messaedjes"}. {"End User Session","Fini l' session d' l' uzeu"}. {"Enter nickname you want to register","Dinez l' metou no ki vos vloz edjîstrer"}. {"Enter path to backup file","Dinez l' tchimin viè l' fitchî copeye di såvrité"}. {"Enter path to jabberd14 spool dir","Dinez l' tchimin viè l' ridant di spool jabberd14"}. {"Enter path to jabberd14 spool file","Dinez l' tchimin viè l' fitchî di spool jabberd14"}. {"Enter path to text file","Dinez l' tchimin viè l' fitchî tecse"}. {"Enter the text you see","Tapez l' tecse ki vos voeyoz"}. {"Error","Aroke"}. {"Exclude Jabber IDs from CAPTCHA challenge","Esclure les IDs Jabber des kesses CAPTCHA"}. {"Export all tables as SQL queries to a file:","Espoirter totes les tåves, come des cmandes SQL, viè on fitchî"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Espoirter les dnêyes di tos les uzeus do sierveu viè des fitchîs PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Espoirter les dnêyes di tos les uzeus do sierveu viè des fitchîs PIEFXIS (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Nén moyén di rsaetchî on JID foû d' l' aprovaedje di vosse dimande di vwès"}. {"Family Name","No d' famile"}. {"February","fevrî"}. {"Friday","vénrdi"}. {"From","Di"}. {"Full Name","No etir"}. {"Get Number of Online Users","Riçure li nombe d' uzeus raloyîs"}. {"Get Number of Registered Users","Riçure li nombe d' uzeus edjîstrés"}. {"Get User Last Login Time","Riçure li date/eure do dierin elodjaedje di l' uzeu"}. {"Get User Password","Riçure sicret d' l' uzeu"}. {"Get User Statistics","Riçure les statistikes di l' uzeu"}. {"Grant voice to this person?","Permete li vwès po cisse djin ci?"}. {"Group","Groupe"}. {"Groups","Groupes"}. {"has been banned","a stî bani"}. {"has been kicked because of a system shutdown","a stî pité evoye cåze d' èn arestaedje do sistinme"}. {"has been kicked because of an affiliation change","a stî pité evoye cåze d' on candjmint d' afiyaedje"}. {"has been kicked because the room has been changed to members-only","a stî pité evoye cåze ki l' såle a stî ristrindowe åzès mimbes seulmint"}. {"has been kicked","a stî pité evoye"}. {"Host","Sierveu"}. {"If you don't see the CAPTCHA image here, visit the web page.","Si vos n' voeyoz nole imådje CAPTCHA chal, vizitez l' pådje waibe."}. {"Import Directory","Sititchî d' on ridant"}. {"Import File","Sititchî d' on fitchî"}. {"Import user data from jabberd14 spool file:","Sititchî des dnêyes uzeus foû d' on fitchî spoûle jabberd14:"}. {"Import User from File at ","Sititchî uzeu d' on fitchî so "}. {"Import users data from a PIEFXIS file (XEP-0227):","Sititchî des dnêyes uzeus foû d' on fitchî PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Sititchî des dnêyes uzeus foû d' on ridant spoûle jabberd14:"}. {"Import Users from Dir at ","Sitichî des uzeus d' on ridant so "}. {"Import Users From jabberd14 Spool Files","Sititchî des uzeus Jabberd 1.4"}. {"Improper message type","Sôre di messaedje nén valide"}. {"Incoming s2s Connections:","Raloyaedjes s2s en intrêye:"}. {"Incorrect password","Sicret nén corek"}. {"IP addresses","Adresses IP"}. {"is now known as","est asteure kinoxhou come"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","On n' pout nén evoyî des messaedjes d' aroke sol såle. Li pårticipan (~s) a-st evoyî on messaedje d' aroke (~s) ey a stî tapé foû."}. {"It is not allowed to send private messages of type \"groupchat\"","C' est nén possibe d' evoyî des messaedjes privés del sôre «groupchat»"}. {"It is not allowed to send private messages to the conference","On n' pout nén evoyî des messaedjes privés dins cisse conferince ci"}. {"It is not allowed to send private messages","Ci n' est nén permetou d' evoyî des messaedjes privés"}. {"Jabber ID","ID Jabber"}. {"January","djanvî"}. {"joins the room","arive sol såle"}. {"July","djulete"}. {"June","djun"}. {"Last Activity","Dierinne activité"}. {"Last login","Dierin elodjaedje"}. {"Last month","Dierin moes"}. {"Last year","Dierinne anêye"}. {"leaves the room","cwite li såle"}. {"List of rooms","Djivêye des såles"}. {"Low level update script","Sicripe di metaedje a djoû d' bas livea"}. {"Make participants list public","Rinde publike li djivêye des pårticipants"}. {"Make room CAPTCHA protected","Rinde li såle di berdelaedje protedjeye pa CAPTCHA"}. {"Make room members-only","Rinde li såle di berdelaedje ristrindowe ås mimbes seulmint"}. {"Make room moderated","Rinde li såle di berdelaedje moderêye"}. {"Make room password protected","Rinde li såle di berdelaedje protedjeye pa scret"}. {"Make room persistent","Rinde li såle permaninte"}. {"Make room public searchable","Rinde li såle di berdelaedje cweråve publicmint"}. {"March","måss"}. {"Max # of items to persist","Nombe macsimoms di cayets permanints"}. {"Max payload size in bytes","Contnou macsimom en octets"}. {"Maximum Number of Occupants","Nombe macsimom di prezints"}. {"May","may"}. {"Membership is required to enter this room","I fåt esse mimbe po poleur intrer dins cisse såle ci"}. {"Members:","Mimbes:"}. {"Memory","Memwere"}. {"Message body","Coir do messaedje"}. {"Middle Name","No do mitan"}. {"Minimum interval between voice requests (in seconds)","Tins minimom etur deus dmandes di vwès (e segondes)"}. {"Moderator privileges required","I fåt des priviledjes di moderateu"}. {"Moderator","Moderateu"}. {"Modified modules","Modules di candjîs"}. {"Monday","londi"}. {"Multicast","Multicast"}. {"Multi-User Chat","Berdelaedje a sacwants"}. {"Name","No"}. {"Name:","Pitit no:"}. {"Never","Måy"}. {"New Password:","Novea scret:"}. {"Nickname Registration at ","Edjîstraedje di metou no amon "}. {"Nickname ~s does not exist in the room","Li metou no ~s n' egzistêye nén dins l' såle"}. {"Nickname","Metou no"}. {"No body provided for announce message","I n' a nou coir do messaedje po ciste anonce la"}. {"No Data","Nole dinêye disponibe"}. {"No limit","Pont d' limite"}. {"Node ID","ID d' nuk"}. {"Node not found","Nuk nén trové"}. {"Node ~p","Nuk ~p"}. {"Nodes","Nuks"}. {"None","Nole"}. {"Not Found","Nén trové"}. {"Notify subscribers when items are removed from the node","Notifyî åzès abounés cwand des cayets sont oisté foû do nuk"}. {"Notify subscribers when the node configuration changes","Notifyî åzès abounés cwand l' apontiaedje do nuk candje"}. {"Notify subscribers when the node is deleted","Notifyî åzès abounés cwand l' nuk est disfacé"}. {"November","nôvimbe"}. {"Number of occupants","Nombe di prezints"}. {"Number of online users","Nombe d' uzeus raloyîs"}. {"Number of registered users","Nombe d' uzeus edjîstrés"}. {"October","octôbe"}. {"Offline Messages","Messaedjes ki ratindèt"}. {"Offline Messages:","Messaedjes ki ratindèt:"}. {"OK","'l est bon"}. {"Old Password:","Vî scret:"}. {"Online Users","Uzeus raloyîs"}. {"Online Users:","Uzeus raloyîs:"}. {"Online","Raloyî"}. {"Only deliver notifications to available users","Seulmint evoyî des notifiaedje åzès uzeus disponibes"}. {"Only members may query archives of this room","Seulmint les mimbes polèt cweri les årtchives dins cisse såle ci"}. {"Only moderators and participants are allowed to change the subject in this room","Seulmint les moderateus et les pårticipants polèt candjî l' sudjet dins cisse såle ci"}. {"Only moderators are allowed to change the subject in this room","Seulmint les moderateus polèt candjî l' sudjet dins cisse såle ci"}. {"Only moderators can approve voice requests","Seulmint les moderateus polèt aprover des dmandes di vwès"}. {"Only occupants are allowed to send messages to the conference","Seulmint les prezints polèt evoyî des messaedjes al conferince"}. {"Only occupants are allowed to send queries to the conference","Seulmint les prezints polèt evoyî des cweraedjes sol conferince"}. {"Only service administrators are allowed to send service messages","Seulmint les manaedjeus d' siervices polèt evoyî des messaedjes di siervice"}. {"Organization Name","No d' l' organizåcion"}. {"Organization Unit","Unité d' l' organizåcion"}. {"Outgoing s2s Connections","Raloyaedjes s2s e rexhowe"}. {"Outgoing s2s Connections:","Raloyaedjes s2s e rexhowe:"}. {"Owner privileges required","I fåt des priviledjes di prôpietaire"}. {"Packet","Paket"}. {"Participant","Pårticipant"}. {"Password Verification","Acertinaedje do scret"}. {"Password Verification:","Acertinaedje do scret:"}. {"Password","Sicret"}. {"Password:","Sicret:"}. {"Path to Dir","Tchimin viè l' ridant"}. {"Path to File","Tchimin viè l' fitchî"}. {"Pending","Ratindant"}. {"Period: ","Termene:"}. {"Persist items to storage","Cayets permanints a wårder"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Notez ki ces tchuzes la vont seulmint fé ene copeye di såvrité del båze di dnêyes Mnesia costrûte å dvins do programe. Si vos eployîz ene difoûtrinne båze di dnêyes avou l' module ODBC, vos dvoz fé ene copeye di såvrité del båze SQL da vosse sepårumint."}. {"Please, wait for a while before sending new voice request","Ratindez ene miete s' i vs plait divant d' rivoyî ene nouve dimande di vwès"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Mostrer les vraiys Jabber IDs a"}. {"private, ","privé, "}. {"Publish-Subscribe","Eplaidaedje-abounmint"}. {"PubSub subscriber request","Dimande d' eplaidaedje-abounmint d' èn abouné"}. {"Purge all items when the relevant publisher goes offline","Purdjî tos les cayets cwand l' eplaideu aloyî va foû raloyaedje"}. {"Queries to the conference members are not allowed in this room","Les cweraedjes des mimbes del conferince ni sont nén permetous dins cisse såle ci"}. {"RAM and disc copy","Copeye e memwere (RAM) et sol deure plake"}. {"RAM copy","Copeye e memwere (RAM)"}. {"Really delete message of the day?","Voloz vs vormint disfacer l' messaedje do djoû?"}. {"Recipient is not in the conference room","Li riçuveu n' est nén dins l' såle di conferince"}. {"Registered Users","Uzeus edjistrés"}. {"Registered Users:","Uzeus edjistrés:"}. {"Register","Edjîstrer"}. {"Remote copy","Copeye å lon"}. {"Remove All Offline Messages","Oister tos les messaedjes ki ratindèt"}. {"Remove User","Disfacer l' uzeu"}. {"Remove","Oister"}. {"Replaced by new connection","Replaecî pa on novea raloyaedje"}. {"Resources","Rissoûces"}. {"Restart Service","Renonder siervice"}. {"Restart","Renonder"}. {"Restore Backup from File at ","Rapexhî dispoy li fitchî copeye di såvrité so "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Rapexhî l' copeye di såvrité binaire après l' renondaedje ki vént d' ejabberd (çoula prind moens d' memwere del fé insi):"}. {"Restore binary backup immediately:","Rapexhî do côp foû d' ene copeye di såvrité binaire:"}. {"Restore plain text backup immediately:","Rapexhî do côp foû d' ene copeye di såvrité tecse:"}. {"Restore","Rapexhî"}. {"Roles for which Presence is Broadcasted","Roles ki leu prezince est difuzêye"}. {"Room Configuration","Apontiaedje del såle"}. {"Room creation is denied by service policy","L' ahivaedje del såle est rfuzé pal politike do siervice"}. {"Room description","Discrijhaedje del såle"}. {"Room Occupants","Prezints el såle"}. {"Room title","Tite del såle"}. {"Roster groups allowed to subscribe","Pårtaedjîs groupes di soçons k' on s' î pout abouner"}. {"Roster size","Grandeu del djivêye des soçons"}. {"RPC Call Error","Aroke di houcaedje RPC"}. {"Running Nodes","Nuks en alaedje"}. {"Saturday","semdi"}. {"Script check","Acertinaedje do scripe"}. {"Search Results for ","Rizultats do cweraedje po "}. {"Search users in ","Cweri des uzeus dins "}. {"Send announcement to all online users on all hosts","Evoyî l' anonce a tos les uzeus raloyîs so tos les lodjoes"}. {"Send announcement to all online users","Evoyî l' anonce a tos les uzeus raloyîs"}. {"Send announcement to all users on all hosts","Evoyî l' anonce a tos les uzeus so tos les lodjoes"}. {"Send announcement to all users","Evoyî l' anonce a tos les uzeus"}. {"September","setimbe"}. {"Server:","Sierveu:"}. {"Set message of the day and send to online users","Defini l' messaedje do djoû et l' evoyî åzès uzeus raloyîs"}. {"Set message of the day on all hosts and send to online users","Defini l' messaedje do djoû so tos les lodjoes et l' evoyî åzès uzeus raloyîs"}. {"Shared Roster Groups","Pårtaedjîs groupes ezès djivêyes di soçons"}. {"Show Integral Table","Mostrer totå"}. {"Show Ordinary Table","Mostrer crexhince"}. {"Shut Down Service","Arester siervice"}. {"Specify the access model","Sipecifyî l' modele d' accès"}. {"Specify the event message type","Sipecifyî l' sôre do messaedje d' evenmint"}. {"Specify the publisher model","Dinez l' modele d' eplaideu"}. {"Statistics of ~p","Sitatistikes di ~p"}. {"Statistics","Sitatistikes"}. {"Stop","Arester"}. {"Stopped Nodes","Nuks essoctés"}. {"Storage Type","Sôre di wårdaedje"}. {"Store binary backup:","Copeye di såvrité binaire:"}. {"Store plain text backup:","Copeye di såvrité tecse:"}. {"Subject","Sudjet"}. {"Submit","Evoyî"}. {"Submitted","Candjmints evoyîs"}. {"Subscriber Address","Adresse di l' abouné"}. {"Subscription","Abounmimnt"}. {"Sunday","dimegne"}. {"That nickname is already in use by another occupant","Li metou no est ddja eployî pa ene ôte sakî sol såle"}. {"That nickname is registered by another person","Li metou no est ddja edjîstré pa ene ôte sakî"}. {"The CAPTCHA is valid.","Li CAPTCHA est valide."}. {"The CAPTCHA verification has failed","Li verifiaedje CAPTCHA a fwait berwete"}. {"The collections with which a node is affiliated","Les ramexhnêyes k' on nuk est afiyî avou"}. {"The password is too weak","li scret est trop flåw"}. {"the password is","li scret est"}. {"There was an error creating the account: ","Åk n' a nén stî tot ahivant l' conte: "}. {"There was an error deleting the account: ","Åk n' a nén stî tot disfaçant l' conte: "}. {"This room is not anonymous","Cisse såle ci n' est nén anonime"}. {"Thursday","djudi"}. {"Time delay","Tårdjaedje"}. {"Time","Date"}. {"Too many CAPTCHA requests","Pår trop di dmandes CAPTCHA"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","I gn a-st avou pår trop (~p) d' otintifiaedjes k' ont fwait berwete vinant di ciste adresse IP la (~s). L' adresse serè disblokêye a ~s UTC"}. {"Too many unacked stanzas","Pår trop di messaedjes sins acertinaedje di rçuvaedje"}. {"To","Po"}. {"Total rooms","Totå di såles"}. {"Traffic rate limit is exceeded","Li limite pol volume di trafik a stî passêye"}. {"Transactions Aborted:","Transaccions arestêyes:"}. {"Transactions Committed:","Transaccions evoyeyes:"}. {"Transactions Logged:","Transaccions wårdêyes e djournå:"}. {"Transactions Restarted:","Transaccions renondêyes:"}. {"Tuesday","mårdi"}. {"Unable to generate a CAPTCHA","Nén moyén di djenerer on CAPTCHA"}. {"Unauthorized","Nén otorijhî"}. {"Unregister","Disdjîstrer"}. {"Update message of the day (don't send)","Mete a djoû l' messaedje do djoû (nén l' evoyî)"}. {"Update message of the day on all hosts (don't send)","Mete a djoû l' messaedje do djoû so tos les lodjoes (nén l' evoyî)"}. {"Update plan","Plan d' metaedje a djoû"}. {"Update ~p","Metaedje a djoû di ~p"}. {"Update script","Sicripe di metaedje a djoû"}. {"Update","Mete a djoû"}. {"Uptime:","Tins dispoy l' enondaedje:"}. {"User JID","JID d' l' uzeu"}. {"User Management","Manaedjaedje des uzeus"}. {"Username:","No d' uzeu:"}. {"Users are not allowed to register accounts so quickly","Les noveas uzeus n' si polèt nén edjîstrer si raddimint"}. {"Users Last Activity","Dierinne activité des uzeus"}. {"Users","Uzeus"}. {"User","Uzeu"}. {"Validate","Valider"}. {"vCard User Search","Calpin des uzeus"}. {"Virtual Hosts","Forveyous sierveus"}. {"Visitors are not allowed to change their nicknames in this room","Les viziteus èn polèt nén candjî leus metous no po ç' såle ci"}. {"Visitors are not allowed to send messages to all occupants","Les viziteus n' polèt nén evoyî des messaedjes a tos les prezints"}. {"Visitor","Viziteu"}. {"Voice request","Dimande di vwès"}. {"Voice requests are disabled in this conference","Les dmandes di vwès sont dismetowes e cisse conferince ci"}. {"Wednesday","mierkidi"}. {"When to send the last published item","Cwand evoyî l' dierin cayet eplaidî"}. {"Whether to allow subscriptions","Si on permete les abounmints"}. {"You have been banned from this room","Vos avoz stî bani di cisse såle ci"}. {"You must fill in field \"Nickname\" in the form","Vos dvoz rimpli l' tchamp «Metou no» dins l' formiulaire"}. {"You need a client that supports x:data and CAPTCHA to register","Vos avoz mezåjhe d' on cliyint ki sopoite x:data eyet CAPTCHA po vs edjîstrer"}. {"You need a client that supports x:data to register the nickname","Vos avoz mezåjhe d' on cliyint ki sopoite x:data po-z edjîstrer l' metou no"}. {"You need an x:data capable client to search","Vos avoz mezåjhe d' on cliyint ki sopoite x:data po fé on cweraedje"}. {"Your active privacy list has denied the routing of this stanza.","Vosse djivêye di privaceye active a rfuzé l' evoyaedje di ç' messaedje ci."}. {"Your contact offline message queue is full. The message has been discarded.","Li cawêye di messaedjes e môde disraloyî di vosse soçon est plinne. Li messaedje a stî tapé å diale."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Vos messaedjes po ~s sont blokés. Po les disbloker, alez vey ~s"}. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/sk.msg���������������������������������������������������������������������0000644�0002322�0002322�00000050173�14154362354�016716� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: ","zmenil(a) tému na: "}. {"A friendly name for the node","Prístupný názov pre uzol"}. {"A password is required to enter this room","Pre vstup do miestnosti je potrebné heslo"}. {"Access denied by service policy","Prístup bol zamietnutý nastavením služby"}. {"Action on user","Operácia aplikovaná na užívateľa"}. {"Add Jabber ID","Pridať Jabber ID"}. {"Add New","Pridať nový"}. {"Add User","Pridať používateľa"}. {"Administration of ","Administrácia "}. {"Administration","Administrácia"}. {"Administrator privileges required","Sú potrebné práva administrátora"}. {"All activity","Všetky aktivity"}. {"All Users","Všetci užívatelia"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Dovoliť tomuto Jabber ID odoberať PubSub uzol?"}. {"Allow users to change the subject","Povoliť užívateľom meniť tému"}. {"Allow users to query other users","Povoliť užívateľom dotazovať sa informácie o iných užívateľoch"}. {"Allow users to send invites","Povoliť používateľom posielanie pozvánok"}. {"Allow users to send private messages","Povoliť užívateľom odosielať súkromné správy"}. {"Allow visitors to change nickname","Návštevníci môžu meniť prezývky"}. {"Allow visitors to send private messages to","Povoliť užívateľom odosielať súkromné správy"}. {"Allow visitors to send status text in presence updates","Návštevníci môžu posielať textové informácie v stavových správach"}. {"Allow visitors to send voice requests","Povoliť používateľom posielanie pozvánok"}. {"Announcements","Oznámenia"}. {"April","Apríl"}. {"August","August"}. {"Backup Management","Správa zálohovania"}. {"Backup to File at ","Záloha do súboru na "}. {"Backup","Zálohovať"}. {"Bad format","Zlý formát"}. {"Birthday","Dátum narodenia"}. {"CAPTCHA web page","Webová stránka CAPTCHA"}. {"Change Password","Zmeniť heslo"}. {"Change User Password","Zmeniť heslo užívateľa"}. {"Characters not allowed:","Nepovolené znaky:"}. {"Chatroom configuration modified","Nastavenie diskusnej miestnosti bolo zmenené"}. {"Chatroom is created","Diskusná miestnosť je vytvorená"}. {"Chatroom is destroyed","Diskusná miestnosť je zrušená"}. {"Chatroom is started","Diskusná miestnosť je obnovená"}. {"Chatroom is stopped","Diskusná miestnosť je pozastavená"}. {"Chatrooms","Diskusné miestnosti"}. {"Choose a username and password to register with this server","Zvolte meno užívateľa a heslo pre registráciu na tomto servere"}. {"Choose storage type of tables","Vyberte typ úložiska pre tabuľky"}. {"Choose whether to approve this entity's subscription.","Zvolte, či chcete povoliť toto odoberanie"}. {"City","Mesto"}. {"Commands","Príkazy"}. {"Conference room does not exist","Diskusná miestnosť neexistuje"}. {"Configuration of room ~s","Konfigurácia miestnosti ~s"}. {"Configuration","Konfigurácia"}. {"Connected Resources:","Pripojené zdroje:"}. {"Country","Krajina"}. {"CPU Time:","Čas procesoru"}. {"Database Tables Configuration at ","Konfigurácia databázových tabuliek "}. {"Database","Databáza"}. {"December","December"}. {"Default users as participants","Užívatelia sú implicitne členmi"}. {"Delete message of the day on all hosts","Zmazať správu dňa na všetkých serveroch"}. {"Delete message of the day","Zmazať správu dňa"}. {"Delete Selected","Zmazať vybrané"}. {"Delete User","Vymazať užívateľa"}. {"Deliver event notifications","Doručiť oznamy o udalosti"}. {"Deliver payloads with event notifications","Doručiť náklad s upozornením na udalosť"}. {"Description:","Popis:"}. {"Disc only copy","Len kópia disku"}. {"Dump Backup to Text File at ","Uložiť zálohu do textového súboru na "}. {"Dump to Text File","Uložiť do textového súboru"}. {"Edit Properties","Editovať vlastnosti"}. {"Either approve or decline the voice request.","Povolte alebo zamietnite žiadosť o Voice."}. {"ejabberd MUC module","ejabberd MUC modul"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe modul"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams modul"}. {"ejabberd vCard module","ejabberd vCard modul"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"Elements","Prvky"}. {"Email","E-mail"}. {"Enable logging","Zapnúť zaznamenávanie histórie"}. {"End User Session","Ukončiť reláciu užívateľa"}. {"Enter nickname you want to register","Zadajte prezývku, ktorú chcete registrovať"}. {"Enter path to backup file","Zadajte cestu k súboru so zálohou"}. {"Enter path to jabberd14 spool dir","Zadajte cestu k jabberd14 spool adresáru"}. {"Enter path to jabberd14 spool file","Zadajte cestu k spool súboru jabberd14"}. {"Enter path to text file","Zadajte cestu k textovému súboru"}. {"Enter the text you see","Zadajte zobrazený text"}. {"Error","Chyba"}. {"Exclude Jabber IDs from CAPTCHA challenge","Nepoužívať CAPTCHA pre nasledujúce Jabber ID"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Exportovať dáta všetkých uživateľov na serveri do súborov PIEFXIS (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Exportovať dáta uživateľov na hostitelovi do súborov PIEFXIS (XEP-0227):"}. {"Failed to extract JID from your voice request approval","Nepodarilo sa nájsť JID v súhlase o Voice."}. {"Family Name","Priezvisko"}. {"February","Február"}. {"Friday","Piatok"}. {"From","Od"}. {"Full Name","Celé meno: "}. {"Get Number of Online Users","Zobraziť počet pripojených užívateľov"}. {"Get Number of Registered Users","Zobraziť počet registrovaných užívateľov"}. {"Get User Last Login Time","Zobraziť čas posledného prihlásenia"}. {"Get User Password","Zobraziť heslo užívateľa"}. {"Get User Statistics","Zobraziť štatistiku užívateľa"}. {"Grant voice to this person?","Prideltiť Voice tejto osobe?"}. {"Group","Skupina"}. {"Groups","Skupiny"}. {"has been banned","bol(a) zablokovaný(á)"}. {"has been kicked because of a system shutdown","bol vyhodený(á) kvôli reštartu systému"}. {"has been kicked because of an affiliation change","bol vyhodený(á) kvôli zmene priradenia"}. {"has been kicked because the room has been changed to members-only","bol vyhodený(á), pretože miestnosť bola vyhradená len pre členov"}. {"has been kicked","bol(a) vyhodený(á) z miestnosti"}. {"Host","Server"}. {"If you don't see the CAPTCHA image here, visit the web page.","Pokiaľ nevidíte obrázok CAPTCHA, navštívte webovú stránku."}. {"Import Directory","Import adresára"}. {"Import File","Import súboru"}. {"Import user data from jabberd14 spool file:","Importovať dáta užívateľov z jabberd14 spool súboru:"}. {"Import User from File at ","Importovať užívateľa zo súboru na "}. {"Import users data from a PIEFXIS file (XEP-0227):","Importovat dáta užívateľov zo súboru PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Importovať dáta užívateľov z jabberd14 spool adresára:"}. {"Import Users from Dir at ","Importovať užívateľov z adresára na "}. {"Import Users From jabberd14 Spool Files","Importovať užívateľov z jabberd14 spool súborov"}. {"Improper message type","Nesprávny typ správy"}. {"Incorrect password","Nesprávne heslo"}. {"IP addresses","IP adresa"}. {"is now known as","sa premenoval(a) na"}. {"It is not allowed to send private messages of type \"groupchat\"","Nie je dovolené odoslanie súkromnej správy typu \"Skupinová správa\" "}. {"It is not allowed to send private messages to the conference","Nie je povolené odosielať súkromné správy do konferencie"}. {"It is not allowed to send private messages","Nieje povolené posielať súkromné správy"}. {"Jabber ID","Jabber ID"}. {"January","Január"}. {"joins the room","vstúpil(a) do miestnosti"}. {"July","Júl"}. {"June","Jún"}. {"Last Activity","Posledná aktivita"}. {"Last login","Posledné prihlásenie"}. {"Last month","Posledný mesiac"}. {"Last year","Posledný rok"}. {"leaves the room","odišiel(a) z miestnosti"}. {"Low level update script","Nízkoúrovňový aktualizačný skript"}. {"Make participants list public","Nastaviť zoznam zúčastnených ako verejný"}. {"Make room CAPTCHA protected","Chrániť miestnosť systémom CAPTCHA"}. {"Make room members-only","Nastaviť miestnosť len pre členov"}. {"Make room moderated","Nastaviť miestnosť ako moderovanú"}. {"Make room password protected","Chrániť miestnosť heslom"}. {"Make room persistent","Nastaviť miestnosť ako trvalú"}. {"Make room public searchable","Nastaviť miestnosť ako verejne prehľadávateľnú"}. {"March","Marec"}. {"Max # of items to persist","Maximálny počet položiek, ktoré je možné natrvalo uložiť"}. {"Max payload size in bytes","Maximálny náklad v bajtoch"}. {"Maximum Number of Occupants","Počet účastníkov"}. {"May","Máj"}. {"Members:","Členovia:"}. {"Membership is required to enter this room","Pre vstup do miestnosti je potrebné byť členom"}. {"Memory","Pamäť"}. {"Message body","Telo správy"}. {"Middle Name","Prostredné meno: "}. {"Minimum interval between voice requests (in seconds)","Minimum interval between voice requests (in seconds)"}. {"Moderator privileges required","Sú potrebné práva moderátora"}. {"Modified modules","Modifikované moduly"}. {"Monday","Pondelok"}. {"Name","Meno"}. {"Name:","Meno:"}. {"Never","Nikdy"}. {"New Password:","Nové heslo:"}. {"Nickname Registration at ","Registrácia prezývky na "}. {"Nickname ~s does not exist in the room","Prezývka ~s v miestnosti neexistuje"}. {"Nickname","Prezývka"}. {"No body provided for announce message","Správa neobsahuje text"}. {"No Data","Žiadne dáta"}. {"No limit","Bez limitu"}. {"Node ID","ID uzlu"}. {"Node not found","Uzol nenájdený"}. {"Nodes","Uzly"}. {"None","Nič"}. {"Not Found","Nebol nájdený"}. {"Notify subscribers when items are removed from the node","Upozorniť prihlásených používateľov na odstránenie položiek z uzlu"}. {"Notify subscribers when the node configuration changes","Upozorniť prihlásených používateľov na zmenu nastavenia uzlu"}. {"Notify subscribers when the node is deleted","Upozorniť prihlásených používateľov na zmazanie uzlu"}. {"November","November"}. {"Number of occupants","Počet zúčastnených"}. {"Number of online users","Počet online užívateľov"}. {"Number of registered users","Počet registrovaných užívateľov"}. {"October","Október"}. {"Offline Messages","Offline správy"}. {"Offline Messages:","Offline správy"}. {"OK","OK"}. {"Old Password:","Staré heslo:"}. {"Online Users:","Online používatelia:"}. {"Online Users","Online užívatelia"}. {"Online","Online"}. {"Only deliver notifications to available users","Doručovať upozornenia len aktuálne prihláseným používateľom"}. {"Only moderators and participants are allowed to change the subject in this room","Len moderátori a zúčastnený majú povolené meniť tému tejto miestnosti"}. {"Only moderators are allowed to change the subject in this room","Len moderátori majú povolené meniť tému miestnosti"}. {"Only moderators can approve voice requests","Len moderátori môžu schváliť žiadosť o Voice"}. {"Only occupants are allowed to send messages to the conference","Len členovia majú povolené zasielať správy do konferencie"}. {"Only occupants are allowed to send queries to the conference","Len členovia majú povolené dotazovať sa o konferencii"}. {"Only service administrators are allowed to send service messages","Iba správcovia služby majú povolené odosielanie servisných správ"}. {"Organization Name","Meno organizácie: "}. {"Organization Unit","Organizačná jednotka: "}. {"Outgoing s2s Connections","Odchádzajúce s2s spojenia"}. {"Outgoing s2s Connections:","Odchádzajúce s2s spojenia:"}. {"Owner privileges required","Sú vyžadované práva vlastníka"}. {"Packet","Paket"}. {"Password Verification","Overenie hesla"}. {"Password Verification:","Overenie hesla"}. {"Password","Heslo"}. {"Password:","Heslo:"}. {"Path to Dir","Cesta k adresáru"}. {"Path to File","Cesta k súboru"}. {"Pending","Čakajúce"}. {"Period: ","Čas:"}. {"Persist items to storage","Uložiť položky natrvalo do úložiska"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Prosím, berte na vedomie, že tieto nastavenia zázálohujú iba zabudovnú Mnesia databázu. Ak používate ODBC modul, musíte zálohovať vašu SQL databázu separátne."}. {"Please, wait for a while before sending new voice request","Prosím počkate, predtým než pošlete novú žiadosť o Voice"}. {"Pong","Pong"}. {"Present real Jabber IDs to","Zobrazovať skutočné Jabber ID"}. {"private, ","súkromná, "}. {"Publish-Subscribe","Publish-Subscribe"}. {"PubSub subscriber request","Žiadosť odberateľa PubSub"}. {"Purge all items when the relevant publisher goes offline","Odstrániť všetky relevantné položky, keď užívateľ prejde do módu offline"}. {"Queries to the conference members are not allowed in this room","Dotazovať sa o členoch nie je v tejto miestnosti povolené"}. {"RAM and disc copy","Kópia RAM a disku"}. {"RAM copy","Kópia RAM"}. {"Really delete message of the day?","Skutočne zmazať správu dňa?"}. {"Recipient is not in the conference room","Príjemca sa nenachádza v konferenčnej miestnosti"}. {"Registered Users","Registrovaní používatelia"}. {"Registered Users:","Registrovaní používatelia:"}. {"Register","Zoznam kontaktov"}. {"Remote copy","Vzdialená kópia"}. {"Remove All Offline Messages","Odstrániť všetky offline správy"}. {"Remove User","Odstrániť užívateľa"}. {"Remove","Odstrániť"}. {"Replaced by new connection","Nahradené novým spojením"}. {"Resources","Zdroje"}. {"Restart Service","Reštartovať službu"}. {"Restart","Reštart"}. {"Restore Backup from File at ","Obnoviť zálohu zo súboru na "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Obnoviť binárnu zálohu pri nasledujúcom reštarte ejabberd (vyžaduje menej pamäte)"}. {"Restore binary backup immediately:","Okamžite obnoviť binárnu zálohu:"}. {"Restore plain text backup immediately:","Okamžite obnoviť zálohu z textového súboru:"}. {"Restore","Obnoviť"}. {"Room Configuration","Nastavenia miestnosti"}. {"Room creation is denied by service policy","Vytváranie miestnosti nie je povolené"}. {"Room description","Popis miestnosti"}. {"Room Occupants","Ľudí v miestnosti"}. {"Room title","Názov miestnosti"}. {"Roster groups allowed to subscribe","Skupiny kontaktov, ktoré môžu odoberať"}. {"Roster size","Počet kontaktov v zozname"}. {"RPC Call Error","Chyba RPC volania"}. {"Running Nodes","Bežiace uzly"}. {"Saturday","Sobota"}. {"Script check","Kontrola skriptu"}. {"Search Results for ","Hľadať výsledky pre "}. {"Search users in ","Hľadať užívateľov v "}. {"Send announcement to all online users on all hosts","Odoslať oznam všetkým online používateľom na všetkých serveroch"}. {"Send announcement to all online users","Odoslať zoznam všetkým online používateľom"}. {"Send announcement to all users on all hosts","Poslať oznámenie všetkým užívateľom na všetkých serveroch"}. {"Send announcement to all users","Odoslať oznam všetkým používateľom"}. {"September","September"}. {"Set message of the day and send to online users","Nastaviť správu dňa a odoslať ju online používateľom"}. {"Set message of the day on all hosts and send to online users","Nastaviť správu dňa na všetkých serveroch a poslať ju online užívateľom"}. {"Shared Roster Groups","Skupiny pre zdieľaný zoznam kontaktov"}. {"Show Integral Table","Zobraziť kompletnú tabuľku"}. {"Show Ordinary Table","Zobraziť bežnú tabuľku"}. {"Shut Down Service","Vypnúť službu"}. {"Specify the access model","Uveďte model prístupu"}. {"Specify the event message type","Uveďte typ pre správu o udalosti"}. {"Specify the publisher model","Špecifikovať model publikovania"}. {"Statistics of ~p","Štatistiky ~p"}. {"Statistics","Štatistiky"}. {"Stopped Nodes","Zastavené uzly"}. {"Stop","Zastaviť"}. {"Storage Type","Typ úložiska"}. {"Store binary backup:","Uložiť binárnu zálohu:"}. {"Store plain text backup:","Uložiť zálohu do textového súboru:"}. {"Subject","Predmet"}. {"Submit","Odoslať"}. {"Submitted","Odoslané"}. {"Subscriber Address","Adresa odberateľa"}. {"Subscription","Prihlásenie"}. {"Sunday","Nedeľa"}. {"That nickname is already in use by another occupant","Prezývka je už používaná iným členom"}. {"That nickname is registered by another person","Prezývka je už zaregistrovaná inou osobou"}. {"The CAPTCHA is valid.","Platná CAPTCHA."}. {"The CAPTCHA verification has failed","Overenie pomocou CAPTCHA zlihalo"}. {"The collections with which a node is affiliated","Kolekcie asociované s uzlom"}. {"The password is too weak","heslo je"}. {"the password is","heslo je"}. {"There was an error creating the account: ","Pri vytváraní účtu nastala chyba: "}. {"There was an error deleting the account: ","Pri rušení účtu nastala chyba:"}. {"This room is not anonymous","Táto miestnosť nie je anonymná"}. {"Thursday","Štvrtok"}. {"Time delay","Časový posun"}. {"Time","Čas"}. {"Too many CAPTCHA requests","Príliš veľa žiadostí o CAPTCHA"}. {"To","Pre"}. {"Traffic rate limit is exceeded","Bol prekročený prenosový limit"}. {"Transactions Aborted:","Transakcie zrušená"}. {"Transactions Committed:","Transakcie potvrdená"}. {"Transactions Logged:","Transakcie zaznamenaná"}. {"Transactions Restarted:","Transakcie reštartovaná"}. {"Tuesday","Utorok"}. {"Unable to generate a CAPTCHA","Nepodarilo sa vygenerovat CAPTCHA"}. {"Unauthorized","Neautorizovaný"}. {"Unregister","Zrušiť účet"}. {"Update message of the day (don't send)","Aktualizovať správu dňa (neodosielať)"}. {"Update message of the day on all hosts (don't send)","Upraviť správu dňa na všetkých serveroch"}. {"Update plan","Aktualizovať plán"}. {"Update script","Aktualizované skripty"}. {"Update","Aktualizovať"}. {"Uptime:","Uptime:"}. {"User JID","Používateľ "}. {"User Management","Správa užívateľov"}. {"Username:","IRC prezývka"}. {"Users are not allowed to register accounts so quickly","Nieje dovolené vytvárať účty tak rýchlo po sebe"}. {"Users Last Activity","Posledná aktivita používateľa"}. {"Users","Používatelia"}. {"User","Užívateľ"}. {"Validate","Overiť"}. {"vCard User Search","Hľadať užívateľov vo vCard"}. {"Virtual Hosts","Virtuálne servery"}. {"Visitors are not allowed to change their nicknames in this room","V tejto miestnosti nieje povolené meniť prezývky"}. {"Visitors are not allowed to send messages to all occupants","Návštevníci nemajú povolené zasielať správy všetkým prihláseným do konferencie"}. {"Voice requests are disabled in this conference","Žiadosti o Voice nie sú povolené v tejto konferencii"}. {"Voice request","Žiadosť o Voice"}. {"Wednesday","Streda"}. {"When to send the last published item","Kedy odoslať posledne publikovanú položku"}. {"Whether to allow subscriptions","Povoliť prihlasovanie"}. {"You have been banned from this room","Boli ste vylúčený z tejto miestnosti"}. {"You must fill in field \"Nickname\" in the form","Musíte vyplniť políčko \"Prezývka\" vo formulári"}. {"You need a client that supports x:data and CAPTCHA to register","Na registráciu prezývky potrebujete klienta podporujúceho z x:data"}. {"You need a client that supports x:data to register the nickname","Na registráciu prezývky potrebujete klienta podporujúceho z x:data"}. {"You need an x:data capable client to search","Na vyhľadávanie potrebujete klienta podporujúceho x:data"}. {"Your active privacy list has denied the routing of this stanza.","Aktívny list súkromia zbránil v smerovaní tejto stanzy."}. {"Your contact offline message queue is full. The message has been discarded.","Fronta offline správ tohoto kontaktu je plná. Správa bola zahodená."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Správa určená pre ~s bola zablokovaná. Oblokovať ju môžete na ~s"}. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/sq.msg���������������������������������������������������������������������0000644�0002322�0002322�00000046160�14154362354�016725� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," ka caktuar si subjekt: "}. {"# participants","# pjesëmarrës"}. {"A description of the node","Përshkrim i nyjës"}. {"A friendly name for the node","Emër miqësor për nyjën"}. {"A password is required to enter this room","Lypset fjalëkalim për të hyrë në këtë dhomë"}. {"A Web Page","Faqe Web"}. {"Accept","Pranoje"}. {"Account doesn't exist","Llogaria s’ekziston"}. {"Add Jabber ID","Shtoni ID Jabber"}. {"Add New","Shtoni të Ri"}. {"Add User","Shtoni Përdorues"}. {"Administration of ","Administrim i "}. {"Administration","Administrim"}. {"Administrator privileges required","Lyp privilegje përgjegjësi"}. {"All activity","Krejt veprimtaria"}. {"All Users","Krejt Përdoruesit"}. {"Allow subscription","Lejo pajtim"}. {"Allow users to query other users","Lejojuni përdoruesve të kërkojnë për përdorues të tjerë"}. {"Allow users to send invites","Lejojuni përdoruesve të dërgojnë ftesa"}. {"Allow users to send private messages","Lejojuni përdoruesve të dërgojnë mesazhe private"}. {"Allow visitors to change nickname","Lejojuni përdoruesve të ndryshojnë nofkë"}. {"Allow visitors to send private messages to","Lejojuni përdoruesve të dërgojnë mesazhe private te"}. {"Announcements","Lajmërime"}. {"Answer to a question","Përgjigjuni një pyetje"}. {"Anyone may publish","Gjithkush mund të publikojë"}. {"Anyone with Voice","Cilido me Zë"}. {"Anyone","Cilido"}. {"April","Prill"}. {"Attribute 'channel' is required for this request","Atributi 'channel' është i domosdoshëm për këtë kërkesë"}. {"Attribute 'jid' is not allowed here","Atributi 'jid' s’lejohet këtu"}. {"Attribute 'node' is not allowed here","Atributi 'node' s’lejohet këtu"}. {"August","Gusht"}. {"Automatic node creation is not enabled","S’është aktivizuar krijimi automatik i nyjes"}. {"Backup Management","Administrim Kopjeruajtjesh"}. {"Backup of ~p","Kopjeruajtje e ~p"}. {"Backup to File at ","Kopjeruaje te Kartelë në "}. {"Backup","Kopjeruajtje"}. {"Bad format","Format i gabuar"}. {"Birthday","Datëlindje"}. {"Both the username and the resource are required","Janë të domosdoshëm të dy, emri i përdoruesit dhe burimi"}. {"Cannot remove active list","S’hiqet dot lista aktive"}. {"Cannot remove default list","S’hiqet dot lista parazgjedhje"}. {"CAPTCHA web page","Faqe web e CAPTCHA-s"}. {"Change Password","Ndryshoni Fjalëkalimin"}. {"Change User Password","Ndryshoni Fjalëkalim Përdoruesi"}. {"Changing password is not allowed","Nuk lejohet ndryshimi i fjalëkalimit"}. {"Changing role/affiliation is not allowed","Nuk lejohet ndryshim roli/përkatësie"}. {"Channel already exists","Kanali ekziston tashmë"}. {"Channel does not exist","Kanali s’ekziston"}. {"Channels","Kanale"}. {"Characters not allowed:","Shenja të palejuara:"}. {"Chatroom configuration modified","Ndryshoi formësimi i dhomës së fjalosjeve"}. {"Chatroom is created","Dhoma e fjalosjes u krijua"}. {"Chatroom is destroyed","Dhoma e fjalosjes u asgjësua"}. {"Chatroom is started","Dhoma e fjalosjes u nis"}. {"Chatroom is stopped","Dhoma e fjalosjes u ndal"}. {"Chatrooms","Dhoma fjalosjeje"}. {"Choose a username and password to register with this server","Zgjidhni një emër përdoruesi dhe fjalëkalim për ta regjistruar me këtë shërbyes"}. {"Choose storage type of tables","Zgjidhni lloj depozitimi tableash"}. {"City","Qytet"}. {"Commands","Urdhra"}. {"Conference room does not exist","Dhoma e konferencës s’ekziston"}. {"Configuration of room ~s","Formësim i dhomë ~s"}. {"Configuration","Formësim"}. {"Country","Vend"}. {"CPU Time:","Kohë CPU-je:"}. {"Current Discussion Topic","Tema e Tanishme e Diskutimit"}. {"Database failure","Dështim baze të dhënash"}. {"Database Tables at ~p","Tabela Baze të Dhënash te ~p"}. {"Database Tables Configuration at ","Formësim Tabelash Baze të Dhënash te "}. {"Database","Bazë të dhënash"}. {"December","Dhjetor"}. {"Delete content","Fshini lëndë"}. {"Delete message of the day","Fshini mesazhin e ditës"}. {"Delete Selected","Fshi të Përzgjedhurin"}. {"Delete table","Fshini tabelën"}. {"Delete User","Fshi Përdorues"}. {"Deliver event notifications","Dërgo njoftime aktesh"}. {"Description:","Përshkrim:"}. {"Disc only copy","Kopje vetëm në disk"}. {"Duplicated groups are not allowed by RFC6121","Grupe të përsëdytur s’lejohen nga RFC6121"}. {"Edit Properties","Përpunoni Veti"}. {"ejabberd","ejabberd"}. {"Elements","Elementë"}. {"Email Address","Adresë Email"}. {"Email","Email"}. {"Enable logging","Aktivizo regjistrim"}. {"Enable message archiving","Aktivizoni arkivim mesazhesh"}. {"Enter path to backup file","Jepni shteg për te kartelë kopjeruajtje"}. {"Enter path to text file","Jepni shteg për te kartelë tekst"}. {"Enter the text you see","Jepni tekstin që shihni"}. {"Error","Gabim"}. {"External component failure","Dështim përbërësi të jashtëm"}. {"External component timeout","Mbarim kohe për përbërës të jashtëm"}. {"Failed to parse HTTP response","S’u arrit të përtypet përgjigje HTTP"}. {"Failed to process option '~s'","S’u arrit të përpunohej mundësia '~s'"}. {"Family Name","Mbiemër"}. {"FAQ Entry","Zë PBR-sh"}. {"February","Shkurt"}. {"File larger than ~w bytes","Kartelë më e madhe se ~w bajte"}. {"Fill in the form to search for any matching XMPP User","Plotësoni formularin që të kërkohet për çfarëdo përdoruesi XMPP me përputhje"}. {"Friday","E premte"}. {"From ~ts","Nga ~ts"}. {"From","Nga"}. {"Full List of Room Admins","Listë e Plotë Përgjegjësish Dhome"}. {"Full List of Room Owners","Listë e Plotë të Zotësh Dhome"}. {"Full Name","Emër i Plotë"}. {"Get Number of Online Users","Merr Numër Përdoruesish Në Linjë"}. {"Get Number of Registered Users","Merr Numër Përdoruesish të Regjistruar"}. {"Get User Password","Merr Fjalëkalim Përdoruesi"}. {"Get User Statistics","Merr Statistika Përdoruesi"}. {"Given Name","Emër"}. {"Grant voice to this person?","T’i akordohet zë këtij personi?"}. {"Group","Grup"}. {"Groups that will be displayed to the members","Grupe që do t’u shfaqen anëtarëve"}. {"Groups","Grupe"}. {"has been banned","është dëbuar"}. {"has been kicked","është përzënë"}. {"Host unknown","Strehë e panjohur"}. {"Host","Strehë"}. {"HTTP File Upload","Ngarkim Kartelash HTTP"}. {"Idle connection","Lidhje e plogësht"}. {"Import Directory","Importoni Drejtori"}. {"Import File","Importoni Kartelë"}. {"Import User from File at ","Importo Përdorues prej Kartele te "}. {"Import Users from Dir at ","Importo Përdorues nga Drejtori te "}. {"Improper message type","Lloj i pasaktë mesazhesh"}. {"Incoming s2s Connections:","Lidhje s2s Ardhëse:"}. {"Incorrect CAPTCHA submit","Parashtim Captcha-je të pasaktë"}. {"Incorrect password","Fjalëkalim i pasaktë"}. {"Incorrect value of 'action' attribute","Vlerë e pavlefshme atributi 'action'"}. {"Insufficient privilege","Privilegj i pamjaftueshëm"}. {"Internal server error","Gabim i brendshëm shërbyesi"}. {"Invalid node name","Emër i pavlefshëm nyjeje"}. {"Invalid 'previd' value","Vlerë e pavlefshme 'previd'"}. {"Invitations are not allowed in this conference","Në këtë konferencë nuk lejohen ftesa"}. {"IP addresses","Adresa IP"}. {"is now known as","tani njihet si"}. {"It is not allowed to send private messages to the conference","Nuk lejohet të dërgohen mesazhe private te konferenca"}. {"It is not allowed to send private messages","Nuk lejohet të dërgohen mesazhe private"}. {"Jabber ID","ID Jabber"}. {"January","Janar"}. {"JID normalization failed","Normalizimi JID dështoi"}. {"joins the room","hyn te dhoma"}. {"July","Korrik"}. {"June","Qershor"}. {"Just created","Të sapokrijuara"}. {"Label:","Etiketë:"}. {"Last Activity","Veprimtaria e Fundit"}. {"Last login","Hyrja e fundit"}. {"Last message","Mesazhi i fundit"}. {"Last month","Muaji i fundit"}. {"Last year","Viti i shkuar"}. {"leaves the room","del nga dhoma"}. {"List of rooms","Listë dhomash"}. {"Logging","Regjistrim"}. {"Make participants list public","Bëje publike listën e pjesëmarrësve"}. {"Make room CAPTCHA protected","Bëje dhomën të mbrojtur me CAPTCHA"}. {"Make room members-only","Bëje dhomën vetëm për anëtarët"}. {"Make room moderated","Bëje dhomën të moderuar"}. {"Make room password protected","Bëje dhomën të mbrojtur me fjalëkalim"}. {"Make room persistent","Bëje dhomën të qëndrueshme"}. {"Make room public searchable","Bëje dhomën të kërkueshme publikisht"}. {"Malformed username","Faqerojtës i keqformuar"}. {"March","Mars"}. {"Max payload size in bytes","Madhësi maksimum ngarkese në bajte"}. {"Maximum file size","Madhësi maksimum kartelash"}. {"Maximum Number of Occupants","Numër Maksimum të Pranishmish"}. {"May","Maj"}. {"Members not added (inexistent vhost!): ","S’u shtuan anëtarë (vhost joekzistuese!): "}. {"Members:","Anëtarë:"}. {"Membership is required to enter this room","Lypset anëtarësim për të hyrë në këtë dhomë"}. {"Memory","Kujtesë"}. {"Message body","Lëndë mesazhi"}. {"Messages from strangers are rejected","Mesazhet prej të panjohurish hidhen tej"}. {"Messages of type headline","Mesazhe të llojit titull"}. {"Messages of type normal","Mesazhe të llojit normal"}. {"Middle Name","Emër i Dytë"}. {"Moderator privileges required","Lypset privilegj moderatori"}. {"Moderator","Moderator"}. {"Moderators Only","Vetëm Moderatorët"}. {"Module failed to handle the query","Moduli s’arrii të trajtonte kërkesën"}. {"Monday","E hënë"}. {"Multicast","Multikast"}. {"Multi-User Chat","Fjalosje Me Shumë Përdorues Njëherësh"}. {"Name","Emër"}. {"Name:","Emër:"}. {"Natural-Language Room Name","Emër Dhome Në Gjuhë Natyrale"}. {"Never","Kurrë"}. {"New Password:","Fjalëkalim i Ri:"}. {"Nickname can't be empty","Nofka s’mund të jetë e zbrazët"}. {"Nickname Registration at ","Regjistrim Nofke te "}. {"Nickname ~s does not exist in the room","Në këtë dhomë s’ekziston nofka ~s"}. {"Nickname","Nofkë"}. {"No address elements found","S’u gjetën elementë adrese"}. {"No addresses element found","S’u gjetën elementë adresash"}. {"No child elements found","S’u gjetën elementë pjella"}. {"No Data","S’ka të Dhëna"}. {"No items found in this query","S’u gjetën objekte në këtë kërkesë"}. {"No limit","Pa kufi"}. {"No node specified","S’u përcaktua nyjë"}. {"No pending subscriptions found","S’u gjetën pajtime pezull"}. {"No privacy list with this name found","S’u gjet listë privatësie me atë emër"}. {"No running node found","S’u gjet nyjë në funksionim"}. {"No services available","S’ka shërbime të gatshme"}. {"No statistics found for this item","S’u gjetën statistika për këtë objekt"}. {"Nobody","Askush"}. {"Node already exists","Nyja ekziston tashmë"}. {"Node ID","ID Nyjeje"}. {"Node index not found","S’u gjet tregues nyje"}. {"Node not found","S’u gjet nyjë"}. {"Node ~p","Nyjë ~p"}. {"Nodes","Nyja"}. {"None","Asnjë"}. {"Not allowed","E palejuar"}. {"Not Found","S’u Gjet"}. {"Not subscribed","Jo i pajtuar"}. {"November","Nëntor"}. {"Number of answers required","Numër përgjigjesh të domosdoshme"}. {"Number of occupants","Numër të pranishmish"}. {"Number of Offline Messages","Numër Mesazhesh Jo Në Linjë"}. {"Number of online users","Numër përdoruesish në linjë"}. {"Number of registered users","Numër përdoruesish të regjistruar"}. {"Occupants are allowed to invite others","Të pranishmëve u është lejuar të ftojnë të tjerë"}. {"Occupants May Change the Subject","Të pranishmit Mund të Ndryshojnë Subjektin"}. {"October","Tetor"}. {"Offline Messages","Mesazhe Jo Në Linjë"}. {"Offline Messages:","Mesazhe Jo Në Linjë:"}. {"OK","OK"}. {"Old Password:","Fjalëkalimi i Vjetër:"}. {"Online Users","Përdorues Në Linjë"}. {"Online Users:","Përdorues Në Linjë:"}. {"Online","Në linjë"}. {"Only admins can see this","Këtë mund ta shohin vetëm përgjegjësit"}. {"Only deliver notifications to available users","Dorëzo njoftime vetëm te përdoruesit e pranishëm"}. {"Only occupants are allowed to send messages to the conference","Vetëm të pranishmëve u lejohet të dërgojnë mesazhe te konferenca"}. {"Only publishers may publish","Vetëm botuesit mund të botojnë"}. {"Organization Name","Emër Enti"}. {"Organization Unit","Njësi Organizative"}. {"Outgoing s2s Connections","Lidhje s2s Ikëse"}. {"Outgoing s2s Connections:","Lidhje s2s Ikëse:"}. {"Owner privileges required","Lypset privilegje të zoti"}. {"Packet","Paketë"}. {"Participant","Pjesëmarrës"}. {"Password Verification","Verifikim Fjalëkalimi"}. {"Password Verification:","Verifikim Fjalëkalimi:"}. {"Password","Fjalëkalim"}. {"Password:","Fjalëkalim:"}. {"Path to Dir","Shteg për te Drejtori"}. {"Path to File","Shteg për te Kartelë"}. {"Payload type","Lloj ngarkese"}. {"Pending","Pezull"}. {"Period: ","Periudhë: "}. {"Ping","Ping"}. {"Pong","Pong"}. {"Previous session not found","S’u gjet sesion i mëparshëm"}. {"Previous session PID is dead","PID e sesionit të mëparshëm është e asgjësuar"}. {"Previous session timed out","Sesionit të mëparshëm i mbaroi koha"}. {"private, ","private, "}. {"RAM and disc copy","RAM dhe kopje në disk"}. {"RAM copy","Kopje në RAM"}. {"Really delete message of the day?","Të fshihet vërtet mesazhi i ditës?"}. {"Recipient is not in the conference room","Pjesëmarrësi s’është në dhomën e konferencës"}. {"Register an XMPP account","Regjistroni një llogari XMPP"}. {"Registered Users","Përdorues të Regjistruar"}. {"Registered Users:","Përdorues të Regjistruar:"}. {"Register","Regjistrohuni"}. {"Remote copy","Kopje e largët"}. {"Remove All Offline Messages","Hiq Krejt Mesazhet Jo Në Linjë"}. {"Remove User","Hiqeni Përdoruesin"}. {"Remove","Hiqe"}. {"Replaced by new connection","Zëvendësuar nga lidhje e re"}. {"Request has timed out","Kërkesës i mbaroi koha"}. {"Request is ignored","Kërkesa u shpërfill"}. {"Requested role","Rol i domosdoshëm"}. {"Resources","Burime"}. {"Restart Service","Rinise Shërbimin"}. {"Restart","Rinise"}. {"Restore","Riktheje"}. {"Roles that May Send Private Messages","Role që Mund të Dërgojnë Mesazhe Private"}. {"Room Configuration","Formësim Dhome"}. {"Room description","Përshkrim i dhomës"}. {"Room Occupants","Të pranishëm Në Dhomë"}. {"Room title","Titull dhome"}. {"RPC Call Error","Gabim Thirrjeje RPC"}. {"Running Nodes","Nyje Në Punë"}. {"Saturday","E shtunë"}. {"Search from the date","Kërko nga data"}. {"Search Results for ","Përfundime Kërkimi për "}. {"Search the text","Kërkoni për tekst"}. {"Search until the date","Kërko deri më datën"}. {"Search users in ","Kërko përdorues te "}. {"Select All","Përzgjidheni Krejt"}. {"Send announcement to all users","Dërgo njoftim krejt përdoruesve"}. {"September","Shtator"}. {"Server:","Shërbyes:"}. {"Show Integral Table","Shfaq Tabelë të Plotë"}. {"Show Ordinary Table","Shfaq Tabelë të Rëndomtë"}. {"Shut Down Service","Fike Shërbimin"}. {"Specify the access model","Specifikoni model hyrjeje"}. {"Specify the event message type","Përcaktoni llojin e mesazhit për aktin"}. {"Specify the publisher model","Përcaktoni model botuesi"}. {"Statistics of ~p","Statistika për ~p"}. {"Statistics","Statistika"}. {"Stop","Ndale"}. {"Stopped Nodes","Nyja të Ndalura"}. {"Storage Type","Lloj Depozitimi"}. {"Subject","Subjekti"}. {"Submit","Parashtrojeni"}. {"Submitted","Parashtruar"}. {"Subscriber Address","Adresë e Pajtimtarit"}. {"Subscription","Pajtim"}. {"Sunday","E diel"}. {"The account already exists","Ka tashmë një llogari të tillë"}. {"The account was not unregistered","Llogaria s’qe çregjistruar"}. {"The CAPTCHA is valid.","Kaptça është e vlefshme."}. {"The default language of the node","Gjuha parazgjedhje e nyjës"}. {"The feature requested is not supported by the conference","Veçoria e kërkuar nuk mbulohen nga konferenca"}. {"The JID of the node creator","JID i krijjuesit të nyjës"}. {"The name of the node","Emri i nyjës"}. {"The number of subscribers to the node","Numri i pajtimtarëve te nyja"}. {"The number of unread or undelivered messages","Numri i mesazheve të palexuar ose të padorëzuar"}. {"The password is too weak","Fjalëkalimi është shumë i dobët"}. {"the password is","fjalëkalimi është"}. {"The password of your XMPP account was successfully changed.","Fjalëkalimi i llogarisë tuaj XMPP u ndryshua me sukses."}. {"The password was not changed","Fjalëkalimi s’u ndryshua"}. {"The passwords are different","Fjalëkalimet janë të ndryshëm"}. {"The sender of the last received message","Dërguesi i mesazhit të fundit të marrë"}. {"The username is not valid","Emri i përdoruesit s’është i vlefshëm"}. {"There was an error changing the password: ","Pati një gabim në ndryshimin e fjalëkalimit: "}. {"There was an error creating the account: ","Pati një gabim në krijimin e llogarisë: "}. {"This room is not anonymous","Kjo dhomë s’është anonime"}. {"Thursday","E enjte"}. {"Time delay","Vonesë kohore"}. {"Time","Kohë"}. {"Too many CAPTCHA requests","Shumë kërkesa ndaj CAPTCHA-s"}. {"Too many child elements","Shumë elementë pjella"}. {"Too many <item/> elements","Shumë elementë <item/>"}. {"Too many <list/> elements","Shumë elementë <list/>"}. {"Too many users in this conference","Shumë përdorues në këtë konferencë"}. {"Total rooms","Dhoma gjithsej"}. {"Tuesday","E martë"}. {"Unable to generate a CAPTCHA","S’arrihet të prodhohet një CAPTCHA"}. {"Unauthorized","E paautorizuar"}. {"Unexpected action","Veprim i papritur"}. {"Unregister an XMPP account","Çregjistroni një llogari XMPP"}. {"Unregister","Çregjistrohuni"}. {"Unselect All","Shpërzgjidhi Krejt"}. {"Unsupported version","Version i pambuluar"}. {"Update","Përditësoje"}. {"Uptime:","Kohëpunim:"}. {"User already exists","Ka tashmë një përdorues të tillë"}. {"User JID","JID përdoruesi"}. {"User (jid)","Përdorues (jid)"}. {"User Management","Administrim Përdoruesish"}. {"User removed","Përdoruesi u hoq"}. {"User session not found","S’u gjet sesion përdoruesi"}. {"User session terminated","Sesioni i përdoruesit përfundoi"}. {"Username:","Emër përdoruesi:"}. {"User","Përdorues"}. {"Users Last Activity","Veprimtaria e Fundit Nga Përdorues"}. {"Users","Përdorues"}. {"Validate","Vleftësoje"}. {"View Queue","Shihni Radhën"}. {"Virtual Hosts","Streha Virtuale"}. {"Visitor","Vizitor"}. {"Wednesday","E mërkurë"}. {"When a new subscription is processed","Kur përpunohet një pajtim i ri"}. {"Whether to allow subscriptions","Nëse duhen lejuar apo jo pajtime"}. {"Wrong parameters in the web formulary","Parametër i gabuar në formular web"}. {"XMPP Account Registration","Regjistrim Llogarish XMPP"}. {"XMPP Domains","Përkatësi XMPP"}. {"You are not joined to the channel","S’keni hyrë te kanali"}. {"You have been banned from this room","Jeni dëbuar prej kësaj dhome"}. {"You have joined too many conferences","Keni hyrë në shumë konferenca"}. {"Your XMPP account was successfully registered.","Llogaria juaj XMPP u regjistrua me sukses."}. {"Your XMPP account was successfully unregistered.","Llogaria juaj XMPP u çregjistrua me sukses."}. {"You're not allowed to create nodes","S’keni leje të krijoni nyja"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/th.msg���������������������������������������������������������������������0000644�0002322�0002322�00000054546�14154362354�016724� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" has set the subject to: "," ตั้งหัวข้อว่า: "}. {"Access denied by service policy","การเข้าถึงถูกปฏิเสธโดยนโยบายการบริการ"}. {"Action on user","การดำเนินการกับผู้ใช้"}. {"Add Jabber ID","เพิ่ม Jabber ID"}. {"Add New","เพิ่มผู้ใช้ใหม่"}. {"Add User","เพิ่มผู้ใช้"}. {"Administration of ","การดูแล "}. {"Administration","การดูแล"}. {"Administrator privileges required","ต้องมีสิทธิพิเศษของผู้ดูแลระบบ"}. {"All activity","กิจกรรมทั้งหมด"}. {"All Users","ผู้ใช้ทั้งหมด"}. {"Allow this Jabber ID to subscribe to this pubsub node?","อนุญาตให้ Jabber ID นี้เข้าร่วมเป็นสมาชิกของโหนด pubsub หรือไม่"}. {"Allow users to query other users","อนุญาตให้ผู้ใช้ถามคำถามกับผู้ใช้คนอื่นๆ ได้"}. {"Allow users to send invites","อนุญาตให้ผู้ใช้ส่งคำเชิญถึงกันได้"}. {"Allow users to send private messages","อนุญาตให้ผู้ใช้ส่งข้อความส่วนตัว"}. {"Announcements","ประกาศ"}. {"April","เมษายน"}. {"August","สิงหาคม"}. {"Backup Management","การจัดการข้อมูลสำรอง"}. {"Backup to File at ","สำรองไฟล์ข้อมูลที่"}. {"Backup","การสำรองข้อมูล "}. {"Bad format","รูปแบบที่ไม่ถูกต้อง"}. {"Birthday","วันเกิด"}. {"Change Password","เปลี่ยนรหัสผ่าน"}. {"Change User Password","เปลี่ยนรหัสผ่านของผู้ใช้"}. {"Chatroom configuration modified","มีการปรับเปลี่ยนการกำหนดค่าของห้องสนทนา"}. {"Chatrooms","ห้องสนทนา"}. {"Choose a username and password to register with this server","เลือกชื่อผู้ใช้และรหัสผ่านเพื่อลงทะเบียนกับเซิร์ฟเวอร์นี้"}. {"Choose storage type of tables","เลือกชนิดการจัดเก็บของตาราง"}. {"Choose whether to approve this entity's subscription.","เลือกว่าจะอนุมัติการสมัครเข้าใช้งานของเอนทิตี้นี้หรือไม่"}. {"City","เมือง"}. {"Commands","คำสั่ง"}. {"Conference room does not exist","ไม่มีห้องประชุม"}. {"Configuration","การกำหนดค่า"}. {"Connected Resources:","ทรัพยากรที่เชื่อมต่อ:"}. {"Country","ประเทศ"}. {"CPU Time:","เวลาการทำงานของ CPU:"}. {"Database Tables Configuration at ","การกำหนดค่าตารางฐานข้อมูลที่"}. {"Database","ฐานข้อมูล"}. {"December","ธันวาคม"}. {"Default users as participants","ผู้ใช้เริ่มต้นเป็นผู้เข้าร่วม"}. {"Delete message of the day on all hosts","ลบข้อความของวันบนโฮสต์ทั้งหมด"}. {"Delete message of the day","ลบข้อความของวัน"}. {"Delete Selected","ลบข้อความที่เลือก"}. {"Delete User","ลบผู้ใช้"}. {"Deliver event notifications","ส่งการแจ้งเตือนเหตุการณ์"}. {"Deliver payloads with event notifications","ส่งส่วนของข้อมูล (payload) พร้อมกับการแจ้งเตือนเหตุการณ์"}. {"Description:","รายละเอียด:"}. {"Disc only copy","คัดลอกเฉพาะดิสก์"}. {"Dump Backup to Text File at ","ถ่ายโอนการสำรองข้อมูลไปยังไฟล์ข้อความที่"}. {"Dump to Text File","ถ่ายโอนข้อมูลไปยังไฟล์ข้อความ"}. {"Edit Properties","แก้ไขคุณสมบัติ"}. {"ejabberd MUC module","ejabberd MUC module"}. {"ejabberd Publish-Subscribe module","ejabberd Publish-Subscribe module"}. {"ejabberd SOCKS5 Bytestreams module","ejabberd SOCKS5 Bytestreams module"}. {"ejabberd vCard module","ejabberd vCard module"}. {"Email","อีเมล"}. {"Enable logging","เปิดใช้งานการบันทึก"}. {"End User Session","สิ้นสุดเซสชันของผู้ใช้"}. {"Enter nickname you want to register","ป้อนชื่อเล่นที่คุณต้องการลงทะเบียน"}. {"Enter path to backup file","ป้อนพาธเพื่อสำรองไฟล์ข้อมูล"}. {"Enter path to jabberd14 spool dir","ป้อนพาธไปยัง jabberd14 spool dir"}. {"Enter path to jabberd14 spool file","ป้อนพาธไปยังไฟล์เก็บพักข้อมูล jabberd14"}. {"Enter path to text file","ป้อนพาธของไฟล์ข้อความ"}. {"Family Name","นามสกุล"}. {"February","กุมภาพันธ์"}. {"Friday","วันศุกร์"}. {"From","จาก"}. {"Full Name","ชื่อเต็ม"}. {"Get Number of Online Users","แสดงจำนวนผู้ใช้ออนไลน์"}. {"Get Number of Registered Users","แสดงจำนวนผู้ใช้ที่ลงทะเบียน"}. {"Get User Last Login Time","แสดงเวลาเข้าสู่ระบบครั้งล่าสุดของผู้ใช้"}. {"Get User Password","ขอรับรหัสผ่านของผู้ใช้"}. {"Get User Statistics","แสดงสถิติของผู้ใช้"}. {"Groups","กลุ่ม"}. {"Group","กลุ่"}. {"has been banned","ถูกสั่งห้าม"}. {"has been kicked","ถูกไล่ออก"}. {"Host","โฮสต์"}. {"Import Directory","อิมพอร์ตไดเร็กทอรี"}. {"Import File","อิมพอร์ตไฟล์"}. {"Import User from File at ","อิมพอร์ตผู้ใช้จากไฟล์ที่"}. {"Import Users from Dir at ","อิมพอร์ตผู้ใช้จาก Dir ที่"}. {"Import Users From jabberd14 Spool Files","อิมพอร์ตผู้ใช้จากไฟล์เก็บพักข้อมูล jabberd14"}. {"Improper message type","ประเภทข้อความไม่เหมาะสม"}. {"Incorrect password","รหัสผ่านไม่ถูกต้อง"}. {"IP addresses","ที่อยู่ IP"}. {"is now known as","ซึ่งรู้จักกันในชื่อ"}. {"It is not allowed to send private messages of type \"groupchat\"","ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยัง \"กลุ่มสนทนา\""}. {"It is not allowed to send private messages to the conference","ไม่อนุญาตให้ส่งข้อความส่วนตัวไปยังห้องประชุม"}. {"Jabber ID","Jabber ID"}. {"January","มกราคม"}. {"joins the room","เข้าห้องสนทนานี้"}. {"July","กรกฎาคม"}. {"June","มิถุนายน"}. {"Last Activity","กิจกรรมล่าสุด"}. {"Last login","การเข้าสู่ระบบครั้งล่าสุด"}. {"Last month","เดือนที่แล้ว"}. {"Last year","ปีที่แล้ว"}. {"leaves the room","ออกจากห้อง"}. {"Low level update script","อัพเดตสคริปต์ระดับต่ำ"}. {"Make participants list public","สร้างรายการผู้เข้าร่วมสำหรับใช้งานโดยบุคคลทั่วไป"}. {"Make room members-only","สร้างห้องสำหรับสมาชิกเท่านั้น"}. {"Make room password protected","สร้างห้องที่มีการป้องกันด้วยรหัสผ่าน"}. {"Make room persistent","สร้างเป็นห้องถาวร"}. {"Make room public searchable","สร้างเป็นห้องที่บุคคลทั่วไปสามารถค้นหาได้"}. {"March","มีนาคม"}. {"Max # of items to persist","จำนวนสูงสุดของรายการที่ยืนยัน"}. {"Max payload size in bytes","ขนาดสูงสุดของส่วนของข้อมูล (payload) มีหน่วยเป็นไบต์"}. {"Maximum Number of Occupants","จำนวนผู้ครอบครองห้องสูงสุด"}. {"May","พฤษภาคม"}. {"Members:","สมาชิก:"}. {"Memory","หน่วยความจำ"}. {"Message body","เนื้อหาของข้อความ"}. {"Middle Name","ชื่อกลาง"}. {"Moderator privileges required","ต้องมีสิทธิพิเศษของผู้ดูแลการสนทนา"}. {"Monday","วันจันทร์"}. {"Name","ชื่อ"}. {"Name:","ชื่อ:"}. {"Never","ไม่เคย"}. {"Nickname Registration at ","การลงทะเบียนชื่อเล่นที่ "}. {"Nickname","ชื่อเล่น"}. {"No body provided for announce message","ไม่ได้ป้อนเนื้อหาสำหรับข้อความที่ประกาศ"}. {"No Data","ไม่มีข้อมูล"}. {"No limit","ไม่จำกัด"}. {"Node ID","ID โหนด"}. {"Node not found","ไม่พบโหนด"}. {"Nodes","โหนด"}. {"None","ไม่มี"}. {"Notify subscribers when items are removed from the node","แจ้งเตือนผู้สมัครสมาชิกเมื่อรายการถูกลบออกจากโหนด"}. {"Notify subscribers when the node configuration changes","แจ้งเตือนผู้สมัครสมาชิกเมื่อการกำหนดค่าโหนดเปลี่ยนแปลง"}. {"Notify subscribers when the node is deleted","แจ้งเตือนผู้สมัครสมาชิกเมื่อโหนดถูกลบ"}. {"November","พฤศจิกายน"}. {"Number of occupants","จำนวนผู้ครอบครองห้อง"}. {"Number of online users","จำนวนผู้ใช้ออนไลน์"}. {"Number of registered users","จำนวนผู้ใช้ที่ลงทะเบียน"}. {"October","ตุลาคม"}. {"Offline Messages","ข้อความออฟไลน์"}. {"Offline Messages:","ข้อความออฟไลน์:"}. {"OK","ตกลง"}. {"Online Users","ผู้ใช้ออนไลน์"}. {"Online Users:","ผู้ใช้ออนไลน์:"}. {"Online","ออนไลน์"}. {"Only deliver notifications to available users","ส่งการแจ้งเตือนถึงผู้ใช้ที่สามารถติดต่อได้เท่านั้น"}. {"Only occupants are allowed to send messages to the conference","ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความไปยังห้องประชุม"}. {"Only occupants are allowed to send queries to the conference","ผู้ครอบครองห้องเท่านั้นที่ได้รับอนุญาตให้ส่งกระทู้ถามไปยังห้องประชุม"}. {"Only service administrators are allowed to send service messages","ผู้ดูแลด้านการบริการเท่านั้นที่ได้รับอนุญาตให้ส่งข้อความการบริการ"}. {"Organization Name","ชื่อองค์กร"}. {"Organization Unit","หน่วยขององค์กร"}. {"Outgoing s2s Connections","การเชื่อมต่อ s2s ขาออก"}. {"Outgoing s2s Connections:","การเชื่อมต่อ s2s ขาออก:"}. {"Owner privileges required","ต้องมีสิทธิพิเศษของเจ้าของ"}. {"Packet","แพ็กเก็ต"}. {"Password Verification","การตรวจสอบรหัสผ่าน"}. {"Password","รหัสผ่าน"}. {"Password:","รหัสผ่าน:"}. {"Path to Dir","พาธไปยัง Dir"}. {"Path to File","พาธของไฟล์ข้อมูล"}. {"Pending","ค้างอยู่"}. {"Period: ","ระยะเวลา:"}. {"Persist items to storage","ยืนยันรายการที่จะจัดเก็บ"}. {"Ping","Ping"}. {"Pong","Pong"}. {"Present real Jabber IDs to","แสดง Jabber IDs ที่ถูกต้องแก่"}. {"private, ","ส่วนตัว, "}. {"Publish-Subscribe","เผยแพร่-สมัครเข้าใช้งาน"}. {"PubSub subscriber request","คำร้องขอของผู้สมัครเข้าใช้งาน PubSub"}. {"Queries to the conference members are not allowed in this room","ห้องนี้ไม่อนุญาตให้ส่งกระทู้ถามถึงสมาชิกในห้องประชุม"}. {"RAM and disc copy","คัดลอก RAM และดิสก์"}. {"RAM copy","คัดลอก RAM"}. {"Really delete message of the day?","แน่ใจว่าต้องการลบข้อความของวันหรือไม่"}. {"Recipient is not in the conference room","ผู้รับไม่ได้อยู่ในห้องประชุม"}. {"Registered Users","ผู้ใช้ที่ลงทะเบียน"}. {"Registered Users:","ผู้ใช้ที่ลงทะเบียน:"}. {"Remote copy","คัดลอกระยะไกล"}. {"Remove User","ลบผู้ใช้"}. {"Remove","ลบ"}. {"Replaced by new connection","แทนที่ด้วยการเชื่อมต่อใหม่"}. {"Resources","ทรัพยากร"}. {"Restart Service","เริ่มต้นการบริการใหม่อีกครั้ง"}. {"Restart","เริ่มต้นใหม่"}. {"Restore Backup from File at ","คืนค่าการสำรองข้อมูลจากไฟล์ที่"}. {"Restore binary backup after next ejabberd restart (requires less memory):","คืนค่าข้อมูลสำรองแบบไบนารีหลังจากที่ ejabberd ถัดไปเริ่มการทำงานใหม่ (ใช้หน่วยความจำน้อยลง):"}. {"Restore binary backup immediately:","คืนค่าข้อมูลสำรองแบบไบนารีโดยทันที:"}. {"Restore plain text backup immediately:","คืนค่าข้อมูลสำรองที่เป็นข้อความธรรมดาโดยทันที:"}. {"Restore","การคืนค่า"}. {"Room Configuration","การกำหนดค่าห้องสนทนา"}. {"Room creation is denied by service policy","การสร้างห้องสนทนาถูกปฏิเสธโดยนโยบายการบริการ"}. {"Room title","ชื่อห้อง"}. {"Roster size","ขนาดของบัญชีรายชื่อ"}. {"RPC Call Error","ข้อผิดพลาดจากการเรียกใช้ RPC"}. {"Running Nodes","โหนดที่ทำงาน"}. {"Saturday","วันเสาร์"}. {"Script check","ตรวจสอบคริปต์"}. {"Search Results for ","ผลการค้นหาสำหรับ "}. {"Search users in ","ค้นหาผู้ใช้ใน "}. {"Send announcement to all online users on all hosts","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมดบนโฮสต์ทั้งหมด"}. {"Send announcement to all online users","ส่งประกาศถึงผู้ใช้ออนไลน์ทั้งหมด"}. {"Send announcement to all users on all hosts","ส่งประกาศถึงผู้ใช้ทั้งหมดบนโฮสต์ทั้งหมด"}. {"Send announcement to all users","ส่งประกาศถึงผู้ใช้ทั้งหมด"}. {"September","กันยายน"}. {"Set message of the day and send to online users","ตั้งค่าข้อความของวันและส่งถึงผู้ใช้ออนไลน์"}. {"Set message of the day on all hosts and send to online users","ตั้งค่าข้อความของวันบนโฮสต์ทั้งหมดและส่งถึงผู้ใช้ออนไลน์"}. {"Shared Roster Groups","กลุ่มบัญชีรายชื่อที่ใช้งานร่วมกัน"}. {"Show Integral Table","แสดงตารางรวม"}. {"Show Ordinary Table","แสดงตารางทั่วไป"}. {"Shut Down Service","ปิดการบริการ"}. {"Specify the access model","ระบุโมเดลการเข้าถึง"}. {"Specify the publisher model","ระบุโมเดลผู้เผยแพร่"}. {"Statistics of ~p","สถิติของ ~p"}. {"Statistics","สถิติ"}. {"Stopped Nodes","โหนดที่หยุด"}. {"Stop","หยุด"}. {"Storage Type","ชนิดที่เก็บข้อมูล"}. {"Store binary backup:","จัดเก็บข้อมูลสำรองแบบไบนารี:"}. {"Store plain text backup:","จัดเก็บข้อมูลสำรองที่เป็นข้อความธรรมดา:"}. {"Subject","หัวเรื่อง"}. {"Submitted","ส่งแล้ว"}. {"Submit","ส่ง"}. {"Subscriber Address","ที่อยู่ของผู้สมัคร"}. {"Subscription","การสมัครสมาชิก"}. {"Sunday","วันอาทิตย์"}. {"the password is","รหัสผ่านคือ"}. {"This room is not anonymous","ห้องนี้ไม่ปิดบังชื่อ"}. {"Thursday","วันพฤหัสบดี"}. {"Time delay","การหน่วงเวลา"}. {"Time","เวลา"}. {"To","ถึง"}. {"Traffic rate limit is exceeded","อัตราของปริมาณการเข้าใช้เกินขีดจำกัด"}. {"Transactions Aborted:","ทรานแซกชันที่ถูกยกเลิก:"}. {"Transactions Committed:","ทรานแซกชันที่ได้รับมอบหมาย:"}. {"Transactions Logged:","ทรานแซกชันที่บันทึก:"}. {"Transactions Restarted:","ทรานแซกชันที่เริ่มทำงานใหม่อีกครั้ง:"}. {"Tuesday","วันอังคาร"}. {"Update message of the day (don't send)","อัพเดตข้อความของวัน (ไม่ต้องส่ง)"}. {"Update message of the day on all hosts (don't send)","อัพเดตข้อความของวันบนโฮสต์ทั้งหมด (ไม่ต้องส่ง) "}. {"Update plan","แผนการอัพเดต"}. {"Update script","อัพเดตสคริปต์"}. {"Update","อัพเดต"}. {"Uptime:","เวลาการทำงานต่อเนื่อง:"}. {"User Management","การจัดการผู้ใช้"}. {"Users Last Activity","กิจกรรมล่าสุดของผู้ใช้"}. {"Users","ผู้ใช้"}. {"User","ผู้ใช้"}. {"Validate","ตรวจสอบ"}. {"vCard User Search","ค้นหาผู้ใช้ vCard "}. {"Virtual Hosts","โฮสต์เสมือน"}. {"Visitors are not allowed to send messages to all occupants","ผู้เยี่ยมเยือนไม่ได้รับอนุญาตให้ส่งข้อความถึงผู้ครอบครองห้องทั้งหมด"}. {"Wednesday","วันพุธ"}. {"When to send the last published item","เวลาที่ส่งรายการที่เผยแพร่ครั้งล่าสุด"}. {"Whether to allow subscriptions","อนุญาตให้เข้าร่วมเป็นสมาชิกหรือไม่"}. {"You have been banned from this room","คุณถูกสั่งห้ามไมให้เข้าห้องนี้"}. {"You must fill in field \"Nickname\" in the form","คุณต้องกรอกฟิลด์ \"Nickname\" ในแบบฟอร์ม"}. {"You need an x:data capable client to search","คุณต้องใช้ไคลเอ็นต์ที่รองรับ x:data เพื่อค้นหา"}. {"Your contact offline message queue is full. The message has been discarded.","ลำดับข้อความออฟไลน์ของผู้ที่ติดต่อของคุณเต็มแล้ว ข้อความถูกลบทิ้งแล้ว"}. ����������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/priv/msgs/el.msg���������������������������������������������������������������������0000644�0002322�0002322�00000211732�14154362354�016701� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make translations` instead %% To improve translations please read: %% https://docs.ejabberd.im/developer/extending-ejabberd/localization/ {" (Add * to the end of field to match substring)"," (Προσθέστε * στο τέλος του πεδίου για ταίριασμα με το substring)"}. {" has set the subject to: "," έχει θέσει το θέμα σε: "}. {"# participants","# συμμετέχοντες"}. {"A description of the node","Μία περιγραφή του κόμβου"}. {"A friendly name for the node","Ένα φιλικό όνομα για τον κόμβο"}. {"A password is required to enter this room","Απαιτείται κωδικός πρόσβασης για είσοδο σε αυτή την αίθουσα"}. {"A Web Page","Μία ιστοσελίδα"}. {"Accept","Αποδοχή"}. {"Access denied by service policy","Άρνηση πρόσβασης, λόγω τακτικής παροχής υπηρεσιών"}. {"Access model of authorize","Μοντέλο πρόσβασης της πιστοποίησης"}. {"Access model of open","Μοντέλο πρόσβασης του ανοικτού"}. {"Access model of presence","Μοντέλο πρόσβασης της παρουσίας"}. {"Access model of roster","Μοντέλο πρόσβασης της Λίστας Επαφών"}. {"Access model of whitelist","Μοντέλο πρόσβασης της Λευκής Λίστας"}. {"Access model","Καθορίστε το μοντέλο πρόσβασης"}. {"Account doesn't exist","Ο λογαριασμός δεν υπάρχει"}. {"Action on user","Eνέργεια για το χρήστη"}. {"Add Jabber ID","Προσθήκη Jabber Ταυτότητας"}. {"Add New","Προσθήκη νέου"}. {"Add User","Προσθήκη Χρήστη"}. {"Administration of ","Διαχείριση του "}. {"Administration","Διαχείριση"}. {"Administrator privileges required","Aπαιτούνται προνόμια διαχειριστή"}. {"All activity","Όλες οι δραστηριότητες"}. {"All Users","Όλοι οι χρήστες"}. {"Allow subscription","Επιτρέψτε την Συνδρομή"}. {"Allow this Jabber ID to subscribe to this pubsub node?","Επιτρέπετε σε αυτή την Jabber Ταυτότητα να εγγραφεί σε αυτό τον κόμβο Δημοσίευσης-Εγγραφής;"}. {"Allow this person to register with the room?","Επιτρέπτε στο άτομο να καταχωρηθεί στην αίθουσα;"}. {"Allow users to change the subject","Επιτρέψτε στους χρήστες να αλλάζουν το θέμα"}. {"Allow users to query other users","Επιτρέψτε στους χρήστες να ερωτούν άλλους χρήστες"}. {"Allow users to send invites","Επιτρέψτε στους χρήστες να αποστέλλουν προσκλήσεις"}. {"Allow users to send private messages","Επιτρέψτε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα"}. {"Allow visitors to change nickname","Επιτρέψτε στους επισκέπτες να αλλάζου ψευδώνυμο"}. {"Allow visitors to send private messages to","Επιτρέψτε στους χρήστες να αποστέλλουν ιδιωτικά μηνύματα σε"}. {"Allow visitors to send status text in presence updates","Επιτρέψτε στους επισκέπτες να αποστέλλουν κατάσταση στις ενημερώσεις παρουσίας"}. {"Allow visitors to send voice requests","Επιτρέψτε στους επισκέπτες να στέλνουν αιτήματα φωνής"}. {"An associated LDAP group that defines room membership; this should be an LDAP Distinguished Name according to an implementation-specific or deployment-specific definition of a group.","Μία σχετική LDAP ομάδα που καθορίζει τις συνδρομές στην αίθουσα: αυτό θα πρέπει να είναι ένα Διακεκριμένο Όνομα LDAP, σύμφωνα με τον προσδιορισμό κατεύθυνσης υλοποίησης ή ανάπτυξης της ομάδας."}. {"Announcements","Ανακοινώσεις"}. {"Answer associated with a picture","Η απάντηση σχετίστηκε με μια εικόνα"}. {"Answer associated with a video","Η απάντηση σχετίστηκε με ένα βίντεο"}. {"Answer associated with speech","Η απάντηση σχετίστηκε με ομιλία"}. {"Answer to a question","Απάντηση σε ερώτηση"}. {"Anyone in the specified roster group(s) may subscribe and retrieve items","Οποιοσδήποτε στις ορισθείσες Λίστες Ομάδων δύναται να εγγραφεί και να παραλάβει αντικείμενα"}. {"Anyone may associate leaf nodes with the collection","Οποιοσδήποτε μπορεί να συσχετίσει κόμβους φύλλων με τη συλλογή"}. {"Anyone may publish","Οποιοσδήποτε δύναται να δημοσιέυει"}. {"Anyone may subscribe and retrieve items","Οποιοσδήποτε δύναται να εγγραφεί και να παραλάβει αντικείμενα"}. {"Anyone with a presence subscription of both or from may subscribe and retrieve items","Όποιος έχει συνδρομή παρουσίας και των δύο ή από μπορεί να εγγραφεί και να ανακτήσει στοιχεία"}. {"Anyone with Voice","Οποιοσδήποτε με Φωνή"}. {"Anyone","Οποιοσδήποτε"}. {"April","Απρίλιος"}. {"Attribute 'channel' is required for this request","Το δηλωτικό 'channel' απαιτείται για αυτό το Ερώτημα"}. {"Attribute 'id' is mandatory for MIX messages","Το δηλωτικό 'id' επιτακτικό για μηνύματα MIX"}. {"Attribute 'jid' is not allowed here","Το δηλωτικό 'jid' δεν επιτρέπεται εδώ"}. {"Attribute 'node' is not allowed here","Το δηλωτικό 'node' δεν επιτρέπεται εδώ"}. {"Attribute 'to' of stanza that triggered challenge","Το δηλωτικό 'to' του Δωματίου που ξεκίνησε την πρόκληση"}. {"August","Αύγουστος"}. {"Automatic node creation is not enabled","Η αυτόματη δημιουργία κόμβων δεν είναι ενεργοποιημένη"}. {"Backup Management","Διαχείριση Αντιγράφου Ασφαλείας"}. {"Backup of ~p","Αντιγράφο Ασφαλείας του ~p"}. {"Backup to File at ","Αποθήκευση Αντιγράφου Ασφαλείας σε Αρχείο στο "}. {"Backup","Αποθήκευση Αντιγράφου Ασφαλείας"}. {"Bad format","Ακατάλληλη μορφή"}. {"Birthday","Γενέθλια"}. {"Both the username and the resource are required","Τόσο το όνομα χρήστη όσο και ο πόρος είναι απαραίτητα"}. {"Bytestream already activated","Το Bytestream έχει ήδη ενεργοποιηθεί"}. {"Cannot remove active list","Δεν είναι δυνατή η κατάργηση της ενεργής λίστας"}. {"Cannot remove default list","Δεν μπορείτε να καταργήσετε την προεπιλεγμένη λίστα"}. {"CAPTCHA web page","Ιστοσελίδα CAPTCHA"}. {"Challenge ID","Ταυτότητα Πρόκλησης"}. {"Change Password","Αλλαγή κωδικού πρόσβασης"}. {"Change User Password","Αλλαγή κωδικού πρόσβασης χρήστη"}. {"Changing password is not allowed","Η αλλαγή του κωδικού πρόσβασης δεν επιτρέπεται"}. {"Changing role/affiliation is not allowed","Η αλλαγή ρόλου/ομάδας δεν επιτρέπεται"}. {"Channel already exists","Το κανάλι υπάρχει ήδη"}. {"Channel does not exist","Το κανάλι δεν υπάρχει"}. {"Channels","Κανάλια"}. {"Characters not allowed:","Χαρακτήρες που δεν επιτρέπονται:"}. {"Chatroom configuration modified","Η ρύθμιση παραμέτρων της αίθουσας σύνεδριασης τροποποιηθηκε"}. {"Chatroom is created","Η αίθουσα συνομιλίας έχει δημιουργηθεί"}. {"Chatroom is destroyed","Η αίθουσα σύνεδριασης διαγράφηκε"}. {"Chatroom is started","Η αίθουσα σύνεδριασης έχει ξεκινήσει"}. {"Chatroom is stopped","Η αίθουσα σύνεδριασης έχει σταματήσει"}. {"Chatrooms","Αίθουσες σύνεδριασης"}. {"Choose a username and password to register with this server","Επιλέξτε ένα όνομα χρήστη και κωδικό πρόσβασης για να εγγραφείτε σε αυτό τον διακομιστή"}. {"Choose storage type of tables","Επιλέξτε τύπο αποθήκευσης των πινάκων"}. {"Choose whether to approve this entity's subscription.","Επιλέξτε αν θα εγκρίθεί η εγγραφή αυτής της οντότητας."}. {"City","Πόλη"}. {"Client acknowledged more stanzas than sent by server","Ο πελάτης γνωρίζει περισσότερα δωμάτια από αυτά που στάλθηκαν από τον εξυπηρετητή"}. {"Commands","Εντολές"}. {"Conference room does not exist","Η αίθουσα σύνεδριασης δεν υπάρχει"}. {"Configuration of room ~s","Διαμόρφωση δωματίου ~ s"}. {"Configuration","Ρύθμιση παραμέτρων"}. {"Connected Resources:","Συνδεδεμένοι Πόροι:"}. {"Contact Addresses (normally, room owner or owners)","Διευθύνσεις της Επαφής (κανονικά, ιδιοκτήτης (-ες) αίθουσας)"}. {"Country","Χώρα"}. {"CPU Time:","Ώρα CPU:"}. {"Current Discussion Topic","Τρέχων θέμα συζήτησης"}. {"Database failure","Αποτυχία βάσης δεδομένων"}. {"Database Tables at ~p","Πίνακες βάσης δεδομένων στο ~p"}. {"Database Tables Configuration at ","Διαμόρφωση Πίνακων βάσης δεδομένων στο "}. {"Database","Βάση δεδομένων"}. {"December","Δεκέμβριος"}. {"Default users as participants","Προρυθμισμένοι χρήστες ως συμμετέχοντες"}. {"Delete content","Διαγραφή περιεχομένων"}. {"Delete message of the day on all hosts","Διαγράψτε το μήνυμα της ημέρας σε όλους τους κεντρικούς υπολογιστές"}. {"Delete message of the day","Διαγράψτε το μήνυμα της ημέρας"}. {"Delete Selected","Διαγραφή επιλεγμένων"}. {"Delete table","Διαγραφή Πίνακα"}. {"Delete User","Διαγραφή Χρήστη"}. {"Deliver event notifications","Παράδοση ειδοποιήσεων συμβάντων"}. {"Deliver payloads with event notifications","Κοινοποίηση φόρτου εργασιών με τις ειδοποιήσεις συμβάντων"}. {"Description:","Περιγραφή:"}. {"Disc only copy","Αντίγραφο μόνο σε δίσκο"}. {"'Displayed groups' not added (they do not exist!): ","'Οι εμφανιζόμενες ομάδες' δεν προστέθηκαν (δεν υπάρχουν!): "}. {"Displayed:","Απεικονίζεται:"}. {"Don't tell your password to anybody, not even the administrators of the XMPP server.","Μην πείτε τον κωδικό πρόσβασής σας σε κανέναν, ούτε στους διαχειριστές του διακομιστή XMPP."}. {"Dump Backup to Text File at ","Αποθήκευση Αντιγράφου Ασφαλείας σε αρχείο κειμένου στο "}. {"Dump to Text File","Αποθήκευση σε αρχείο κειμένου"}. {"Duplicated groups are not allowed by RFC6121","Δεν επιτρέπονται διπλότυπες ομάδες από το RFC6121"}. {"Dynamically specify a replyto of the item publisher","Δυναμικά προσδιορίστε το Απάντηση σε του εκδότη του αντικειμένου"}. {"Edit Properties","Επεξεργασία ιδιοτήτων"}. {"Either approve or decline the voice request.","Είτε εγκρίνετε ή απορρίψτε το αίτημα φωνής."}. {"ejabberd HTTP Upload service","Υπηρεσία ανεβάσματος αρχείων του ejabberd"}. {"ejabberd MUC module","ενότητα ejabberd MUC"}. {"ejabberd Multicast service","υπηρεσία ejabberd Multicast"}. {"ejabberd Publish-Subscribe module","ejabberd module Δημοσίευσης-Εγγραφής"}. {"ejabberd SOCKS5 Bytestreams module","ενότητα ejabberd SOCKS5 Bytestreams"}. {"ejabberd vCard module","ejabberd vCard module"}. {"ejabberd Web Admin","ejabberd Web Admin"}. {"ejabberd","ejabberd"}. {"Elements","Στοιχεία"}. {"Email Address","Ηλεκτρονική Διεύθυνση"}. {"Email","Ηλεκτρονικό ταχυδρομείο"}. {"Enable logging","Ενεργοποίηση καταγραφής"}. {"Enable message archiving","Ενεργοποιήστε την αρχειοθέτηση μηνυμάτων"}. {"Enabling push without 'node' attribute is not supported","Η ενεργοποίηση της ώθησης χωρίς το χαρακτηριστικό 'κόμβος' δεν υποστηρίζεται"}. {"End User Session","Τερματισμός Συνεδρίας Χρήστη"}. {"Enter nickname you want to register","Πληκτρολογήστε το ψευδώνυμο που θέλετε να καταχωρήσετε"}. {"Enter path to backup file","Εισάγετε τοποθεσία αρχείου αντιγράφου ασφαλείας"}. {"Enter path to jabberd14 spool dir","Εισάγετε κατάλογο αρχείων σειράς jabberd14"}. {"Enter path to jabberd14 spool file","Εισάγετε τοποθεσία αρχείου σειράς jabberd14"}. {"Enter path to text file","Εισάγετε Τοποθεσία Αρχείου Κειμένου"}. {"Enter the text you see","Πληκτρολογήστε το κείμενο που βλέπετε"}. {"Erlang XMPP Server","Διακομιστής Erlang XMPP"}. {"Error","Σφάλμα"}. {"Exclude Jabber IDs from CAPTCHA challenge","Εξαίρεσε αυτές τις ταυτότητες Jabber από την CAPTCHA πρόκληση"}. {"Export all tables as SQL queries to a file:","Εξαγωγή όλων των πινάκων ως ερωτημάτων SQL σε ένα αρχείο:"}. {"Export data of all users in the server to PIEFXIS files (XEP-0227):","Εξαγωγή δεδομένων όλων των χρηστών του διακομιστή σε PIEFXIS αρχεία (XEP-0227):"}. {"Export data of users in a host to PIEFXIS files (XEP-0227):","Εξαγωγή δεδομένων των χρηστών κεντρικού υπολογιστή σε PIEFXIS αρχεία (XEP-0227):"}. {"External component failure","Βλάβη εξωτερικού στοιχείου"}. {"External component timeout","Τέλος χρονικού όριου εξωτερικού στοιχείου"}. {"Failed to activate bytestream","Απέτυχε η ενεργοποίηση του bytestream"}. {"Failed to extract JID from your voice request approval","Απέτυχε η εξαγωγή JID από την έγκριση του αιτήματος φωνής σας"}. {"Failed to map delegated namespace to external component","Αποτυχία ταξιθέτησης μεταγεγραμμένου χώρου ονομάτων σε εξωτερικό στοιχείο"}. {"Failed to parse HTTP response","Αποτυχία ανάλυσης της απόκρισης HTTP"}. {"Failed to process option '~s'","Αποτυχία επεξεργασίας της επιλογής '~s'"}. {"Family Name","Επώνυμο"}. {"FAQ Entry","Καταχώριση συχνών ερωτήσεων"}. {"February","Φεβρουάριος"}. {"File larger than ~w bytes","Αρχείο μεγαλύτερο από ~w bytes"}. {"Fill in the form to search for any matching XMPP User","Συμπληρώστε την φόρμα για αναζήτηση χρηστών XMPP"}. {"Friday","Παρασκευή"}. {"From ~ts","Από ~ts"}. {"From","Από"}. {"Full List of Room Admins","Πλήρης Κατάλογος Διαχειριστών αιθουσών"}. {"Full List of Room Owners","Πλήρης Κατάλογος Ιδιοκτητών αιθουσών"}. {"Full Name","Ονοματεπώνυμο"}. {"Get Number of Online Users","Έκθεση αριθμού συνδεδεμένων χρηστών"}. {"Get Number of Registered Users","Έκθεση αριθμού εγγεγραμμένων χρηστών"}. {"Get Pending","Λήψη των εκκρεμοτήτων"}. {"Get User Last Login Time","Έκθεση Τελευταίας Ώρας Σύνδεσης Χρήστη"}. {"Get User Password","Έκθεση Κωδικού Πρόσβασης Χρήστη"}. {"Get User Statistics","Έκθεση Στατιστικών Χρήστη"}. {"Given Name","Όνομα"}. {"Grant voice to this person?","Παραχώρηση φωνής σε αυτό το άτομο;"}. {"Groups that will be displayed to the members","Ομάδες που θα εμφανίζονται στα μέλη"}. {"Groups","Ομάδες"}. {"Group","Ομάδα"}. {"has been banned","έχει αποβληθεί διαπαντώς"}. {"has been kicked because of a system shutdown","αποβλήθηκε λόγω τερματισμού συστήματος"}. {"has been kicked because of an affiliation change","έχει αποβληθεί λόγω αλλαγής υπαγωγής"}. {"has been kicked because the room has been changed to members-only","αποβλήθηκε επειδή η αίθουσα αλλάξε γιά μέλη μόνο"}. {"has been kicked","αποβλήθηκε"}. {"Host unknown","Άγνωστος εξυπηρετητής"}. {"Host","Εξυπηρετητής"}. {"HTTP File Upload","Ανέβασμα αρχείου"}. {"Idle connection","Αδρανής σύνδεση"}. {"If you don't see the CAPTCHA image here, visit the web page.","Εάν δεν βλέπετε την εικόνα CAPTCHA εδώ, επισκεφθείτε την ιστοσελίδα."}. {"Import Directory","Εισαγωγή κατάλογου αρχείων"}. {"Import File","Εισαγωγή αρχείων"}. {"Import user data from jabberd14 spool file:","Εισαγωγή δεδομένων χρήστη από το αρχείο σειράς jabberd14:"}. {"Import User from File at ","Εισαγωγή χρηστών από αρχείο στο "}. {"Import users data from a PIEFXIS file (XEP-0227):","Εισαγωγή δεδομένων χρηστών από ένα αρχείο PIEFXIS (XEP-0227):"}. {"Import users data from jabberd14 spool directory:","Εισαγωγή δεδομένων χρηστών από κατάλογο αρχείων σειράς jabberd14:"}. {"Import Users from Dir at ","Εισαγωγή χρηστών από κατάλογο αρχείων στο "}. {"Import Users From jabberd14 Spool Files","Εισαγωγή Χρηστών από αρχεία σειράς jabberd14"}. {"Improper domain part of 'from' attribute","Ανάρμοστο τμήμα τομέα του χαρακτηριστικού 'from'"}. {"Improper message type","Ακατάλληλο είδος μηνύματος"}. {"Incoming s2s Connections:","Εισερχόμενες συνδέσεις s2s:"}. {"Incorrect CAPTCHA submit","Λάθος υποβολή CAPTCHA"}. {"Incorrect data form","Εσφαλμένη φόρμα δεδομένων"}. {"Incorrect password","Εσφαλμένος κωδικός πρόσβασης"}. {"Incorrect value of 'action' attribute","Λανθασμένη τιμή του χαρακτηριστικού 'action'"}. {"Incorrect value of 'action' in data form","Λανθασμένη τιμή 'action' στη φόρμα δεδομένων"}. {"Incorrect value of 'path' in data form","Λανθασμένη τιμή 'path' στη φόρμα δεδομένων"}. {"Insufficient privilege","Ανεπαρκή προνόμια"}. {"Internal server error","Εσωτερικό σφάλμα"}. {"Invalid 'from' attribute in forwarded message","Μη έγκυρο χαρακτηριστικό 'από' στο προωθούμενο μήνυμα"}. {"Invalid node name","Μη έγκυρο όνομα κόμβου"}. {"Invalid 'previd' value","Μη έγκυρη τιμή 'previd'"}. {"Invitations are not allowed in this conference","Οι προσκλήσεις δεν επιτρέπονται σε αυτή τη διάσκεψη"}. {"IP addresses","Διευθύνσεις IP"}. {"is now known as","είναι τώρα γνωστή ως"}. {"It is not allowed to send error messages to the room. The participant (~s) has sent an error message (~s) and got kicked from the room","Δεν επιτρέπεται η αποστολή μηνυμάτων σφάλματος στο δωμάτιο. Ο συμμετέχων (~s) έχει στείλει ένα μήνυμα σφάλματος (~s) και έχει πεταχτεί έξω από την αίθουσα"}. {"It is not allowed to send private messages of type \"groupchat\"","Δεν επιτρέπεται η αποστολή προσωπικών μηνυμάτων του τύπου \"groupchat\""}. {"It is not allowed to send private messages to the conference","Δεν επιτρέπεται να στείλει προσωπικά μηνύματα για τη διάσκεψη"}. {"It is not allowed to send private messages","Δεν επιτρέπεται η αποστολή προσωπικών μηνυμάτων"}. {"Jabber ID","Ταυτότητα Jabber"}. {"January","Ιανουάριος"}. {"JID normalization denied by service policy","Απετράπη η κανονικοποίηση του JID, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"JID normalization failed","Απετράπη η κανονικοποίηση του JID"}. {"joins the room","συνδέεται στην αίθουσα"}. {"July","Ιούλιος"}. {"June","Ιούνιος"}. {"Just created","Μόλις δημιουργήθηκε"}. {"Label:","Ετικέτα:"}. {"Last Activity","Τελευταία Δραστηριότητα"}. {"Last login","Τελευταία σύνδεση"}. {"Last message","Τελευταίο μήνυμα"}. {"Last month","Περασμένο μήνα"}. {"Last year","Πέρυσι"}. {"Least significant bits of SHA-256 hash of text should equal hexadecimal label","Τα ψηφία μικρότερης αξίας του αθροίσματος SHA-256 του κειμένου θα έπρεπε να ισούνται με την δεκαεξαδική ετικέτα"}. {"leaves the room","εγκαταλείπει την αίθουσα"}. {"List of rooms","Κατάλογος αιθουσών"}. {"Logging","Καταγραφή"}. {"Low level update script","Προγράμα ενημέρωσης χαμηλού επίπεδου"}. {"Make participants list public","Κάντε δημόσιο τον κατάλογο συμμετεχόντων"}. {"Make room CAPTCHA protected","Κάντε την αίθουσα προστατεύομενη με CAPTCHA"}. {"Make room members-only","Κάντε την αίθουσα μόνο για μέλη"}. {"Make room moderated","Κάντε την αίθουσα εποπτεύομενη"}. {"Make room password protected","Κάντε την αίθουσα προστατεύομενη με κωδικό πρόσβασης"}. {"Make room persistent","Κάντε την αίθουσα μόνιμη"}. {"Make room public searchable","Κάντε την δημόσια αναζήτηση δυνατή για αυτή την αίθουσα"}. {"Malformed username","Λανθασμένη μορφή ονόματος χρήστη"}. {"MAM preference modification denied by service policy","Άρνηση αλλαγής προτιμήσεων MAM, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"March","Μάρτιος"}. {"Max # of items to persist","Μέγιστος αριθμός μόνιμων στοιχείων"}. {"Max payload size in bytes","Μέγιστο μέγεθος φορτίου σε bytes"}. {"Maximum file size","Μέγιστο μέγεθος αρχείου"}. {"Maximum Number of History Messages Returned by Room","Μέγιστος αριθμός μηνυμάτων Ιστορικού που επιστρέφονται από την Αίθουσα"}. {"Maximum number of items to persist","Μέγιστος αριθμός μόνιμων στοιχείων"}. {"Maximum Number of Occupants","Μέγιστος αριθμός συμμετεχόντων"}. {"May","Μάιος"}. {"Members not added (inexistent vhost!): ","Τα μέλη δεν προστέθηκαν (ανύπαρκτος vhost!): "}. {"Membership is required to enter this room","Απαιτείται αίτηση συμετοχής για είσοδο σε αυτή την αίθουσα"}. {"Members:","Μέλη:"}. {"Memorize your password, or write it in a paper placed in a safe place. In XMPP there isn't an automated way to recover your password if you forget it.","Απομνημονεύστε τον κωδικό πρόσβασής σας ή γράψτε τον σε χαρτί που βρίσκεται σε ασφαλές μέρος. Στο XMPP δεν υπάρχει αυτοματοποιημένος τρόπος ανάκτησης του κωδικού πρόσβασής σας εάν τον ξεχάσετε."}. {"Memory","Μνήμη"}. {"Mere Availability in XMPP (No Show Value)","Διαθεσιμότητα στο XMPP (Χωρίς ένδειξη)"}. {"Message body","Περιεχόμενο μηνύματος"}. {"Message not found in forwarded payload","Δεν βρέθηκε μήνυμα στον προωθημένο φόρτο εργασίας"}. {"Messages from strangers are rejected","Τα μηνύματα από αγνώστους απορρίπτονται"}. {"Messages of type headline","Μηνύματα του τύπου headline"}. {"Messages of type normal","Μηνύματα του τύπου normal"}. {"Middle Name","Πατρώνυμο"}. {"Minimum interval between voice requests (in seconds)","Ελάχιστο χρονικό διάστημα μεταξύ αιτημάτων φωνής (σε δευτερόλεπτα)"}. {"Moderator privileges required","Aπαιτούνται προνόμια επόπτου"}. {"Moderators Only","Επόπτες μόμον"}. {"Moderator","Επόπτης"}. {"Modified modules","Τροποποιημένα modules"}. {"Module failed to handle the query","Το module απέτυχε να χειριστεί το ερώτημα"}. {"Monday","Δευτέρα"}. {"Multicast","Πολλαπλή διανομή (Multicast)"}. {"Multiple <item/> elements are not allowed by RFC6121","Πολλαπλά στοιχεία <item/> δεν επιτρέπονται από το RFC6121"}. {"Multi-User Chat","Συνομιλία με πολλούς χρήστες"}. {"Name in the rosters where this group will be displayed","Όνομα στις λίστες όπου αυτή η ομάδα θα εμφανίζεται"}. {"Name","Όνομα"}. {"Name:","Όνομα:"}. {"Natural Language for Room Discussions","Μητρική Γλώσσα για τις Συζητήσεις Αιθουσών"}. {"Natural-Language Room Name","Αίθουσα Μητρικής Γλώσσας"}. {"Neither 'jid' nor 'nick' attribute found","Δεν βρέθηκε κανένα χαρακτηριστικό 'jid' ούτε 'nick'"}. {"Neither 'role' nor 'affiliation' attribute found","Δεν βρέθηκε ούτε χαρακτηριστικό 'role' ούτε 'affiliation'"}. {"Never","Ποτέ"}. {"New Password:","Νέος κωδικός πρόσβασης:"}. {"Nickname can't be empty","Το Ψευδώνυμο δεν μπορεί να είναι άδειο"}. {"Nickname Registration at ","Εγγραφή με Ψευδώνυμο στο "}. {"Nickname ~s does not exist in the room","Ψευδώνυμο ~s δεν υπάρχει σε αυτή την αίθουσα"}. {"Nickname","Ψευδώνυμο"}. {"No address elements found","Δεν βρέθηκαν στοιχεία διεύθυνσης"}. {"No addresses element found","Δεν βρέθηκε στοιχείο διεύθυνσης"}. {"No 'affiliation' attribute found","Δεν βρέθηκε το χαρακτηριστικό 'affiliation'"}. {"No available resource found","Δεν βρέθηκε διαθέσιμος πόρος"}. {"No body provided for announce message","Δεν προμηθεύτηκε περιεχόμενο ανακοινώσης"}. {"No child elements found","Δεν βρέθηκαν θυγατρικά στοιχεία"}. {"No data form found","Δεν βρέθηκε φόρμα δεδομένων"}. {"No Data","Κανένα στοιχείο"}. {"No features available","Δεν υπάρχουν διαθέσιμες λειτουργίες"}. {"No <forwarded/> element found","Δεν βρέθηκε στοιχείο <forwarded/>"}. {"No hook has processed this command","Κανένα άγκιστρο δεν έχει επεξεργαστεί αυτήν την εντολή"}. {"No info about last activity found","Δεν βρέθηκαν πληροφορίες για την τελευταία δραστηριότητα"}. {"No 'item' element found","Δεν βρέθηκε το στοιχείο 'item'"}. {"No items found in this query","Δεν βρέθηκαν στοιχεία σε αυτό το ερώτημα"}. {"No limit","Χωρίς όριο"}. {"No module is handling this query","Κανένα module δεν χειρίζεται αυτό το ερώτημα"}. {"No node specified","Δεν καθορίστηκε κόμβος"}. {"No 'password' found in data form","Δεν υπάρχει 'password' στη φόρμα δεδομένων"}. {"No 'password' found in this query","Δεν βρέθηκε 'password' σε αυτό το ερώτημα"}. {"No 'path' found in data form","Δεν υπάρχει 'path' στη φόρμα δεδομένων"}. {"No pending subscriptions found","Δεν βρέθηκαν εκκρεμείς συνδρομές"}. {"No privacy list with this name found","Δεν βρέθηκε κατάλογος απορρήτου με αυτό το όνομα"}. {"No private data found in this query","Δεν βρέθηκαν ιδιωτικά δεδομένα σε αυτό το ερώτημα"}. {"No running node found","Δεν βρέθηκε ενεργός κόμβος"}. {"No services available","Δεν υπάρχουν διαθέσιμες υπηρεσίες"}. {"No statistics found for this item","Δεν βρέθηκαν στατιστικά στοιχεία για αυτό το στοιχείο"}. {"No 'to' attribute found in the invitation","Δε βρέθηκε το χαρακτηριστικό 'to' στην πρόσκληση"}. {"Nobody","Κανείς"}. {"Node already exists","Ο κόμβος υπάρχει ήδη"}. {"Node ID","Ταυτότητα Κόμβου"}. {"Node index not found","Ο δείκτης κόμβου δεν βρέθηκε"}. {"Node not found","Κόμβος δεν βρέθηκε"}. {"Node ~p","Κόμβος ~p"}. {"Nodeprep has failed","Το Nodeprep απέτυχε"}. {"Nodes","Κόμβοι"}. {"None","Κανένα"}. {"Not allowed","Δεν επιτρέπεται"}. {"Not Found","Δε βρέθηκε"}. {"Not subscribed","Δεν έχετε εγγραφεί"}. {"Notify subscribers when items are removed from the node","Ειδοποιήστε τους συνδρομητές όταν αφαιρούνται στοιχεία από τον κόμβο"}. {"Notify subscribers when the node configuration changes","Ειδοποίηση στους συνδρομητές όταν αλλάζει η διαμόρφωση κόμβου"}. {"Notify subscribers when the node is deleted","Ειδοποίηση στους συνδρομητές όταν ο κόμβος διαγράφεται"}. {"November","Νοέμβριος"}. {"Number of answers required","Πλήθος αναζητημένων ερωτημάτων"}. {"Number of occupants","Αριθμός συμετεχόντων"}. {"Number of Offline Messages","Πλήθος μηνυμάτων Χωρίς Σύνδεση"}. {"Number of online users","Αριθμός συνδεδεμένων χρηστών"}. {"Number of registered users","Αριθμός εγγεγραμμένων χρηστών"}. {"Number of seconds after which to automatically purge items","Πλήθος δευτερολέπτων μετά τα οποία αυτομάτως εκκαθαρίζονται αντικείμενα"}. {"Occupants are allowed to invite others","Οι συμμετέχοντες μπορούν να προσκαλέσουν και άλλους"}. {"Occupants May Change the Subject","Επιτρέψτε στους χρήστες να αλλάζουν το Θέμα"}. {"October","Οκτώβριος"}. {"Offline Messages","Χωρίς Σύνδεση Μηνύματα"}. {"Offline Messages:","Χωρίς Σύνδεση Μηνύματα:"}. {"OK","Εντάξει"}. {"Old Password:","Παλαιός κωδικός πρόσβασης:"}. {"Online Users:","Online Χρήστες:"}. {"Online Users","Συνδεμένοι χρήστες"}. {"Online","Συνδεδεμένο"}. {"Only admins can see this","Μόνον οι διαχειριστές μπορούν να το δουν αυτό"}. {"Only collection node owners may associate leaf nodes with the collection","Μόνον οι ιδιοκτήτες των κόμβων μπορούν να συσχετίσουν leaf nodes με την Συλλογή"}. {"Only deliver notifications to available users","Παράδοση ειδοποιήσεων μόνο σε διαθέσιμους χρήστες"}. {"Only <enable/> or <disable/> tags are allowed","Επιτρέπονται μόνο tags <enable /> ή <disable />"}. {"Only <list/> element is allowed in this query","Στο ερώτημα αυτό επιτρέπεται μόνο το στοιχείο <list />"}. {"Only members may query archives of this room","Μόνο μέλη μπορούν να δούνε τα αρχεία αυτής της αίθουσας"}. {"Only moderators and participants are allowed to change the subject in this room","Μόνο οι συντονιστές και οι συμμετέχοντες μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}. {"Only moderators are allowed to change the subject in this room","Μόνο οι συντονιστές μπορούν να αλλάξουν το θέμα αυτής της αίθουσας"}. {"Only moderators can approve voice requests","Μόνο οι συντονιστές μπορούν να εγκρίνουν τις αιτήσεις φωνής"}. {"Only occupants are allowed to send messages to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στέλνουν μηνύματα στο συνέδριο"}. {"Only occupants are allowed to send queries to the conference","Μόνο οι συμμετέχοντες επιτρέπεται να στείλουν ερωτήματα στη διάσκεψη"}. {"Only publishers may publish","Μόνον εκδότες μπορούν να δημοσιεύσουν"}. {"Only service administrators are allowed to send service messages","Μόνο οι διαχειριστές των υπηρεσιών επιτρέπεται να στείλουν υπηρεσιακά μηνύματα"}. {"Only those on a whitelist may associate leaf nodes with the collection","Μόνον οι εξαιρεθέντες μπορούν να συσχετίσουν leaf nodes με τη συλλογή"}. {"Only those on a whitelist may subscribe and retrieve items","Μόνο όσοι βρίσκονται στη λίστα επιτρεπόμενων μπορούν να εγγραφούν και να ανακτήσουν αντικείμενα"}. {"Organization Name","Όνομα Οργανισμού"}. {"Organization Unit","Μονάδα Οργανισμού"}. {"Outgoing s2s Connections","Εξερχόμενες S2S Συνδέσεις"}. {"Outgoing s2s Connections:","Εξερχόμενες S2S Συνδέσεις:"}. {"Owner privileges required","Aπαιτούνται προνόμια ιδιοκτήτη"}. {"Packet relay is denied by service policy","Απαγορεύεται η αναμετάδοση πακέτων, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"Packet","Πακέτο"}. {"Participant","Συμμετέχων"}. {"Password Verification","Επαλήθευση κωδικού πρόσβασης"}. {"Password Verification:","Επαλήθευση κωδικού πρόσβασης:"}. {"Password","Κωδικός πρόσβασης"}. {"Password:","Κωδικός πρόσβασης:"}. {"Path to Dir","Τοποθεσία κατάλογου αρχείων"}. {"Path to File","Τοποθεσία Αρχείου"}. {"Payload type","Τύπος φόρτου εργασιών"}. {"Pending","Εκκρεμεί"}. {"Period: ","Περίοδος: "}. {"Persist items to storage","Μόνιμη αποθήκευση στοιχείων"}. {"Persistent","Μόνιμη"}. {"Ping query is incorrect","Το Ping είναι λανθασμένο"}. {"Ping","Ping"}. {"Please note that these options will only backup the builtin Mnesia database. If you are using the ODBC module, you also need to backup your SQL database separately.","Παρακαλώ σημειώστε ότι οι επιλογές αυτές θα αποθήκευσουν Αντιγράφο Ασφαλείας μόνο της ενσωματωμένης βάσης δεδομένων Mnesia. Εάν χρησιμοποιείτε το module ODBC, θα πρέπει επίσης να κάνετε χωριστά Αντιγράφο Ασφαλείας της SQL βάσης δεδομένων σας."}. {"Please, wait for a while before sending new voice request","Παρακαλώ, περιμένετε για λίγο πριν την αποστολή νέου αιτήματος φωνής"}. {"Pong","Πονγκ"}. {"Possessing 'ask' attribute is not allowed by RFC6121","Η κατοχή χαρακτηριστικού \"ask\" δεν επιτρέπεται από το RFC6121"}. {"Present real Jabber IDs to","Παρούσιαση πραγματικών ταυτοτήτων Jabber σε"}. {"Previous session not found","Η προηγούμενη περίοδος σύνδεσης χρήστη δεν βρέθηκε"}. {"Previous session PID has been killed","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID αφαιρέθηκε"}. {"Previous session PID has exited","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID τερμάτισε"}. {"Previous session PID is dead","Το προηγούμενο αναγνωριστικό περιόδου σύνδεσης PID είναι νεκρό"}. {"Previous session timed out","Η προηγούμενη περίοδος σύνδεσης χρήστη έληξε"}. {"private, ","ιδιωτικό, "}. {"Public","Δημόσιο"}. {"Publish model","Δημοσιεύστε μοντέλο"}. {"Publish-Subscribe","Δημοσίευση-Εγγραφή"}. {"PubSub subscriber request","Αίτηση συνδρομητή Δημοσίευσης-Εγγραφής"}. {"Purge all items when the relevant publisher goes offline","Διαγραφή όλων των στοιχείων όταν ο σχετικός εκδότης αποσυνδέεται"}. {"Push record not found","Το αρχείο Ώσης δεν βρέθηκε"}. {"Queries to the conference members are not allowed in this room","Ερωτήματα προς τα μέλη της διασκέψεως δεν επιτρέπονται σε αυτήν την αίθουσα"}. {"Query to another users is forbidden","Το ερώτημα σε άλλους χρήστες είναι απαγορευμένο"}. {"RAM and disc copy","Αντίγραφο μόνο σε RAM καί δίσκο"}. {"RAM copy","Αντίγραφο σε RAM"}. {"Really delete message of the day?","Πραγματικά να διαγραφεί το μήνυμα της ημέρας;"}. {"Receive notification from all descendent nodes","Λάβετε ειδοποίηση από όλους τους υπό-κόμβους"}. {"Receive notification from direct child nodes only","Λάβετε ειδοποίηση μόνο από direct child κόμβους"}. {"Receive notification of new items only","Λάβετε ειδοποίηση μόνο από νέα αντικείμενα"}. {"Receive notification of new nodes only","Λάβετε ειδοποίηση μόνο από νέους κόμβους"}. {"Recipient is not in the conference room","Ο παραλήπτης δεν είναι στην αίθουσα συνεδριάσεων"}. {"Register an XMPP account","Καταχωρείστε έναν XMPP λογαριασμό χρήστη"}. {"Registered Users","Εγγεγραμμένοι Χρήστες"}. {"Registered Users:","Εγγεγραμμένοι Χρήστες:"}. {"Register","Καταχωρήστε"}. {"Remote copy","Εξ αποστάσεως αντίγραφο"}. {"Remove All Offline Messages","Αφαίρεση όλων των μηνυμάτων χωρίς σύνδεση"}. {"Remove User","Αφαίρεση χρήστη"}. {"Remove","Αφαίρεση"}. {"Replaced by new connection","Αντικαταστάθηκε από μια νέα σύνδεση"}. {"Request has timed out","Το αίτημα έληξε"}. {"Request is ignored","Το αίτημα θα αγνοηθεί"}. {"Requested role","Αιτούμενος ρόλος"}. {"Resources","Πόροι"}. {"Restart Service","Επανεκκίνηση Υπηρεσίας"}. {"Restart","Επανεκκίνηση"}. {"Restore Backup from File at ","Επαναφορά Αντιγράφου Ασφαλείας από αρχείο στο "}. {"Restore binary backup after next ejabberd restart (requires less memory):","Επαναφορά δυαδικού αντιγράφου ασφαλείας μετά την επόμενη επανεκκίνηση του ejabberd (απαιτεί λιγότερη μνήμη):"}. {"Restore binary backup immediately:","Επαναφορά δυαδικού αντιγράφου ασφαλείας αμέσως:"}. {"Restore plain text backup immediately:","Επαναφορά αντιγράφου ασφαλείας από αρχείο κειμένου αμέσως:"}. {"Restore","Επαναφορά Αντιγράφου Ασφαλείας"}. {"Roles and Affiliations that May Retrieve Member List","Ρόλοι και δεσμοί που μπορούν να λάβουν την λίστα μελών"}. {"Roles for which Presence is Broadcasted","Ρόλοι των οποίων η παρουσία δηλώνεται δημόσια"}. {"Roles that May Send Private Messages","Ρόλοι που επιτρέπεται να αποστέλλουν ιδιωτικά μηνύματα"}. {"Room Configuration","Διαμόρφωση Αίθουσας σύνεδριασης"}. {"Room creation is denied by service policy","Άρνηση δημιουργίας αίθουσας, λόγω της τακτικής Παροχής Υπηρεσιών"}. {"Room description","Περιγραφή αίθουσας"}. {"Room Occupants","Συμμετεχόντες Αίθουσας σύνεδριασης"}. {"Room terminates","Τερματισμός Αίθουσας"}. {"Room title","Τίτλος Αίθουσας"}. {"Roster groups allowed to subscribe","Ομάδες Καταλόγου Επαφών μπορούν να εγγραφούν"}. {"Roster of ~ts","Καταλόγου Επαφών του ~ts"}. {"Roster size","Μέγεθος Καταλόγου Επαφών"}. {"Roster:","Καταλόγος Επαφών:"}. {"RPC Call Error","Σφάλμα RPC Κλήσης"}. {"Running Nodes","Ενεργοί Κόμβοι"}. {"~s invites you to the room ~s","~s Σας καλεί στο δωμάτιο ~s"}. {"Saturday","Σάββατο"}. {"Script check","Script ελέγχου"}. {"Search from the date","Αναζήτηση από της"}. {"Search Results for ","Αποτελέσματα αναζήτησης για "}. {"Search the text","Αναζήτηση του κειμένου"}. {"Search until the date","Αναζήτηση μέχρι της"}. {"Search users in ","Αναζήτηση χρηστών στο "}. {"Select All","Επιλογή όλων"}. {"Send announcement to all online users on all hosts","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Send announcement to all online users","Αποστολή ανακοίνωσης σε όλους τους συνδεδεμένους χρήστες"}. {"Send announcement to all users on all hosts","Αποστολή ανακοίνωσης σε όλους τους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Send announcement to all users","Αποστολή ανακοίνωσης σε όλους τους χρήστες"}. {"September","Σεπτέμβριος"}. {"Server:","Διακομιστής:"}. {"Service list retrieval timed out","Η λήψη της λίστας Υπηρεσιών έληξε"}. {"Session state copying timed out","Η αντιγραφή της καταστασης περιόδου σύνδεσης έληξε"}. {"Set message of the day and send to online users","Ορίστε μήνυμα ημέρας και αποστολή στους συνδεδεμένους χρήστες"}. {"Set message of the day on all hosts and send to online users","Ορίστε μήνυμα ημέρας και άμεση αποστολή στους συνδεδεμένους χρήστες σε όλους τους κεντρικούς υπολογιστές"}. {"Shared Roster Groups","Κοινές Ομάδες Καταλόγων Επαφών"}. {"Show Integral Table","Δείτε Ολοκληρωτικό Πίνακα"}. {"Show Ordinary Table","Δείτε Κοινό Πίνακα"}. {"Shut Down Service","Τερματισμός Υπηρεσίας"}. {"SOCKS5 Bytestreams","Bytestreams του SOCKS5"}. {"Some XMPP clients can store your password in the computer, but you should do this only in your personal computer for safety reasons.","Ορισμένοι πελάτες XMPP μπορούν να αποθηκεύσουν τον κωδικό πρόσβασής σας στον υπολογιστή, αλλά θα πρέπει να το κάνετε μόνο στον προσωπικό σας υπολογιστή για λόγους ασφαλείας."}. {"Specify the access model","Καθορίστε το μοντέλο πρόσβασης"}. {"Specify the event message type","Καθορίστε τον τύπο μηνύματος συμβάντος"}. {"Specify the publisher model","Καθορίστε το μοντέλο εκδότη"}. {"Stanza ID","Ταυτότητα Δωματίου"}. {"Statically specify a replyto of the node owner(s)","Προσδιορίστε (στατικά) το Απάντηση Προς του ιδιοκτήτη-ων του κόμβου"}. {"Statistics of ~p","Στατιστικές του ~p"}. {"Statistics","Στατιστικές"}. {"Stopped Nodes","Σταματημένοι Κόμβοι"}. {"Stop","Σταμάτημα"}. {"Storage Type","Τύπος Αποθήκευσης"}. {"Store binary backup:","Αποθηκεύση δυαδικού αντιγράφου ασφαλείας:"}. {"Store plain text backup:","Αποθηκεύση αντιγράφου ασφαλείας σε αρχείο κειμένου:"}. {"Stream management is already enabled","Η διαχείριση Ροών επιτρέπεται ηδη"}. {"Stream management is not enabled","Η διαχείριση Ροών δεν είναι ενεργοποιημένη"}. {"Subject","Θέμα"}. {"Submitted","Υποβλήθηκε"}. {"Submit","Υποβολή"}. {"Subscriber Address","Διεύθυνση Συνδρομητή"}. {"Subscribers may publish","Οι συνδρομητές μπορούν να δημοσιεύσουν"}. {"Subscription requests must be approved and only subscribers may retrieve items","Τα αιτήματα για συνδρομή πρέπει να εγκριθούν και μόνο οι συνδρομητές μπορούν να λάβουν αντικείμενα"}. {"Subscriptions are not allowed","Οι συνδρομές δεν επιτρέπονται"}. {"Subscription","Συνδρομή"}. {"Sunday","Κυριακή"}. {"Text associated with a picture","Το κείμενο σχετίστηκε με μία εικόνα"}. {"Text associated with a sound","Το κείμενο σχετίστηκε με έναν ήχο"}. {"Text associated with a video","Το κείμενο σχετίστηκε με ένα βίντεο"}. {"Text associated with speech","Το κείμενο σχετίστηκε με ομιλία"}. {"That nickname is already in use by another occupant","Αυτό το ψευδώνυμο είναι ήδη σε χρήση από άλλον συμμετέχοντα"}. {"That nickname is registered by another person","Αυτό το ψευδώνυμο είναι καταχωρημένο από άλλο πρόσωπο"}. {"The account already exists","Ο λογαριασμός υπάρχει ήδη"}. {"The account was not unregistered","Ο λογαριασμός δεν καταχωρήθηκε"}. {"The body text of the last received message","Ο κορμός του κειμένου του τελευταίου μηνύματος"}. {"The CAPTCHA is valid.","Το CAPTCHA είναι έγκυρο."}. {"The CAPTCHA verification has failed","Η επαλήθευση της εικόνας CAPTCHA απέτυχε"}. {"The captcha you entered is wrong","Το captcha που εισαγάγατε είναι λάθος"}. {"The child nodes (leaf or collection) associated with a collection","Οι θυγατρικοί κόμβοι (leaf ή collection) που σχετίζονται με μια συλλογή"}. {"The collections with which a node is affiliated","Οι συλλογές με τις οποίες ένας κόμβος σχετίζεται"}. {"The DateTime at which a leased subscription will end or has ended","Ο Χρόνος στον οποίο μια μισθωμένη συνδρομή θα Εκπνεύσει ή Τελειώσει"}. {"The datetime when the node was created","Η χρονοσφραγίδα δημιουργίας του κόμβου"}. {"The default language of the node","Η προρυθμισμένη γλώσσα του κόμβου"}. {"The feature requested is not supported by the conference","Η λειτουργία που ζητήθηκε δεν υποστηρίζεται από τη διάσκεψη"}. {"The JID of the node creator","Το JID του δημιουγού του κόμβου"}. {"The JIDs of those to contact with questions","Το JID αυτών με τους οποίους θα επικοινωνήσετε με ερωτήσεις"}. {"The JIDs of those with an affiliation of owner","Το JID αυτών που σχετίζονται με τον ιδιοκτήτη"}. {"The JIDs of those with an affiliation of publisher","Το JID αυτών που σχετίζονται με τον εκδότη"}. {"The list of JIDs that may associate leaf nodes with a collection","Λίστα των JIDs που μπορούν να σχετίζουν leaf κόμβους με μια Συλλογή"}. {"The maximum number of child nodes that can be associated with a collection","Το μέγιστο πλήθος θυγατρικών κόμβων που μπορούν να συσχετιστούν με μία Συλλογή"}. {"The minimum number of milliseconds between sending any two notification digests","Το ελάχιστο πλήθος χιλιοστών του δευτερολέπτου μεταξύ της αποστολής δύο συγχωνεύσεων ειδοποιήσεων"}. {"The name of the node","Το όνομα του κόμβου"}. {"The node is a collection node","Ο κόμβος είναι κόμβος Συλλογής"}. {"The node is a leaf node (default)","Ο κόμβος είναι leaf κόμβος (προεπιλογή)"}. {"The NodeID of the relevant node","Το NodeID του σχετικού κόμβου"}. {"The number of pending incoming presence subscription requests","Το πλήθος των αιτημάτων εισερχομένων συνδρομών παρουσίας σε αναμονή"}. {"The number of subscribers to the node","Το πλήθος των συνδρομητών στον κόμβο"}. {"The number of unread or undelivered messages","Το πλήθος των μη αναγνωσμένων ή μη παραδοτέων μηνυμάτων"}. {"The password contains unacceptable characters","Ο κωδικός πρόσβασης περιέχει μη αποδεκτούς χαρακτήρες"}. {"The password is too weak","Ο κωδικός πρόσβασης είναι πολύ αδύναμος"}. {"the password is","ο κωδικός πρόσβασης είναι"}. {"The password of your XMPP account was successfully changed.","Ο κωδικός πρόσβασης του XMPP λογαριασμού σας έχει αλλάξει επιτυχώς."}. {"The password was not changed","Ο κωδικός πρόσβασης δεν άλλαξε"}. {"The passwords are different","Οι κωδικοί πρόσβασης δεν ταιριάζουν"}. {"The presence states for which an entity wants to receive notifications","Η παρουσία δηλώνει για ποιους θέλει κάποιος να λαμβάνει ειδοποιήσεις"}. {"The query is only allowed from local users","Το ερώτημα επιτρέπεται μόνο από τοπικούς χρήστες"}. {"The query must not contain <item/> elements","Το ερώτημα δεν πρέπει να περιέχει στοιχείο <item/>"}. {"The room subject can be modified by participants","Το θέμα μπορεί να τροποποιηθεί από τους συμμετέχοντες"}. {"The sender of the last received message","Ο αποστολέας του τελευταίου εισερχομένου μηνύματος"}. {"The stanza MUST contain only one <active/> element, one <default/> element, or one <list/> element","Η stanza ΠΡΕΠΕΙ να περιέχει μόνο ένα στοιχείο <active />, ένα στοιχείο <default /> ή ένα στοιχείο <list />"}. {"The subscription identifier associated with the subscription request","Το αναγνωριστικό συνδρομής συσχετίστηκε με το αίτημα συνδρομής"}. {"The type of node data, usually specified by the namespace of the payload (if any)","Ο τύπος των δεδομένων του κόμβου συνήθως προσδιορίζεται από το namespace του φόρτου εργασιών (αν υπάρχουν)"}. {"The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.","Το URL ενός μετασχηματισμού XSL το οποίο μπορεί να εφαρμοστεί σε φόρτους εργασίας για να παραχθεί το κατάλληλο στοιχείο του σώματος του μηνύματος."}. {"The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine","Το URL ενός μετασχηματισμού XSL, το οποίο μπορεί να εφαρμοστεί στους τύπους φόρτου εργασίας για να παραχθεί έγκυρο αποτέλεσμα Data Forms, τέτοιο που ο πελάτης μπορεί να εμφανίσει, χρησιμοποιώντας μια ευρείας χρήσης μηχανή επεξεργασίας Data Forms"}. {"The username is not valid","Το όνομα Χρήστη δεν είναι έγκυρο"}. {"There was an error changing the password: ","Παρουσιάστηκε σφάλμα κατά την αλλαγή του κωδικού πρόσβασης: "}. {"There was an error creating the account: ","Υπήρξε ένα σφάλμα κατά τη δημιουργία του λογαριασμού: "}. {"There was an error deleting the account: ","Υπήρξε ένα σφάλμα κατά τη διαγραφή του λογαριασμού: "}. {"This is case insensitive: macbeth is the same that MacBeth and Macbeth.","Αυτό σημαίνει ότι δεν έχει σημασία αν είναι κεφαλαία ή πεζά γράμματα: \"κατσαντώνης\" είναι το ίδιο με \"ΚατσΑντώνης\" , όπως και \"Κατσαντώνης\"."}. {"This page allows to register an XMPP account in this XMPP server. Your JID (Jabber ID) will be of the form: username@server. Please read carefully the instructions to fill correctly the fields.","Αυτή η σελίδα επιτρέπει την εγγραφή ενός λογαριασμού XMPP σε αυτόν τον διακομιστή XMPP. Το JID (Jabber ID) θα έχει τη μορφή: όνομαχρήστη@διακομιστής. Διαβάστε προσεκτικά τις οδηγίες για να συμπληρώσετε σωστά τα πεδία."}. {"This page allows to unregister an XMPP account in this XMPP server.","Αυτή η σελίδα επιτρέπει την κατάργηση εγγραφής ενός λογαριασμού XMPP σε αυτόν τον διακομιστή XMPP."}. {"This room is not anonymous","Η αίθουσα αυτή δεν είναι ανώνυμη"}. {"This service can not process the address: ~s","Αυτή η υπηρεσία δεν μπορεί να επεξεργαστεί την διεύθυνση: ~s"}. {"Thursday","Πέμπτη"}. {"Time delay","Χρόνος καθυστέρησης"}. {"Timed out waiting for stream resumption","Υπερέβην το όριο αναμονής για επανασύνδεση της Ροής"}. {"Time","Χρόνος"}. {"To register, visit ~s","Για να εγγραφείτε, επισκεφθείτε το ~s"}. {"To ~ts","Προς ~ts"}. {"Token TTL","Διακριτικό TTL"}. {"Too many active bytestreams","Πάρα πολλά ενεργά bytestreams"}. {"Too many CAPTCHA requests","Πάρα πολλά αιτήματα CAPTCHA"}. {"Too many child elements","Πάρα πολλά θυγατρικά στοιχεία"}. {"Too many <item/> elements","Πάρα πολλά στοιχεία <item/>"}. {"Too many <list/> elements","Πάρα πολλά στοιχεία <list/>"}. {"Too many (~p) failed authentications from this IP address (~s). The address will be unblocked at ~s UTC","Πάρα πολλές (~p) αποτυχημένες προσπάθειες σύνδεσης από την IP σας (~s). Άρση φραγής στις ~s UTC"}. {"Too many receiver fields were specified","Πάρα πολλά πεδία δεκτών προσδιορίστηκαν"}. {"Too many unacked stanzas","Πάρα πολλές μη αναγνωρισμένες stanzas"}. {"Too many users in this conference","Πάρα πολλοί χρήστες σε αυτή τη διάσκεψη"}. {"Total rooms","Συνολικές Αίθουσες σύνεδριασης"}. {"To","Προς"}. {"Traffic rate limit is exceeded","Υπέρφορτωση"}. {"Transactions Aborted:","Αποτυχημένες συναλλαγές:"}. {"Transactions Committed:","Παραδοθείσες συναλλαγές:"}. {"Transactions Logged:","Καταγεγραμμένες συναλλαγές:"}. {"Transactions Restarted:","Επανειλημμένες συναλλαγές:"}. {"~ts's Offline Messages Queue","~ts's Χωρίς Σύνδεση Μηνύματα"}. {"Tuesday","Τρίτη"}. {"Unable to generate a CAPTCHA","Αδύνατη η δημιουργία CAPTCHA"}. {"Unable to register route on existing local domain","Δεν είναι δυνατή η καταχώρηση της διαδρομής σε υπάρχοντα τοπικό τομέα"}. {"Unauthorized","Χωρίς Εξουσιοδότηση"}. {"Unexpected action","Απροσδόκητη ενέργεια"}. {"Unexpected error condition: ~p","Απροσδόκητες συνθήκες σφάλματος: ~p"}. {"Unregister an XMPP account","Καταργήση λογαριασμού XMPP"}. {"Unregister","Καταργήση εγγραφής"}. {"Unselect All","Αποεπιλογή όλων"}. {"Unsupported <index/> element","Μη υποστηριζόμενο στοιχείο <index />"}. {"Unsupported version","Μη υποστηριζόμενη έκδοση"}. {"Update message of the day (don't send)","Ενημέρωση μηνύματος ημέρας (χωρίς άμεση αποστολή)"}. {"Update message of the day on all hosts (don't send)","Ενημέρωση μηνύματος ημέρας σε όλους τους κεντρικούς υπολογιστές (χωρίς άμεση αποστολή)"}. {"Update plan","Σχέδιο ενημέρωσης"}. {"Update ~p","Ενημέρωση ~p"}. {"Update script","Προγράμα ενημέρωσης"}. {"Update","Ενημέρωση"}. {"Uptime:","Χρόνος σε λειτουργία:"}. {"URL for Archived Discussion Logs","URL αρχειοθετημένων καταγραφών συζητήσεων"}. {"User already exists","Ο χρήστης υπάρχει ήδη"}. {"User JID","JID Χρήστη"}. {"User (jid)","Χρήστης (jid)"}. {"User Management","Διαχείριση χρηστών"}. {"User removed","Ο Χρήστης αφαιρέθηκε"}. {"User session not found","Η περίοδος σύνδεσης χρήστη δεν βρέθηκε"}. {"User session terminated","Η περίοδος σύνδεσης χρήστη τερματίστηκε"}. {"User ~ts","Χρήστης ~ts"}. {"Username:","Όνομα χρήστη:"}. {"Users are not allowed to register accounts so quickly","Οι χρήστες δεν επιτρέπεται να δημιουργούν λογαριασμούς τόσο γρήγορα"}. {"Users Last Activity","Τελευταία Δραστηριότητα Χρήστη"}. {"Users","Χρήστες"}. {"User","Χρήστης"}. {"Validate","Επαληθεύστε"}. {"Value 'get' of 'type' attribute is not allowed","Η τιμή 'get' του 'type' δεν επιτρέπεται"}. {"Value of '~s' should be boolean","Η τιμή του '~s' πρέπει να είναι boolean"}. {"Value of '~s' should be datetime string","Η τιμή του '~s' θα πρέπει να είναι χρονοσειρά"}. {"Value of '~s' should be integer","Η τιμή του '~s' θα πρέπει να είναι ακέραιος"}. {"Value 'set' of 'type' attribute is not allowed","Δεν επιτρέπεται η παράμετρος 'set' του 'type'"}. {"vCard User Search","vCard Αναζήτηση χρηστών"}. {"View Queue","Εμφάνιση λίστας αναμονής"}. {"View Roster","Εμφάνιση λίστας Επαφών"}. {"Virtual Hosts","Eικονικοί κεντρικοί υπολογιστές"}. {"Visitors are not allowed to change their nicknames in this room","Οι επισκέπτες δεν επιτρέπεται να αλλάξουν τα ψευδώνυμα τους σε αυτή την αίθουσα"}. {"Visitors are not allowed to send messages to all occupants","Οι επισκέπτες δεν επιτρέπεται να στείλουν μηνύματα σε όλους τους συμμετέχοντες"}. {"Visitor","Επισκέπτης"}. {"Voice requests are disabled in this conference","Τα αιτήματα φωνής είναι απενεργοποιημένα, σε αυτό το συνέδριο"}. {"Voice request","Αίτημα φωνής"}. {"Wednesday","Τετάρτη"}. {"When a new subscription is processed and whenever a subscriber comes online","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία και όποτε ένας συνδρομητής συνδεθεί"}. {"When a new subscription is processed","Όταν μία νέα συνδρομή βρίσκεται εν επεξεργασία"}. {"When to send the last published item","Πότε να αποσταλεί το τελευταίο στοιχείο που δημοσιεύθηκε"}. {"Whether an entity wants to receive an XMPP message body in addition to the payload format","Εάν κάποιος θέλει να λάβει το κυρίως XMPP μήνυμα, επιπροσθέτως του τύπου φόρτου εργασιών"}. {"Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually","Εάν μία οντότητα επιθυμεί να λαμβάνει αθροιστικές συνόψεις των ειδοποιήσεων ή όλες τις ειδοποιήσεις ξεχωριστά"}. {"Whether an entity wants to receive or disable notifications","Εάν μία οντότητα επιθυμεί να λαμβάνει ή όχι ειδοποιήσεις"}. {"Whether owners or publisher should receive replies to items","Εάν οι ιδιοκτήτες επιθυμούν να λαμβάνουν απαντήσεις στα αντικείμενα"}. {"Whether the node is a leaf (default) or a collection","Εάν ο κόμβος είναι leaf (προεπιλογή) ή συλλογή"}. {"Whether to allow subscriptions","Εάν επιτρέπονται συνδρομές"}. {"Whether to make all subscriptions temporary, based on subscriber presence","Αν επιτρέπεται να γίνουν όλες οι συνδρομές προσωρινές, βασιζόμενοι στην παρουσία του συνδρομητή"}. {"Whether to notify owners about new subscribers and unsubscribes","Αν πρέπει να ειδοποιούνται οι ιδιοκτήτες για νέους συνδρομητές και αποχωρήσεις"}. {"Who may associate leaf nodes with a collection","Ποιός μπορεί να συσχετίζει leaf nodes με μία συλλογή"}. {"Wrong parameters in the web formulary","Εσφαλμένες παράμετροι στην διαμόρφωση τυπικότητας του δυκτίου"}. {"Wrong xmlns","Εσφαλμένο xmlns"}. {"XMPP Account Registration","Εγγραφή λογαριασμού XMPP"}. {"XMPP Domains","Ονόματα χώρου XMPP"}. {"XMPP Show Value of Away","Δείξε τιμή XMPP Απεμακρύνθην"}. {"XMPP Show Value of Chat","Δείξε τιμή XMPP Αξία Συνομιλίας"}. {"XMPP Show Value of DND (Do Not Disturb)","Δείξε τιμή XMPP Αξία του Μην Ενοχλείτε"}. {"XMPP Show Value of XA (Extended Away)","Δείξε τιμή XMPP Αξία του Λίαν Απομακρυσμένος"}. {"XMPP URI of Associated Publish-Subscribe Node","XMPP URI του συσχετισμένου κόμβου Δημοσίευσης-Εγγραφής"}. {"You are being removed from the room because of a system shutdown","Απαιτείται η απομάκρυνσή σας από την αίθουσα, λόγω τερματισμού συστήματος"}. {"You are not joined to the channel","Δεν λαμβάνετε μέρος στο κανάλι"}. {"You can later change your password using an XMPP client.","Μπορείτε αργότερα να αλλάξετε τον κωδικό πρόσβασής σας χρησιμοποιώντας ένα πρόγραμμα-πελάτη XMPP."}. {"You have been banned from this room","Σας έχει απαγορευθεί η είσοδος σε αυτή την αίθουσα"}. {"You have joined too many conferences","Είσθε σε πάρα πολλά συνέδρια"}. {"You must fill in field \"Nickname\" in the form","Απαιτείται να συμπληρώσετε το πεδίο \"Ψευδώνυμο\" στη φόρμα"}. {"You need a client that supports x:data and CAPTCHA to register","Χρειάζεστε έναν πελάτη που να υποστηρίζει x:data και CAPTCHA"}. {"You need a client that supports x:data to register the nickname","Χρειάζεστε έναν πελάτη που να υποστηρίζει x:data για εγγραφή του ψευδώνυμου"}. {"You need an x:data capable client to search","Χρειάζεστε έναν πελάτη που να υποστηρίζει x:data για να αναζητήσετε"}. {"Your active privacy list has denied the routing of this stanza.","Ο ενεργός κατάλογος απορρήτου, έχει αρνηθεί τη δρομολόγηση αυτής της στροφής (stanza)."}. {"Your contact offline message queue is full. The message has been discarded.","Η μνήμη μηνυμάτων χωρίς σύνδεση είναι πλήρης. Το μήνυμα έχει απορριφθεί."}. {"Your subscription request and/or messages to ~s have been blocked. To unblock your subscription request, visit ~s","Τα μηνύματά σας προς ~s είναι αποκλεισμένα. Για αποδεσμεύση, επισκεφθείτε ~s"}. {"Your XMPP account was successfully registered.","Ο λογαριασμός σας XMPP καταχωρήθηκε με επιτυχία."}. {"Your XMPP account was successfully unregistered.","Ο XMPP λογαριασμός σας διαγράφηκε με επιτυχία."}. {"You're not allowed to create nodes","Δεν σας επιτρέπεται η δημιουργία κόμβων"}. ��������������������������������������ejabberd-21.12/src/���������������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�014421� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_vcard.erl��������������������������������������������������������������������0000644�0002322�0002322�00000062265�14154362354�017076� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : mod_vcard.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Vcard management %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_vcard). -author('alexey@process-one.net'). -protocol({xep, 54, '1.2'}). -protocol({xep, 55, '1.3'}). -behaviour(gen_server). -behaviour(gen_mod). -export([start/2, stop/1, get_sm_features/5, mod_options/1, mod_doc/0, process_local_iq/1, process_sm_iq/1, string2lower/1, remove_user/2, export/1, import_info/0, import/5, import_start/2, depends/2, process_search/1, process_vcard/1, get_vcard/2, disco_items/5, disco_features/5, disco_identity/5, vcard_iq_set/1, mod_opt_type/1, set_vcard/3, make_vcard_search/4]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([route/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_vcard.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -define(VCARD_CACHE, vcard_cache). -callback init(binary(), gen_mod:opts()) -> any(). -callback stop(binary()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback get_vcard(binary(), binary()) -> {ok, [xmlel()]} | error. -callback set_vcard(binary(), binary(), xmlel(), #vcard_search{}) -> {atomic, any()}. -callback search_fields(binary()) -> [{binary(), binary()}]. -callback search_reported(binary()) -> [{binary(), binary()}]. -callback search(binary(), [{binary(), [binary()]}], boolean(), infinity | pos_integer()) -> [{binary(), binary()}]. -callback remove_user(binary(), binary()) -> {atomic, any()}. -callback is_search_supported(binary()) -> boolean(). -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). -record(state, {hosts :: [binary()], server_host :: binary()}). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_VCARD, ?MODULE, process_sm_iq), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), MyHosts = gen_mod:get_opt_hosts(Opts), Search = mod_vcard_opt:search(Opts), if Search -> lists:foreach( fun(MyHost) -> ejabberd_hooks:add( disco_local_items, MyHost, ?MODULE, disco_items, 100), ejabberd_hooks:add( disco_local_features, MyHost, ?MODULE, disco_features, 100), ejabberd_hooks:add( disco_local_identity, MyHost, ?MODULE, disco_identity, 100), gen_iq_handler:add_iq_handler( ejabberd_local, MyHost, ?NS_SEARCH, ?MODULE, process_search), gen_iq_handler:add_iq_handler( ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard), gen_iq_handler:add_iq_handler( ejabberd_local, MyHost, ?NS_DISCO_ITEMS, mod_disco, process_local_iq_items), gen_iq_handler:add_iq_handler( ejabberd_local, MyHost, ?NS_DISCO_INFO, mod_disco, process_local_iq_info), case Mod:is_search_supported(Host) of false -> ?WARNING_MSG("vCard search functionality is " "not implemented for ~ts backend", [mod_vcard_opt:db_type(Opts)]); true -> ejabberd_router:register_route( MyHost, Host, {apply, ?MODULE, route}) end end, MyHosts); true -> ok end, {ok, #state{hosts = MyHosts, server_host = Host}}. handle_call(Call, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Call]), {noreply, State}. handle_cast(Cast, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Cast]), {noreply, State}. handle_info({route, Packet}, State) -> try route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{hosts = MyHosts, server_host = Host}) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_set, 50), Mod = gen_mod:db_mod(Host, ?MODULE), Mod:stop(Host), lists:foreach( fun(MyHost) -> ejabberd_router:unregister_route(MyHost), ejabberd_hooks:delete(disco_local_items, MyHost, ?MODULE, disco_items, 100), ejabberd_hooks:delete(disco_local_features, MyHost, ?MODULE, disco_features, 100), ejabberd_hooks:delete(disco_local_identity, MyHost, ?MODULE, disco_identity, 100), gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_SEARCH), gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO) end, MyHosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); route(_) -> ok. -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | empty | {result, [binary()]}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_features(Acc, _From, _To, Node, _Lang) -> case Node of <<"">> -> case Acc of {result, Features} -> {result, [?NS_VCARD | Features]}; empty -> {result, [?NS_VCARD]} end; _ -> Acc end. -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_vcard_opt:vcard(ServerHost) of undefined -> #vcard_temp{fn = <<"ejabberd">>, url = ejabberd_config:get_uri(), desc = misc:get_descr(Lang, ?T("Erlang XMPP Server")), bday = <<"2002-11-16">>}; V -> V end, xmpp:make_iq_result(IQ, VCard). -spec process_sm_iq(iq()) -> iq(). process_sm_iq(#iq{type = set, lang = Lang, from = From} = IQ) -> #jid{lserver = LServer} = From, case lists:member(LServer, ejabberd_option:hosts()) of true -> case ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []) of drop -> ignore; #stanza_error{} = Err -> xmpp:make_error(IQ, Err); _ -> xmpp:make_iq_result(IQ) end; false -> Txt = ?T("The query is only allowed from local users"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end; process_sm_iq(#iq{type = get, from = From, to = To, lang = Lang} = IQ) -> #jid{luser = LUser, lserver = LServer} = To, case get_vcard(LUser, LServer) of error -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); [] -> xmpp:make_iq_result(IQ, #vcard_temp{}); Els -> IQ#iq{type = result, to = From, from = To, sub_els = Els} end. -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_vcard(#iq{type = get, lang = Lang} = IQ) -> xmpp:make_iq_result( IQ, #vcard_temp{fn = <<"ejabberd/mod_vcard">>, url = ejabberd_config:get_uri(), desc = misc:get_descr(Lang, ?T("ejabberd vCard module"))}). -spec process_search(iq()) -> iq(). process_search(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang)); process_search(#iq{type = set, to = To, lang = Lang, sub_els = [#search{xdata = #xdata{type = submit, fields = Fs}}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), ResultXData = search_result(Lang, To, ServerHost, Fs), xmpp:make_iq_result(IQ, #search{xdata = ResultXData}); process_search(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Incorrect data form"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). -spec disco_items({error, stanza_error()} | {result, [disco_item()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [disco_item()]}. disco_items(empty, _From, _To, <<"">>, _Lang) -> {result, []}; disco_items(empty, _From, _To, _Node, Lang) -> {error, xmpp:err_item_not_found(?T("No services available"), Lang)}; disco_items(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. disco_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_features(Acc, _From, _To, <<"">>, _Lang) -> Features = case Acc of {result, Fs} -> Fs; empty -> [] end, {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_SEARCH | Features]}; disco_features(empty, _From, _To, _Node, Lang) -> Txt = ?T("No features available"), {error, xmpp:err_item_not_found(Txt, Lang)}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_identity(Acc, _From, To, <<"">>, Lang) -> Host = ejabberd_router:host_of_route(To#jid.lserver), Name = mod_vcard_opt:name(Host), [#identity{category = <<"directory">>, type = <<"user">>, name = translate:translate(Lang, Name)}|Acc]; disco_identity(Acc, _From, _To, _Node, _Lang) -> Acc. -spec get_vcard(binary(), binary()) -> [xmlel()] | error. get_vcard(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Result = case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?VCARD_CACHE, {LUser, LServer}, fun() -> Mod:get_vcard(LUser, LServer) end); false -> Mod:get_vcard(LUser, LServer) end, case Result of {ok, Els} -> Els; error -> error end. -spec make_vcard_search(binary(), binary(), binary(), xmlel()) -> #vcard_search{}. make_vcard_search(User, LUser, LServer, VCARD) -> FN = fxml:get_path_s(VCARD, [{elem, <<"FN">>}, cdata]), Family = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"FAMILY">>}, cdata]), Given = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"GIVEN">>}, cdata]), Middle = fxml:get_path_s(VCARD, [{elem, <<"N">>}, {elem, <<"MIDDLE">>}, cdata]), Nickname = fxml:get_path_s(VCARD, [{elem, <<"NICKNAME">>}, cdata]), BDay = fxml:get_path_s(VCARD, [{elem, <<"BDAY">>}, cdata]), CTRY = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"CTRY">>}, cdata]), Locality = fxml:get_path_s(VCARD, [{elem, <<"ADR">>}, {elem, <<"LOCALITY">>}, cdata]), EMail1 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, {elem, <<"USERID">>}, cdata]), EMail2 = fxml:get_path_s(VCARD, [{elem, <<"EMAIL">>}, cdata]), OrgName = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGNAME">>}, cdata]), OrgUnit = fxml:get_path_s(VCARD, [{elem, <<"ORG">>}, {elem, <<"ORGUNIT">>}, cdata]), EMail = case EMail1 of <<"">> -> EMail2; _ -> EMail1 end, LFN = string2lower(FN), LFamily = string2lower(Family), LGiven = string2lower(Given), LMiddle = string2lower(Middle), LNickname = string2lower(Nickname), LBDay = string2lower(BDay), LCTRY = string2lower(CTRY), LLocality = string2lower(Locality), LEMail = string2lower(EMail), LOrgName = string2lower(OrgName), LOrgUnit = string2lower(OrgUnit), US = {LUser, LServer}, #vcard_search{us = US, user = {User, LServer}, luser = LUser, fn = FN, lfn = LFN, family = Family, lfamily = LFamily, given = Given, lgiven = LGiven, middle = Middle, lmiddle = LMiddle, nickname = Nickname, lnickname = LNickname, bday = BDay, lbday = LBDay, ctry = CTRY, lctry = LCTRY, locality = Locality, llocality = LLocality, email = EMail, lemail = LEMail, orgname = OrgName, lorgname = LOrgName, orgunit = OrgUnit, lorgunit = LOrgUnit}. -spec vcard_iq_set(iq()) -> iq() | {stop, stanza_error()}. vcard_iq_set(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> #jid{user = User, lserver = LServer} = From, case set_vcard(User, LServer, VCard) of {error, badarg} -> %% Should not be here? Txt = ?T("Nodeprep has failed"), {stop, xmpp:err_internal_server_error(Txt, Lang)}; ok -> IQ end; vcard_iq_set(Acc) -> Acc. -spec set_vcard(binary(), binary(), xmlel() | vcard_temp()) -> {error, badarg|binary()} | ok. set_vcard(User, LServer, VCARD) -> case jid:nodeprep(User) of error -> {error, badarg}; LUser -> VCardEl = xmpp:encode(VCARD), VCardSearch = make_vcard_search(User, LUser, LServer, VCardEl), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_vcard(LUser, LServer, VCardEl, VCardSearch) of {atomic, ok} -> ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)), ok; {atomic, Error} -> {error, Error} end end. -spec string2lower(binary()) -> binary(). string2lower(String) -> case stringprep:tolower_nofilter(String) of Lower when is_binary(Lower) -> Lower; error -> String end. -spec mk_tfield(binary(), binary(), binary()) -> xdata_field(). mk_tfield(Label, Var, Lang) -> #xdata_field{type = 'text-single', label = translate:translate(Lang, Label), var = Var}. -spec mk_field(binary(), binary()) -> xdata_field(). mk_field(Var, Val) -> #xdata_field{var = Var, values = [Val]}. -spec mk_search_form(jid(), binary(), binary()) -> search(). mk_search_form(JID, ServerHost, Lang) -> Title = <<(translate:translate(Lang, ?T("Search users in ")))/binary, (jid:encode(JID))/binary>>, Mod = gen_mod:db_mod(ServerHost, ?MODULE), SearchFields = Mod:search_fields(ServerHost), Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields], X = #xdata{type = form, title = Title, instructions = [make_instructions(Mod, Lang)], fields = Fs}, #search{instructions = translate:translate( Lang, ?T("You need an x:data capable client to search")), xdata = X}. -spec make_instructions(module(), binary()) -> binary(). make_instructions(Mod, Lang) -> Fill = translate:translate( Lang, ?T("Fill in the form to search for any matching " "XMPP User")), Add = translate:translate( Lang, ?T(" (Add * to the end of field to match substring)")), case Mod of mod_vcard_mnesia -> Fill; _ -> str:concat(Fill, Add) end. -spec search_result(binary(), jid(), binary(), [xdata_field()]) -> xdata(). search_result(Lang, JID, ServerHost, XFields) -> Mod = gen_mod:db_mod(ServerHost, ?MODULE), Reported = [mk_tfield(Label, Var, Lang) || {Label, Var} <- Mod:search_reported(ServerHost)], #xdata{type = result, title = <<(translate:translate(Lang, ?T("Search Results for ")))/binary, (jid:encode(JID))/binary>>, reported = Reported, items = lists:map(fun (Item) -> item_to_field(Item) end, search(ServerHost, XFields))}. -spec item_to_field([{binary(), binary()}]) -> [xdata_field()]. item_to_field(Items) -> [mk_field(Var, Value) || {Var, Value} <- Items]. -spec search(binary(), [xdata_field()]) -> [binary()]. search(LServer, XFields) -> Data = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- XFields], Mod = gen_mod:db_mod(LServer, ?MODULE), AllowReturnAll = mod_vcard_opt:allow_return_all(LServer), MaxMatch = mod_vcard_opt:matches(LServer), Mod:search(LServer, Data, AllowReturnAll, MaxMatch). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), ets_cache:delete(?VCARD_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Host, Opts), ets_cache:new(?VCARD_CACHE, CacheOpts); false -> ets_cache:delete(?VCARD_CACHE) end. -spec cache_opts(binary(), gen_mod:opts()) -> [proplists:property()]. cache_opts(_Host, Opts) -> MaxSize = mod_vcard_opt:cache_size(Opts), CacheMissed = mod_vcard_opt:cache_missed(Opts), LifeTime = mod_vcard_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_vcard_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. import_info() -> [{<<"vcard">>, 3}, {<<"vcard_search">>, 24}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). depends(_Host, _Opts) -> []. mod_opt_type(allow_return_all) -> econf:bool(); mod_opt_type(name) -> econf:binary(); mod_opt_type(matches) -> econf:pos_int(infinity); mod_opt_type(search) -> econf:bool(); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity); mod_opt_type(vcard) -> econf:vcard_temp(). mod_options(Host) -> [{allow_return_all, false}, {host, <<"vjud.", Host/binary>>}, {hosts, []}, {matches, 30}, {search, false}, {name, ?T("vCard User Search")}, {vcard, undefined}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module allows end users to store and retrieve " "their vCard, and to retrieve other users vCards, " "as defined in https://xmpp.org/extensions/xep-0054.html" "[XEP-0054: vcard-temp]. The module also implements an " "uncomplicated Jabber User Directory based on the vCards " "of these users. Moreover, it enables the server to send " "its vCard when queried."), opts => [{allow_return_all, #{value => "true | false", desc => ?T("This option enables you to specify if search " "operations with empty input fields should return " "all users who added some information to their vCard. " "The default value is 'false'.")}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"vjud.\". " "The keyword '@HOST@' is replaced with the real virtual host name.")}}, {name, #{value => ?T("Name"), desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. The default is 'vCard User Search'.")}}, {matches, #{value => "pos_integer() | infinity", desc => ?T("With this option, the number of reported search results " "can be limited. If the option's value is set to 'infinity', " "all search results are reported. The default value is '30'.")}}, {search, #{value => "true | false", desc => ?T("This option specifies whether the search functionality " "is enabled or not. If disabled, the options 'hosts', 'name' " "and 'vcard' will be ignored and the Jabber User Directory " "service will not appear in the Service Discovery item list. " "The default value is 'false'.")}}, {db_type, #{value => "mnesia | sql | ldap", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, {vcard, #{value => ?T("vCard"), desc => ?T("A custom vCard of the server that will be displayed " "by some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML representation " "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => [{?T("For example, the following XML representation of vCard:"), ["<vCard xmlns='vcard-temp'>", " <FN>Conferences</FN>", " <ADR>", " <WORK/>", " <STREET>Elm Street</STREET>", " </ADR>", "</vCard>"]}, {?T("will be translated to:"), ["vcard:", " fn: Conferences", " adr:", " -", " work: true", " street: Elm Street"]}]}}]}. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_multicast.erl����������������������������������������������������������������0000644�0002322�0002322�00000111012�14154362354�017765� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : mod_multicast.erl %%% Author : Badlop <badlop@process-one.net> %%% Purpose : Extended Stanza Addressing (XEP-0033) support %%% Created : 29 May 2007 by Badlop <badlop@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_multicast). -author('badlop@process-one.net'). -protocol({xep, 33, '1.1'}). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, user_send_packet/1]). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -export([purge_loop/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -record(multicastc, {rserver :: binary(), response, ts :: integer()}). -type limit_value() :: {default | custom, integer()}. -record(limits, {message :: limit_value(), presence :: limit_value()}). -record(service_limits, {local :: #limits{}, remote :: #limits{}}). -record(state, {lserver :: binary(), lservice :: binary(), access :: atom(), service_limits :: #service_limits{}}). -type state() :: #state{}. %% All the elements are of type value() -define(VERSION_MULTICAST, <<"$Revision: 440 $ ">>). -define(PURGE_PROCNAME, ejabberd_mod_multicast_purgeloop). -define(MAXTIME_CACHE_POSITIVE, 86400). -define(MAXTIME_CACHE_NEGATIVE, 86400). -define(MAXTIME_CACHE_NEGOTIATING, 600). -define(CACHE_PURGE_TIMER, 86400000). -define(DISCO_QUERY_TIMEOUT, 10000). -define(DEFAULT_LIMIT_LOCAL_MESSAGE, 100). -define(DEFAULT_LIMIT_LOCAL_PRESENCE, 100). -define(DEFAULT_LIMIT_REMOTE_MESSAGE, 20). -define(DEFAULT_LIMIT_REMOTE_PRESENCE, 20). start(LServerS, Opts) -> gen_mod:start_child(?MODULE, LServerS, Opts). stop(LServerS) -> gen_mod:stop_child(?MODULE, LServerS). reload(LServerS, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(LServerS, ?MODULE), gen_server:cast(Proc, {reload, NewOpts, OldOpts}). -define(SETS, gb_sets). user_send_packet({#presence{} = Packet, C2SState} = Acc) -> case xmpp:get_subtag(Packet, #addresses{}) of #addresses{list = Addresses} -> {CC, BCC, _Invalid, _Delivered} = partition_addresses(Addresses), NewState = lists:foldl( fun(Address, St) -> case Address#address.jid of #jid{} = JID -> LJID = jid:tolower(JID), #{pres_a := PresA} = St, A = case Packet#presence.type of available -> ?SETS:add_element(LJID, PresA); unavailable -> ?SETS:del_element(LJID, PresA); _ -> PresA end, St#{pres_a => A}; undefined -> St end end, C2SState, CC ++ BCC), {Packet, NewState}; false -> Acc end; user_send_packet(Acc) -> Acc. %%==================================================================== %% gen_server callbacks %%==================================================================== -spec init(list()) -> {ok, state()}. init([LServerS|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(LServerS, ?MODULE), [LServiceS|_] = gen_mod:get_opt_hosts(Opts), Access = mod_multicast_opt:access(Opts), SLimits = build_service_limit_record(mod_multicast_opt:limits(Opts)), create_cache(), try_start_loop(), ejabberd_router_multicast:register_route(LServerS), ejabberd_router:register_route(LServiceS, LServerS), ejabberd_hooks:add(user_send_packet, LServerS, ?MODULE, user_send_packet, 50), {ok, #state{lservice = LServiceS, lserver = LServerS, access = Access, service_limits = SLimits}}. handle_call(stop, _From, State) -> try_stop_loop(), {stop, normal, ok, State}. handle_cast({reload, NewOpts, NewOpts}, #state{lserver = LServerS, lservice = OldLServiceS} = State) -> Access = mod_multicast_opt:access(NewOpts), SLimits = build_service_limit_record(mod_multicast_opt:limits(NewOpts)), [NewLServiceS|_] = gen_mod:get_opt_hosts(NewOpts), if NewLServiceS /= OldLServiceS -> ejabberd_router:register_route(NewLServiceS, LServerS), ejabberd_router:unregister_route(OldLServiceS); true -> ok end, {noreply, State#state{lservice = NewLServiceS, access = Access, service_limits = SLimits}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({route, #iq{} = Packet}, State) -> case catch handle_iq(Packet, State) of {'EXIT', Reason} -> ?ERROR_MSG("Error when processing IQ stanza: ~p", [Reason]); _ -> ok end, {noreply, State}; %% XEP33 allows only 'message' and 'presence' stanza type handle_info({route, Packet}, #state{lservice = LServiceS, lserver = LServerS, access = Access, service_limits = SLimits} = State) when ?is_stanza(Packet) -> route_untrusted(LServiceS, LServerS, Access, SLimits, Packet), {noreply, State}; %% Handle multicast packets sent by trusted local services handle_info({route_trusted, Destinations, Packet}, #state{lservice = LServiceS, lserver = LServerS} = State) -> From = xmpp:get_from(Packet), case catch route_trusted(LServiceS, LServerS, From, Destinations, Packet) of {'EXIT', Reason} -> ?ERROR_MSG("Error in route_trusted: ~p", [Reason]); _ -> ok end, {noreply, State}; handle_info({get_host, Pid}, State) -> Pid ! {my_host, State#state.lservice}, {noreply, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, State) -> ejabberd_hooks:delete(user_send_packet, State#state.lserver, ?MODULE, user_send_packet, 50), ejabberd_router_multicast:unregister_route(State#state.lserver), ejabberd_router:unregister_route(State#state.lservice), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %%% Internal functions %%==================================================================== %%%------------------------ %%% IQ Request Processing %%%------------------------ handle_iq(Packet, State) -> try IQ = xmpp:decode_els(Packet), case process_iq(IQ, State) of {result, SubEl} -> ejabberd_router:route(xmpp:make_iq_result(Packet, SubEl)); {error, Error} -> ejabberd_router:route_error(Packet, Error); reply -> To = xmpp:get_to(IQ), LServiceS = jid:encode(To), case Packet#iq.type of result -> process_iqreply_result(LServiceS, IQ); error -> process_iqreply_error(LServiceS, IQ) end end catch _:{xmpp_codec, Why} -> Lang = xmpp:get_lang(Packet), Err = xmpp:err_bad_request(xmpp:io_format_error(Why), Lang), ejabberd_router:route_error(Packet, Err) end. -spec process_iq(iq(), state()) -> {result, xmpp_element()} | {error, stanza_error()} | reply. process_iq(#iq{type = get, lang = Lang, from = From, sub_els = [#disco_info{}]}, State) -> {result, iq_disco_info(From, Lang, State)}; process_iq(#iq{type = get, sub_els = [#disco_items{}]}, _) -> {result, #disco_items{}}; process_iq(#iq{type = get, lang = Lang, sub_els = [#vcard_temp{}]}, State) -> {result, iq_vcard(Lang, State)}; process_iq(#iq{type = T}, _) when T == set; T == get -> {error, xmpp:err_service_unavailable()}; process_iq(_, _) -> reply. -define(FEATURE(Feat), Feat). iq_disco_info(From, Lang, State) -> Name = mod_multicast_opt:name(State#state.lserver), #disco_info{ identities = [#identity{category = <<"service">>, type = <<"multicast">>, name = translate:translate(Lang, Name)}], features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_ADDRESS], xdata = iq_disco_info_extras(From, State)}. -spec iq_vcard(binary(), state()) -> #vcard_temp{}. iq_vcard(Lang, State) -> case mod_multicast_opt:vcard(State#state.lserver) of undefined -> #vcard_temp{fn = <<"ejabberd/mod_multicast">>, url = ejabberd_config:get_uri(), desc = misc:get_descr(Lang, ?T("ejabberd Multicast service"))}; VCard -> VCard end. %%%------------------------- %%% Route %%%------------------------- -spec route_trusted(binary(), binary(), jid(), [jid()], stanza()) -> 'ok'. route_trusted(LServiceS, LServerS, FromJID, Destinations, Packet) -> Addresses = [#address{type = bcc, jid = D} || D <- Destinations], Groups = group_by_destinations(Addresses, #{}), route_grouped(LServerS, LServiceS, FromJID, Groups, [], Packet). -spec route_untrusted(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'. route_untrusted(LServiceS, LServerS, Access, SLimits, Packet) -> try route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) catch adenied -> route_error(Packet, forbidden, ?T("Access denied by service policy")); eadsele -> route_error(Packet, bad_request, ?T("No addresses element found")); eadeles -> route_error(Packet, bad_request, ?T("No address elements found")); ewxmlns -> route_error(Packet, bad_request, ?T("Wrong xmlns")); etoorec -> route_error(Packet, not_acceptable, ?T("Too many receiver fields were specified")); edrelay -> route_error(Packet, forbidden, ?T("Packet relay is denied by service policy")); EType:EReason -> ?ERROR_MSG("Multicast unknown error: Type: ~p~nReason: ~p", [EType, EReason]), route_error(Packet, internal_server_error, ?T("Internal server error")) end. -spec route_untrusted2(binary(), binary(), atom(), #service_limits{}, stanza()) -> 'ok'. route_untrusted2(LServiceS, LServerS, Access, SLimits, Packet) -> FromJID = xmpp:get_from(Packet), ok = check_access(LServerS, Access, FromJID), {ok, PacketStripped, Addresses} = strip_addresses_element(Packet), {CC, BCC, NotJids, Rest} = partition_addresses(Addresses), report_not_jid(FromJID, Packet, NotJids), ok = check_limit_dests(SLimits, FromJID, Packet, length(CC) + length(BCC)), Groups0 = group_by_destinations(CC, #{}), Groups = group_by_destinations(BCC, Groups0), ok = check_relay(FromJID#jid.server, LServerS, Groups), route_grouped(LServerS, LServiceS, FromJID, Groups, Rest, PacketStripped). -spec mark_as_delivered([address()]) -> [address()]. mark_as_delivered(Addresses) -> [A#address{delivered = true} || A <- Addresses]. -spec route_individual(jid(), [address()], [address()], [address()], stanza()) -> ok. route_individual(From, CC, BCC, Other, Packet) -> CCDelivered = mark_as_delivered(CC), Addresses = CCDelivered ++ Other, PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), lists:foreach( fun(#address{jid = To}) -> ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)) end, CC), lists:foreach( fun(#address{jid = To} = Address) -> Packet2 = case Addresses of [] -> Packet; _ -> xmpp:append_subtags(Packet, [#addresses{list = [Address | Addresses]}]) end, ejabberd_router:route(xmpp:set_from_to(Packet2, From, To)) end, BCC). -spec route_chunk(jid(), jid(), stanza(), [address()]) -> ok. route_chunk(From, To, Packet, Addresses) -> PacketWithAddresses = xmpp:append_subtags(Packet, [#addresses{list = Addresses}]), ejabberd_router:route(xmpp:set_from_to(PacketWithAddresses, From, To)). -spec route_in_chunks(jid(), jid(), stanza(), integer(), [address()], [address()], [address()]) -> ok. route_in_chunks(_From, _To, _Packet, _Limit, [], [], _) -> ok; route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(CC) > Limit -> {Chunk, Rest} = lists:split(Limit, CC), route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), route_in_chunks(From, To, Packet, Limit, Rest, BCC, RestOfAddresses); route_in_chunks(From, To, Packet, Limit, [], BCC, RestOfAddresses) when length(BCC) > Limit -> {Chunk, Rest} = lists:split(Limit, BCC), route_chunk(From, To, Packet, Chunk ++ RestOfAddresses), route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses) when length(BCC) + length(CC) > Limit -> {Chunk, Rest} = lists:split(Limit - length(CC), BCC), route_chunk(From, To, Packet, CC ++ Chunk ++ RestOfAddresses), route_in_chunks(From, To, Packet, Limit, [], Rest, RestOfAddresses); route_in_chunks(From, To, Packet, _Limit, CC, BCC, RestOfAddresses) -> route_chunk(From, To, Packet, CC ++ BCC ++ RestOfAddresses). -spec route_multicast(jid(), jid(), [address()], [address()], [address()], stanza(), #limits{}) -> ok. route_multicast(From, To, CC, BCC, RestOfAddresses, Packet, Limits) -> {_Type, Limit} = get_limit_number(element(1, Packet), Limits), route_in_chunks(From, To, Packet, Limit, CC, BCC, RestOfAddresses). -spec route_grouped(binary(), binary(), jid(), #{}, [address()], stanza()) -> ok. route_grouped(LServer, LService, From, Groups, RestOfAddresses, Packet) -> maps:fold( fun(Server, {CC, BCC}, _) -> OtherCC = maps:fold( fun(Server2, _, Res) when Server2 == Server -> Res; (_, {CC2, _}, Res) -> mark_as_delivered(CC2) ++ Res end, [], Groups), case search_server_on_cache(Server, LServer, LService, {?MAXTIME_CACHE_POSITIVE, ?MAXTIME_CACHE_NEGATIVE}) of route_single -> route_individual(From, CC, BCC, OtherCC ++ RestOfAddresses, Packet); {route_multicast, Service, Limits} -> route_multicast(From, Service, CC, BCC, OtherCC ++ RestOfAddresses, Packet, Limits) end end, ok, Groups). %%%------------------------- %%% Check access permission %%%------------------------- check_access(LServerS, Access, From) -> case acl:match_rule(LServerS, Access, From) of allow -> ok; _ -> throw(adenied) end. %%%------------------------- %%% Strip 'addresses' XML element %%%------------------------- -spec strip_addresses_element(stanza()) -> {ok, stanza(), [address()]}. strip_addresses_element(Packet) -> case xmpp:get_subtag(Packet, #addresses{}) of #addresses{list = Addrs} -> PacketStripped = xmpp:remove_subtag(Packet, #addresses{}), {ok, PacketStripped, Addrs}; false -> throw(eadsele) end. %%%------------------------- %%% Split Addresses %%%------------------------- partition_addresses(Addresses) -> lists:foldl( fun(#address{delivered = true} = A, {C, B, I, D}) -> {C, B, I, [A | D]}; (#address{type = T, jid = undefined} = A, {C, B, I, D}) when T == to; T == cc; T == bcc -> {C, B, [A | I], D}; (#address{type = T} = A, {C, B, I, D}) when T == to; T == cc -> {[A | C], B, I, D}; (#address{type = bcc} = A, {C, B, I, D}) -> {C, [A | B], I, D}; (A, {C, B, I, D}) -> {C, B, I, [A | D]} end, {[], [], [], []}, Addresses). %%%------------------------- %%% Check does not exceed limit of destinations %%%------------------------- -spec check_limit_dests(#service_limits{}, jid(), stanza(), integer()) -> ok. check_limit_dests(SLimits, FromJID, Packet, NumOfAddresses) -> SenderT = sender_type(FromJID), Limits = get_slimit_group(SenderT, SLimits), StanzaType = type_of_stanza(Packet), {_Type, Limit} = get_limit_number(StanzaType, Limits), case NumOfAddresses > Limit of false -> ok; true -> throw(etoorec) end. -spec report_not_jid(jid(), stanza(), [address()]) -> any(). report_not_jid(From, Packet, Addresses) -> lists:foreach( fun(Address) -> route_error( xmpp:set_from_to(Packet, From, From), jid_malformed, str:format(?T("This service can not process the address: ~s"), [fxml:element_to_binary(xmpp:encode(Address))])) end, Addresses). %%%------------------------- %%% Group destinations by their servers %%%------------------------- group_by_destinations(Addrs, Map) -> lists:foldl( fun (#address{type = Type, jid = #jid{lserver = Server}} = Addr, Map2) when Type == to; Type == cc -> maps:update_with(Server, fun({CC, BCC}) -> {[Addr | CC], BCC} end, {[Addr], []}, Map2); (#address{type = bcc, jid = #jid{lserver = Server}} = Addr, Map2) -> maps:update_with(Server, fun({CC, BCC}) -> {CC, [Addr | BCC]} end, {[], [Addr]}, Map2) end, Map, Addrs). %%%------------------------- %%% Route packet %%%------------------------- %%%------------------------- %%% Check relay %%%------------------------- -spec check_relay(binary(), binary(), #{}) -> ok. check_relay(RS, LS, Gs) -> case lists:suffix(str:tokens(LS, <<".">>), str:tokens(RS, <<".">>)) orelse (maps:is_key(LS, Gs) andalso maps:size(Gs) == 1) of true -> ok; _ -> throw(edrelay) end. %%%------------------------- %%% Check protocol support: Send request %%%------------------------- -spec send_query_info(binary(), binary(), binary()) -> ok. send_query_info(RServerS, LServiceS, ID) -> case str:str(RServerS, <<"echo.">>) of 1 -> ok; _ -> send_query(RServerS, LServiceS, ID, #disco_info{}) end. -spec send_query_items(binary(), binary(), binary()) -> ok. send_query_items(RServerS, LServiceS, ID) -> send_query(RServerS, LServiceS, ID, #disco_items{}). -spec send_query(binary(), binary(), binary(), disco_info()|disco_items()) -> ok. send_query(RServerS, LServiceS, ID, SubEl) -> Packet = #iq{from = stj(LServiceS), to = stj(RServerS), id = ID, type = get, sub_els = [SubEl]}, ejabberd_router:route(Packet). %%%------------------------- %%% Check protocol support: Receive response: Error %%%------------------------- process_iqreply_error(LServiceS, Packet) -> FromS = jts(xmpp:get_from(Packet)), ID = Packet#iq.id, case str:tokens(ID, <<"/">>) of [RServer, _] -> case look_server(RServer) of {cached, {_Response, {wait_for_info, ID}}, _TS} when RServer == FromS -> add_response(RServer, not_supported, cached); {cached, {_Response, {wait_for_items, ID}}, _TS} when RServer == FromS -> add_response(RServer, not_supported, cached); {cached, {Response, {wait_for_items_info, ID, Items}}, _TS} -> case lists:member(FromS, Items) of true -> received_awaiter( FromS, RServer, Response, ID, Items, LServiceS); false -> ok end; _ -> ok end; _ -> ok end. %%%------------------------- %%% Check protocol support: Receive response: Disco %%%------------------------- -spec process_iqreply_result(binary(), iq()) -> any(). process_iqreply_result(LServiceS, #iq{from = From, id = ID, sub_els = [SubEl]}) -> case SubEl of #disco_info{} -> process_discoinfo_result(From, LServiceS, ID, SubEl); #disco_items{} -> process_discoitems_result(From, LServiceS, ID, SubEl); _ -> ok end. %%%------------------------- %%% Check protocol support: Receive response: Disco Info %%%------------------------- process_discoinfo_result(From, LServiceS, ID, DiscoInfo) -> FromS = jts(From), case str:tokens(ID, <<"/">>) of [RServer, _] -> case look_server(RServer) of {cached, {Response, {wait_for_info, ID} = ST}, _TS} when RServer == FromS -> process_discoinfo_result2( From, FromS, LServiceS, DiscoInfo, RServer, Response, ST); {cached, {Response, {wait_for_items_info, ID, Items} = ST}, _TS} -> case lists:member(FromS, Items) of true -> process_discoinfo_result2( From, FromS, LServiceS, DiscoInfo, RServer, Response, ST); false -> ok end; _ -> ok end; _ -> ok end. process_discoinfo_result2(From, FromS, LServiceS, #disco_info{features = Feats} = DiscoInfo, RServer, Response, ST) -> Multicast_support = lists:member(?NS_ADDRESS, Feats), case Multicast_support of true -> SenderT = sender_type(From), RLimits = get_limits_xml(DiscoInfo, SenderT), add_response(RServer, {multicast_supported, FromS, RLimits}, cached); false -> case ST of {wait_for_info, _ID} -> Random = p1_rand:get_string(), ID = <<RServer/binary, $/, Random/binary>>, send_query_items(FromS, LServiceS, ID), add_response(RServer, Response, {wait_for_items, ID}); %% We asked a component, and it does not support XEP33 {wait_for_items_info, ID, Items} -> received_awaiter(FromS, RServer, Response, ID, Items, LServiceS) end end. get_limits_xml(DiscoInfo, SenderT) -> LimitOpts = get_limits_els(DiscoInfo), build_remote_limit_record(LimitOpts, SenderT). -spec get_limits_els(disco_info()) -> [{atom(), integer()}]. get_limits_els(DiscoInfo) -> lists:flatmap( fun(#xdata{type = result} = X) -> get_limits_fields(X); (_) -> [] end, DiscoInfo#disco_info.xdata). -spec get_limits_fields(xdata()) -> [{atom(), integer()}]. get_limits_fields(X) -> {Head, Tail} = lists:partition( fun(#xdata_field{var = Var, type = Type}) -> Var == <<"FORM_TYPE">> andalso Type == hidden end, X#xdata.fields), case Head of [] -> []; _ -> get_limits_values(Tail) end. -spec get_limits_values([xdata_field()]) -> [{atom(), integer()}]. get_limits_values(Fields) -> lists:flatmap( fun(#xdata_field{var = Name, values = [Number]}) -> try [{binary_to_atom(Name, utf8), binary_to_integer(Number)}] catch _:badarg -> [] end; (_) -> [] end, Fields). %%%------------------------- %%% Check protocol support: Receive response: Disco Items %%%------------------------- process_discoitems_result(From, LServiceS, ID, #disco_items{items = Items}) -> FromS = jts(From), case str:tokens(ID, <<"/">>) of [FromS = RServer, _] -> case look_server(RServer) of {cached, {Response, {wait_for_items, ID}}, _TS} -> List = lists:flatmap( fun(#disco_item{jid = #jid{luser = <<"">>, lserver = LServer, lresource = <<"">>}}) -> [LServer]; (_) -> [] end, Items), case List of [] -> add_response(RServer, not_supported, cached); _ -> Random = p1_rand:get_string(), ID2 = <<RServer/binary, $/, Random/binary>>, [send_query_info(Item, LServiceS, ID2) || Item <- List], add_response(RServer, Response, {wait_for_items_info, ID2, List}) end; _ -> ok end; _ -> ok end. %%%------------------------- %%% Check protocol support: Receive response: Received awaiter %%%------------------------- received_awaiter(JID, RServer, Response, ID, JIDs, _LServiceS) -> case lists:delete(JID, JIDs) of [] -> add_response(RServer, not_supported, cached); JIDs2 -> add_response(RServer, Response, {wait_for_items_info, ID, JIDs2}) end. %%%------------------------- %%% Cache %%%------------------------- create_cache() -> ejabberd_mnesia:create(?MODULE, multicastc, [{ram_copies, [node()]}, {attributes, record_info(fields, multicastc)}]). add_response(RServer, Response, State) -> Secs = calendar:datetime_to_gregorian_seconds(calendar:local_time()), mnesia:dirty_write(#multicastc{rserver = RServer, response = {Response, State}, ts = Secs}). search_server_on_cache(RServer, LServerS, _LServiceS, _Maxmins) when RServer == LServerS -> route_single; search_server_on_cache(RServer, _LServerS, LServiceS, _Maxmins) when RServer == LServiceS -> route_single; search_server_on_cache(RServer, _LServerS, LServiceS, Maxmins) -> case look_server(RServer) of not_cached -> query_info(RServer, LServiceS, not_supported), route_single; {cached, {Response, State}, TS} -> Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()), Response2 = case State of cached -> case is_obsolete(Response, TS, Now, Maxmins) of false -> ok; true -> query_info(RServer, LServiceS, Response) end, Response; _ -> if Now - TS > ?MAXTIME_CACHE_NEGOTIATING -> query_info(RServer, LServiceS, not_supported), not_supported; true -> Response end end, case Response2 of not_supported -> route_single; {multicast_supported, Service, Limits} -> {route_multicast, Service, Limits} end end. query_info(RServer, LServiceS, Response) -> Random = p1_rand:get_string(), ID = <<RServer/binary, $/, Random/binary>>, send_query_info(RServer, LServiceS, ID), add_response(RServer, Response, {wait_for_info, ID}). look_server(RServer) -> case mnesia:dirty_read(multicastc, RServer) of [] -> not_cached; [M] -> {cached, M#multicastc.response, M#multicastc.ts} end. is_obsolete(Response, Ts, Now, {Max_pos, Max_neg}) -> Max = case Response of multicast_not_supported -> Max_neg; _ -> Max_pos end, Now - Ts > Max. %%%------------------------- %%% Purge cache %%%------------------------- purge() -> Maxmins_positive = (?MAXTIME_CACHE_POSITIVE), Maxmins_negative = (?MAXTIME_CACHE_NEGATIVE), Now = calendar:datetime_to_gregorian_seconds(calendar:local_time()), purge(Now, {Maxmins_positive, Maxmins_negative}). purge(Now, Maxmins) -> F = fun () -> mnesia:foldl(fun (R, _) -> #multicastc{response = Response, ts = Ts} = R, case is_obsolete(Response, Ts, Now, Maxmins) of true -> mnesia:delete_object(R); false -> ok end end, none, multicastc) end, mnesia:transaction(F). %%%------------------------- %%% Purge cache loop %%%------------------------- try_start_loop() -> case lists:member(?PURGE_PROCNAME, registered()) of true -> ok; false -> start_loop() end, (?PURGE_PROCNAME) ! new_module. start_loop() -> register(?PURGE_PROCNAME, spawn(?MODULE, purge_loop, [0])), (?PURGE_PROCNAME) ! purge_now. try_stop_loop() -> (?PURGE_PROCNAME) ! try_stop. purge_loop(NM) -> receive purge_now -> purge(), timer:send_after(?CACHE_PURGE_TIMER, ?PURGE_PROCNAME, purge_now), purge_loop(NM); new_module -> purge_loop(NM + 1); try_stop when NM > 1 -> purge_loop(NM - 1); try_stop -> purge_loop_finished end. %%%------------------------- %%% Limits: utils %%%------------------------- %% Type definitions for data structures related with XEP33 limits %% limit() = {Name, Value} %% Name = atom() %% Value = {Type, Number} %% Type = default | custom %% Number = integer() | infinite list_of_limits(local) -> [{message, ?DEFAULT_LIMIT_LOCAL_MESSAGE}, {presence, ?DEFAULT_LIMIT_LOCAL_PRESENCE}]; list_of_limits(remote) -> [{message, ?DEFAULT_LIMIT_REMOTE_MESSAGE}, {presence, ?DEFAULT_LIMIT_REMOTE_PRESENCE}]. build_service_limit_record(LimitOpts) -> LimitOptsL = get_from_limitopts(LimitOpts, local), LimitOptsR = get_from_limitopts(LimitOpts, remote), {service_limits, build_limit_record(LimitOptsL, local), build_limit_record(LimitOptsR, remote)}. get_from_limitopts(LimitOpts, SenderT) -> case lists:keyfind(SenderT, 1, LimitOpts) of false -> []; {SenderT, Result} -> Result end. build_remote_limit_record(LimitOpts, SenderT) -> build_limit_record(LimitOpts, SenderT). -spec build_limit_record(any(), local | remote) -> #limits{}. build_limit_record(LimitOpts, SenderT) -> Limits = [get_limit_value(Name, Default, LimitOpts) || {Name, Default} <- list_of_limits(SenderT)], list_to_tuple([limits | Limits]). -spec get_limit_value(atom(), integer(), any()) -> limit_value(). get_limit_value(Name, Default, LimitOpts) -> case lists:keysearch(Name, 1, LimitOpts) of {value, {Name, Number}} -> {custom, Number}; false -> {default, Default} end. type_of_stanza(Stanza) -> element(1, Stanza). -spec get_limit_number(message | presence, #limits{}) -> limit_value(). get_limit_number(message, Limits) -> Limits#limits.message; get_limit_number(presence, Limits) -> Limits#limits.presence. -spec get_slimit_group(local | remote, #service_limits{}) -> #limits{}. get_slimit_group(local, SLimits) -> SLimits#service_limits.local; get_slimit_group(remote, SLimits) -> SLimits#service_limits.remote. %%%------------------------- %%% Limits: XEP-0128 Service Discovery Extensions %%%------------------------- %% Some parts of code are borrowed from mod_muc_room.erl -define(RFIELDT(Type, Var, Val), #xdata_field{type = Type, var = Var, values = [Val]}). -define(RFIELDV(Var, Val), #xdata_field{var = Var, values = [Val]}). iq_disco_info_extras(From, State) -> SenderT = sender_type(From), Service_limits = State#state.service_limits, case iq_disco_info_extras2(SenderT, Service_limits) of [] -> []; List_limits_xmpp -> [#xdata{type = result, fields = [?RFIELDT(hidden, <<"FORM_TYPE">>, ?NS_ADDRESS) | List_limits_xmpp]}] end. sender_type(From) -> Local_hosts = ejabberd_option:hosts(), case lists:member(From#jid.lserver, Local_hosts) of true -> local; false -> remote end. iq_disco_info_extras2(SenderT, SLimits) -> Limits = get_slimit_group(SenderT, SLimits), Stanza_types = [message, presence], lists:foldl(fun (Type_of_stanza, R) -> case get_limit_number(Type_of_stanza, Limits) of {custom, Number} -> [?RFIELDV((to_binary(Type_of_stanza)), (to_binary(Number))) | R]; {default, _} -> R end end, [], Stanza_types). to_binary(A) -> list_to_binary(hd(io_lib:format("~p", [A]))). %%%------------------------- %%% Error report %%%------------------------- route_error(Packet, ErrType, ErrText) -> Lang = xmpp:get_lang(Packet), Err = make_reply(ErrType, Lang, ErrText), ejabberd_router:route_error(Packet, Err). make_reply(bad_request, Lang, ErrText) -> xmpp:err_bad_request(ErrText, Lang); make_reply(jid_malformed, Lang, ErrText) -> xmpp:err_jid_malformed(ErrText, Lang); make_reply(not_acceptable, Lang, ErrText) -> xmpp:err_not_acceptable(ErrText, Lang); make_reply(internal_server_error, Lang, ErrText) -> xmpp:err_internal_server_error(ErrText, Lang); make_reply(forbidden, Lang, ErrText) -> xmpp:err_forbidden(ErrText, Lang). stj(String) -> jid:decode(String). jts(String) -> jid:encode(String). depends(_Host, _Opts) -> []. mod_opt_type(access) -> econf:acl(); mod_opt_type(name) -> econf:binary(); mod_opt_type(limits) -> econf:options( #{local => econf:options( #{message => econf:non_neg_int(infinite), presence => econf:non_neg_int(infinite)}), remote => econf:options( #{message => econf:non_neg_int(infinite), presence => econf:non_neg_int(infinite)})}); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(vcard) -> econf:vcard_temp(). mod_options(Host) -> [{access, all}, {host, <<"multicast.", Host/binary>>}, {hosts, []}, {limits, [{local, []}, {remote, []}]}, {vcard, undefined}, {name, ?T("Multicast")}]. mod_doc() -> #{desc => [?T("This module implements a service for " "https://xmpp.org/extensions/xep-0033.html" "[XEP-0033: Extended Stanza Addressing].")], opts => [{access, #{value => "Access", desc => ?T("The access rule to restrict who can send packets to " "the multicast service. Default value: 'all'.")}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => [?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only " "Jabber ID will be the hostname of the virtual host " "with the prefix \"multicast.\". The keyword '@HOST@' " "is replaced with the real virtual host name."), ?T("The default value is 'multicast.@HOST@'.")]}}, {limits, #{value => "Sender: Stanza: Number", desc => [?T("Specify a list of custom limits which override the " "default ones defined in XEP-0033. Limits are defined " "per sender type and stanza type, where:"), "", ?T("- 'sender' can be: 'local' or 'remote'."), ?T("- 'stanza' can be: 'message' or 'presence'."), ?T("- 'number' can be a positive integer or 'infinite'.")], example => ["# Default values:", "local:", " message: 100", " presence: 100", "remote:", " message: 20", " presence: 20"] }}, {name, #{desc => ?T("Service name to provide in the Info query to the " "Service Discovery. Default is '\"Multicast\"'.")}}, {vcard, #{desc => ?T("vCard element to return when queried. " "Default value is 'undefined'.")}}], example => ["# Only admins can send packets to multicast service", "access_rules:", " multicast:", " - allow: admin", "", "# If you want to allow all your users:", "access_rules:", " multicast:", " - allow", "", "# This allows both admins and remote users to send packets,", "# but does not allow local users", "acl:", " allservers:", " server_glob: \"*\"", "access_rules:", " multicast:", " - allow: admin", " - deny: local", " - allow: allservers", "", "modules:", " mod_multicast:", " host: multicast.example.org", " access: multicast", " limits:", " local:", " message: 40", " presence: infinite", " remote:", " message: 150"]}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_acme.erl����������������������������������������������������������������0000644�0002322�0002322�00000052703�14154362354�017657� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_acme). -behaviour(gen_server). %% API -export([start_link/0]). -export([default_directory_url/0]). %% HTTP API -export([process/2]). %% Hooks -export([ejabberd_started/0, register_certfiles/0, cert_expired/2]). %% ejabberd commands -export([get_commands_spec/0, request_certificate/1, revoke_certificate/1, list_certificates/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). -include("logger.hrl"). -include("ejabberd_commands.hrl"). -include_lib("public_key/include/public_key.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -define(CALL_TIMEOUT, timer:minutes(10)). -record(state, {}). -type state() :: #state{}. -type priv_key() :: public_key:private_key(). -type cert() :: #'OTPCertificate'{}. -type cert_type() :: ec | rsa. -type io_error() :: file:posix(). -type issue_result() :: ok | p1_acme:issue_return() | {error, {file, io_error()} | {idna_failed, binary()}}. %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec register_certfiles() -> ok. register_certfiles() -> lists:foreach(fun ejabberd_pkix:add_certfile/1, list_certfiles()). -spec process([binary()], _) -> {integer(), [{binary(), binary()}], binary()}. process([Token], _) -> ?DEBUG("Received ACME challenge request for token: ~ts", [Token]), try ets:lookup_element(acme_challenge, Token, 2) of Key -> {200, [{<<"Content-Type">>, <<"application/octet-stream">>}], Key} catch _:_ -> {404, [], <<>>} end; process(_, _) -> {404, [], <<>>}. -spec cert_expired(_, pkix:cert_info()) -> ok | stop. cert_expired(_, #{domains := Domains, files := Files}) -> CertFiles = list_certfiles(), case lists:any( fun({File, _}) -> lists:member(File, CertFiles) end, Files) of true -> gen_server:cast(?MODULE, {request, Domains}), stop; false -> ok end. -spec ejabberd_started() -> ok. ejabberd_started() -> gen_server:cast(?MODULE, ejabberd_started). default_directory_url() -> <<"https://acme-v02.api.letsencrypt.org/directory">>. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> ets:new(acme_challenge, [named_table, public]), process_flag(trap_exit, true), ejabberd:start_app(p1_acme), delete_obsolete_data(), ejabberd_hooks:add(cert_expired, ?MODULE, cert_expired, 60), ejabberd_hooks:add(config_reloaded, ?MODULE, register_certfiles, 40), ejabberd_hooks:add(ejabberd_started, ?MODULE, ejabberd_started, 110), ejabberd_hooks:add(config_reloaded, ?MODULE, ejabberd_started, 110), ejabberd_commands:register_commands(get_commands_spec()), register_certfiles(), {ok, #state{}}. handle_call({request, [_|_] = Domains}, _From, State) -> ?INFO_MSG("Requesting new certificate for ~ts from ~ts", [misc:format_hosts_list(Domains), directory_url()]), {Ret, State1} = issue_request(State, Domains), {reply, Ret, State1}; handle_call({revoke, Cert, Key, Path}, _From, State) -> ?INFO_MSG("Revoking certificate from file ~ts", [Path]), {Ret, State1} = revoke_request(State, Cert, Key, Path), {reply, Ret, State1}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(ejabberd_started, State) -> case request_on_start() of {true, Domains} -> ?INFO_MSG("Requesting new certificate for ~ts from ~ts", [misc:format_hosts_list(Domains), directory_url()]), {_, State1} = issue_request(State, Domains), {noreply, State1}; false -> {noreply, State} end; handle_cast({request, [_|_] = Domains}, State) -> ?INFO_MSG("Requesting renewal of certificate for ~ts from ~ts", [misc:format_hosts_list(Domains), directory_url()]), {_, State1} = issue_request(State, Domains), {noreply, State1}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 60), ejabberd_hooks:delete(config_reloaded, ?MODULE, register_certfiles, 40), ejabberd_hooks:delete(ejabberd_started, ?MODULE, ejabberd_started, 110), ejabberd_hooks:delete(config_reloaded, ?MODULE, ejabberd_started, 110), ejabberd_commands:unregister_commands(get_commands_spec()). code_change(_OldVsn, State, _Extra) -> {ok, State}. format_status(_Opt, Status) -> Status. %%%=================================================================== %%% Internal functions %%%=================================================================== %%%=================================================================== %%% Challenge callback %%%=================================================================== -spec register_challenge(p1_acme:challenge_data(), reference()) -> true. register_challenge(Auth, Ref) -> ?DEBUG("Registering ACME challenge ~p -> ~p", [Ref, Auth]), ejabberd_hooks:run(acme_challenge, [{start, Auth, Ref}]), ets:insert( acme_challenge, lists:map( fun(#{token := Token, key := Key}) -> {Token, Key, Ref} end, Auth)). -spec unregister_challenge(reference()) -> non_neg_integer(). unregister_challenge(Ref) -> ?DEBUG("Unregistering ACME challenge ~p", [Ref]), ejabberd_hooks:run(acme_challenge, [{stop, Ref}]), ets:select_delete( acme_challenge, ets:fun2ms( fun({_, _, Ref1}) -> Ref1 == Ref end)). %%%=================================================================== %%% Issuance %%%=================================================================== -spec issue_request(state(), [binary(),...]) -> {issue_result(), state()}. issue_request(State, Domains) -> case check_idna(Domains) of {ok, AsciiDomains} -> case read_account_key() of {ok, AccKey} -> Config = ejabberd_option:acme(), DirURL = maps:get(ca_url, Config, default_directory_url()), Contact = maps:get(contact, Config, []), CertType = maps:get(cert_type, Config, rsa), issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact); {error, Reason} = Err -> ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", [misc:format_hosts_list(Domains), format_error(Reason)]), {Err, State} end; {error, Reason} = Err -> ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", [misc:format_hosts_list(Domains), format_error(Reason)]), {Err, State} end. -spec issue_request(state(), binary(), [binary(),...], [string(), ...], priv_key(), cert_type(), [binary()]) -> {issue_result(), state()}. issue_request(State, DirURL, Domains, AsciiDomains, AccKey, CertType, Contact) -> Ref = make_ref(), ChallengeFun = fun(Auth) -> register_challenge(Auth, Ref) end, Ret = case p1_acme:issue(DirURL, AsciiDomains, AccKey, [{cert_type, CertType}, {contact, Contact}, {debug_fun, debug_fun()}, {challenge_fun, ChallengeFun}]) of {ok, #{cert_key := CertKey, cert_chain := Certs}} -> case store_cert(CertKey, Certs, CertType, Domains) of {ok, Path} -> ejabberd_pkix:add_certfile(Path), ejabberd_pkix:commit(), ?INFO_MSG("Certificate for ~ts has been received, " "stored and loaded successfully", [misc:format_hosts_list(Domains)]), {ok, State}; {error, Reason} = Err -> ?ERROR_MSG("Failed to store certificate for ~ts: ~ts", [misc:format_hosts_list(Domains), format_error(Reason)]), {Err, State} end; {error, Reason} = Err -> ?ERROR_MSG("Failed to request certificate for ~ts: ~ts", [misc:format_hosts_list(Domains), format_error(Reason)]), {Err, State} end, unregister_challenge(Ref), Ret. %%%=================================================================== %%% Revocation %%%=================================================================== revoke_request(State, Cert, Key, Path) -> case p1_acme:revoke(directory_url(), Cert, Key, [{debug_fun, debug_fun()}]) of ok -> ?INFO_MSG("Certificate from file ~ts has been " "revoked successfully", [Path]), case delete_file(Path) of ok -> ejabberd_pkix:del_certfile(Path), ejabberd_pkix:commit(), {ok, State}; Err -> {Err, State} end; {error, Reason} = Err -> ?ERROR_MSG("Failed to revoke certificate from file ~ts: ~ts", [Path, format_error(Reason)]), {Err, State} end. %%%=================================================================== %%% File management %%%=================================================================== -spec acme_dir() -> file:filename_all(). acme_dir() -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "acme"). -spec acme_certs_dir(atom()) -> file:filename_all(). acme_certs_dir(Tag) -> filename:join(acme_dir(), Tag). -spec account_file() -> file:filename_all(). account_file() -> filename:join(acme_dir(), "account.key"). -spec cert_file(cert_type(), [binary()]) -> file:filename_all(). cert_file(CertType, Domains) -> L = [erlang:atom_to_binary(CertType, latin1)|Domains], Hash = str:sha(str:join(L, <<0>>)), filename:join(acme_certs_dir(live), Hash). -spec prep_path(file:filename_all()) -> binary(). prep_path(Path) -> unicode:characters_to_binary(Path). -spec list_certfiles() -> [binary()]. list_certfiles() -> filelib:fold_files( acme_certs_dir(live), "^[0-9a-f]{40}$", false, fun(F, Fs) -> [prep_path(F)|Fs] end, []). -spec read_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. read_account_key() -> Path = account_file(), case pkix:read_file(Path) of {ok, _, KeyMap} -> case maps:keys(KeyMap) of [#'ECPrivateKey'{} = Key|_] -> {ok, Key}; _ -> ?WARNING_MSG("File ~ts doesn't contain ACME account key. " "Trying to create a new one...", [Path]), create_account_key() end; {error, enoent} -> create_account_key(); {error, {bad_cert, _, _} = Reason} -> ?WARNING_MSG("ACME account key from '~ts' is corrupted: ~ts. " "Trying to create a new one...", [Path, pkix:format_error(Reason)]), create_account_key(); {error, Reason} -> ?ERROR_MSG("Failed to read ACME account from ~ts: ~ts. " "Try to fix permissions or delete the file completely", [Path, pkix:format_error(Reason)]), {error, {file, Reason}} end. -spec create_account_key() -> {ok, #'ECPrivateKey'{}} | {error, {file, io_error()}}. create_account_key() -> Path = account_file(), ?DEBUG("Creating ACME account key in ~ts", [Path]), Key = p1_acme:generate_key(ec), DER = public_key:der_encode(element(1, Key), Key), PEM = public_key:pem_encode([{element(1, Key), DER, not_encrypted}]), case write_file(Path, PEM) of ok -> ?DEBUG("ACME account key has been created successfully in ~ts", [Path]), {ok, Key}; {error, Reason} -> {error, {file, Reason}} end. -spec store_cert(priv_key(), [cert()], cert_type(), [binary()]) -> {ok, file:filename_all()} | {error, {file, io_error()}}. store_cert(Key, Chain, CertType, Domains) -> DerKey = public_key:der_encode(element(1, Key), Key), PemKey = [{element(1, Key), DerKey, not_encrypted}], PemChain = lists:map( fun(Cert) -> DerCert = public_key:pkix_encode( element(1, Cert), Cert, otp), {'Certificate', DerCert, not_encrypted} end, Chain), PEM = public_key:pem_encode(PemChain ++ PemKey), Path = cert_file(CertType, Domains), ?DEBUG("Storing certificate for ~ts in ~ts", [misc:format_hosts_list(Domains), Path]), case write_file(Path, PEM) of ok -> {ok, Path}; {error, Reason} -> {error, {file, Reason}} end. -spec read_cert(file:filename_all()) -> {ok, [cert()], priv_key()} | {error, {file, io_error()} | {bad_cert, _, _} | unexpected_certfile}. read_cert(Path) -> ?DEBUG("Reading certificate from ~ts", [Path]), case pkix:read_file(Path) of {ok, CertsMap, KeysMap} -> case {maps:to_list(CertsMap), maps:keys(KeysMap)} of {[_|_] = Certs, [CertKey]} -> {ok, [Cert || {Cert, _} <- lists:keysort(2, Certs)], CertKey}; _ -> {error, unexpected_certfile} end; {error, Why} when is_atom(Why) -> {error, {file, Why}}; {error, _} = Err -> Err end. -spec write_file(file:filename_all(), iodata()) -> ok | {error, io_error()}. write_file(Path, Data) -> case ensure_dir(Path) of ok -> case file:write_file(Path, Data) of ok -> case file:change_mode(Path, 8#600) of ok -> ok; {error, Why} -> ?WARNING_MSG("Failed to change permissions of ~ts: ~ts", [Path, file:format_error(Why)]) end; {error, Why} = Err -> ?ERROR_MSG("Failed to write file ~ts: ~ts", [Path, file:format_error(Why)]), Err end; Err -> Err end. -spec delete_file(file:filename_all()) -> ok | {error, io_error()}. delete_file(Path) -> case file:delete(Path) of ok -> ok; {error, Why} = Err -> ?WARNING_MSG("Failed to delete file ~ts: ~ts", [Path, file:format_error(Why)]), Err end. -spec ensure_dir(file:filename_all()) -> ok | {error, io_error()}. ensure_dir(Path) -> case filelib:ensure_dir(Path) of ok -> ok; {error, Why} = Err -> ?ERROR_MSG("Failed to create directory ~ts: ~ts", [filename:dirname(Path), file:format_error(Why)]), Err end. -spec delete_obsolete_data() -> ok. delete_obsolete_data() -> Path = filename:join(ejabberd_pkix:certs_dir(), "acme"), case filelib:is_dir(Path) of true -> ?INFO_MSG("Deleting obsolete directory ~ts", [Path]), _ = misc:delete_dir(Path), ok; false -> ok end. %%%=================================================================== %%% ejabberd commands %%%=================================================================== get_commands_spec() -> [#ejabberd_commands{name = request_certificate, tags = [acme], desc = "Requests certificates for all or the specified " "domains: all | domain1,domain2,...", module = ?MODULE, function = request_certificate, args_desc = ["Domains for which to acquire a certificate"], args_example = ["all | domain.tld,conference.domain.tld,..."], args = [{domains, string}], result = {res, restuple}}, #ejabberd_commands{name = list_certificates, tags = [acme], desc = "Lists all ACME certificates", module = ?MODULE, function = list_certificates, args = [], result = {certificates, {list, {certificate, {tuple, [{domain, string}, {file, string}, {used, string}]}}}}}, #ejabberd_commands{name = revoke_certificate, tags = [acme], desc = "Revokes the selected ACME certificate", module = ?MODULE, function = revoke_certificate, args_desc = ["Filename of the certificate"], args = [{file, string}], result = {res, restuple}}]. -spec request_certificate(iodata()) -> {ok | error, string()}. request_certificate(Arg) -> Ret = case lists:filter( fun(S) -> S /= <<>> end, re:split(Arg, "[\\h,;]+", [{return, binary}])) of [<<"all">>] -> case auto_domains() of [] -> {error, no_auto_hosts}; Domains -> gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) end; [_|_] = Domains -> case lists:dropwhile( fun(D) -> try ejabberd_router:is_my_route(D) of true -> not is_ip_or_localhost(D); false -> false catch _:{invalid_domain, _} -> false end end, Domains) of [Bad|_] -> {error, {invalid_host, Bad}}; [] -> gen_server:call(?MODULE, {request, Domains}, ?CALL_TIMEOUT) end; [] -> {error, invalid_argument} end, case Ret of ok -> {ok, ""}; {error, Why} -> {error, format_error(Why)} end. -spec revoke_certificate(iodata()) -> {ok | error, string()}. revoke_certificate(Path0) -> Path = prep_path(Path0), Ret = case read_cert(Path) of {ok, [Cert|_], Key} -> gen_server:call(?MODULE, {revoke, Cert, Key, Path}, ?CALL_TIMEOUT); {error, _} = Err -> Err end, case Ret of ok -> {ok, ""}; {error, Reason} -> {error, format_error(Reason)} end. -spec list_certificates() -> [{binary(), binary(), boolean()}]. list_certificates() -> Known = lists:flatmap( fun(Path) -> try {ok, [Cert|_], _} = read_cert(Path), Domains = pkix:extract_domains(Cert), [{Domain, Path} || Domain <- Domains] catch _:{badmatch, _} -> [] end end, list_certfiles()), Used = lists:foldl( fun(Domain, S) -> try {ok, Path} = ejabberd_pkix:get_certfile_no_default(Domain), {ok, [Cert|_], _} = read_cert(Path), {ok, #{files := Files}} = pkix:get_cert_info(Cert), lists:foldl(fun sets:add_element/2, S, [{Domain, File} || {File, _} <- Files]) catch _:{badmatch, _} -> S end end, sets:new(), all_domains()), lists:sort( lists:map( fun({Domain, Path} = E) -> {Domain, Path, sets:is_element(E, Used)} end, Known)). %%%=================================================================== %%% Other stuff %%%=================================================================== -spec all_domains() -> [binary(),...]. all_domains() -> ejabberd_option:hosts() ++ ejabberd_router:get_all_routes(). -spec auto_domains() -> [binary()]. auto_domains() -> lists:filter( fun(Host) -> not is_ip_or_localhost(Host) end, all_domains()). -spec directory_url() -> binary(). directory_url() -> maps:get(ca_url, ejabberd_option:acme(), default_directory_url()). -spec debug_fun() -> fun((string(), list()) -> ok). debug_fun() -> fun(Fmt, Args) -> ?DEBUG(Fmt, Args) end. -spec request_on_start() -> false | {true, [binary()]}. request_on_start() -> Config = ejabberd_option:acme(), case maps:get(auto, Config, true) of false -> false; true -> case ejabberd_listener:tls_listeners() of [] -> false; _ -> case lists:filter( fun(Host) -> not (have_cert_for_domain(Host) orelse is_ip_or_localhost(Host)) end, auto_domains()) of [] -> false; Hosts -> case have_acme_listener() of true -> {true, Hosts}; false -> ?WARNING_MSG( "No HTTP listeners for ACME challenges " "are configured, automatic " "certificate requests are aborted. Hint: " "configure the listener and restart/reload " "ejabberd. Or set acme->auto option to " "`false` to suppress this warning.", []), false end end end end. well_known() -> [<<".well-known">>, <<"acme-challenge">>]. -spec have_cert_for_domain(binary()) -> boolean(). have_cert_for_domain(Host) -> ejabberd_pkix:get_certfile_no_default(Host) /= error. -spec is_ip_or_localhost(binary()) -> boolean(). is_ip_or_localhost(Host) -> Parts = binary:split(Host, <<".">>), TLD = binary_to_list(lists:last(Parts)), case inet:parse_address(TLD) of {ok, _} -> true; _ -> TLD == "localhost" end. -spec have_acme_listener() -> boolean(). have_acme_listener() -> lists:any( fun({_, ejabberd_http, #{tls := false, request_handlers := Handlers}}) -> lists:keymember(well_known(), 1, Handlers); (_) -> false end, ejabberd_option:listen()). -spec check_idna([binary()]) -> {ok, [string()]} | {error, {idna_failed, binary()}}. check_idna(Domains) -> lists:foldl( fun(D, {ok, Ds}) -> try {ok, [idna:utf8_to_ascii(D)|Ds]} catch _:_ -> {error, {idna_failed, D}} end; (_, Err) -> Err end, {ok, []}, Domains). -spec format_error(term()) -> string(). format_error({file, Reason}) -> "I/O error: " ++ file:format_error(Reason); format_error({invalid_host, Domain}) -> "Unknown or unacceptable virtual host: " ++ binary_to_list(Domain); format_error(no_auto_hosts) -> "You have no virtual hosts acceptable for ACME certification"; format_error(invalid_argument) -> "Invalid argument"; format_error(unexpected_certfile) -> "The certificate file was not obtained using ACME"; format_error({idna_failed, Domain}) -> "Not an IDN hostname: " ++ binary_to_list(Domain); format_error({bad_cert, _, _} = Reason) -> "Malformed certificate file: " ++ pkix:format_error(Reason); format_error(Reason) -> p1_acme:format_error(Reason). �������������������������������������������������������������ejabberd-21.12/src/econf.erl������������������������������������������������������������������������0000644�0002322�0002322�00000047252�14154362354�016231� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : econf.erl %%% Purpose : Validator for ejabberd configuration options %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(econf). %% API -export([parse/3, validate/2, fail/1, format_error/2, replace_macros/1]). -export([group_dups/1]). %% Simple types -export([pos_int/0, pos_int/1, non_neg_int/0, non_neg_int/1]). -export([int/0, int/2, number/1, octal/0]). -export([binary/0, binary/1, binary/2]). -export([string/0, string/1, string/2]). -export([enum/1, bool/0, atom/0, any/0]). %% Complex types -export([url/0, url/1]). -export([file/0, file/1]). -export([directory/0, directory/1]). -export([ip/0, ipv4/0, ipv6/0, ip_mask/0, port/0]). -export([re/0, re/1, glob/0, glob/1]). -export([path/0, binary_sep/1]). -export([beam/0, beam/1, base64/0]). -export([timeout/1, timeout/2]). %% Composite types -export([list/1, list/2]). -export([list_or_single/1, list_or_single/2]). -export([map/2, map/3]). -export([either/2, and_then/2, non_empty/1]). -export([options/1, options/2]). %% Custom types -export([acl/0, shaper/0, url_or_file/0, lang/0]). -export([pem/0, queue_type/0]). -export([jid/0, user/0, domain/0, resource/0]). -export([db_type/1, ldap_filter/0]). -export([host/0, hosts/0]). -export([vcard_temp/0]). -ifdef(SIP). -export([sip_uri/0]). -endif. -type error_reason() :: term(). -type error_return() :: {error, error_reason(), yconf:ctx()}. -type validator() :: yconf:validator(). -type validator(T) :: yconf:validator(T). -type validators() :: yconf:validators(). -export_type([validator/0, validator/1, validators/0]). -export_type([error_reason/0, error_return/0]). %%%=================================================================== %%% API %%%=================================================================== parse(File, Validators, Options) -> try yconf:parse(File, Validators, Options) catch _:{?MODULE, Reason, Ctx} -> {error, Reason, Ctx} end. validate(Validator, Y) -> try yconf:validate(Validator, Y) catch _:{?MODULE, Reason, Ctx} -> {error, Reason, Ctx} end. replace_macros(Y) -> yconf:replace_macros(Y). -spec fail(error_reason()) -> no_return(). fail(Reason) -> yconf:fail(?MODULE, Reason). format_error({bad_module, Mod}, Ctx) when Ctx == [listen, module]; Ctx == [listen, request_handlers] -> Mods = ejabberd_config:beams(all), format("~ts: unknown ~ts: ~ts. Did you mean ~ts?", [yconf:format_ctx(Ctx), format_module_type(Ctx), format_module(Mod), format_module(misc:best_match(Mod, Mods))]); format_error({bad_module, Mod}, Ctx) when Ctx == [modules] -> Mods = lists:filter( fun(M) -> case atom_to_list(M) of "mod_" ++ _ -> true; "Elixir.Mod" ++ _ -> true; _ -> false end end, ejabberd_config:beams(all)), format("~ts: unknown ~ts: ~ts. Did you mean ~ts?", [yconf:format_ctx(Ctx), format_module_type(Ctx), format_module(Mod), format_module(misc:best_match(Mod, Mods))]); format_error({bad_export, {F, A}, Mod}, Ctx) when Ctx == [listen, module]; Ctx == [listen, request_handlers]; Ctx == [modules] -> Type = format_module_type(Ctx), Slogan = yconf:format_ctx(Ctx), case lists:member(Mod, ejabberd_config:beams(local)) of true -> format("~ts: '~ts' is not a ~ts", [Slogan, format_module(Mod), Type]); false -> case lists:member(Mod, ejabberd_config:beams(external)) of true -> format("~ts: third-party ~ts '~ts' doesn't export " "function ~ts/~B. If it's really a ~ts, " "consider to upgrade it", [Slogan, Type, format_module(Mod),F, A, Type]); false -> format("~ts: '~ts' doesn't match any known ~ts", [Slogan, format_module(Mod), Type]) end end; format_error({unknown_option, [], _} = Why, Ctx) -> format("~ts. There are no available options", [yconf:format_error(Why, Ctx)]); format_error({unknown_option, Known, Opt} = Why, Ctx) -> format("~ts. Did you mean ~ts? ~ts", [yconf:format_error(Why, Ctx), misc:best_match(Opt, Known), format_known("Available options", Known)]); format_error({bad_enum, Known, Bad} = Why, Ctx) -> format("~ts. Did you mean ~ts? ~ts", [yconf:format_error(Why, Ctx), misc:best_match(Bad, Known), format_known("Possible values", Known)]); format_error({bad_yaml, _, _} = Why, _) -> format_error(Why); format_error(Reason, Ctx) -> yconf:format_ctx(Ctx) ++ ": " ++ format_error(Reason). format_error({bad_db_type, _, Atom}) -> format("unsupported database: ~ts", [Atom]); format_error({bad_lang, Lang}) -> format("Invalid language tag: ~ts", [Lang]); format_error({bad_pem, Why, Path}) -> format("Failed to read PEM file '~ts': ~ts", [Path, pkix:format_error(Why)]); format_error({bad_cert, Why, Path}) -> format_error({bad_pem, Why, Path}); format_error({bad_jwt_key, Path}) -> format("No valid JWT key found in file: ~ts", [Path]); format_error({bad_jwt_key_set, Path}) -> format("JWK set contains multiple JWT keys in file: ~ts", [Path]); format_error({bad_jid, Bad}) -> format("Invalid XMPP address: ~ts", [Bad]); format_error({bad_user, Bad}) -> format("Invalid user part: ~ts", [Bad]); format_error({bad_domain, Bad}) -> format("Invalid domain: ~ts", [Bad]); format_error({bad_resource, Bad}) -> format("Invalid resource part: ~ts", [Bad]); format_error({bad_ldap_filter, Bad}) -> format("Invalid LDAP filter: ~ts", [Bad]); format_error({bad_sip_uri, Bad}) -> format("Invalid SIP URI: ~ts", [Bad]); format_error({route_conflict, R}) -> format("Failed to reuse route '~ts' because it's " "already registered on a virtual host", [R]); format_error({listener_dup, AddrPort}) -> format("Overlapping listeners found at ~ts", [format_addr_port(AddrPort)]); format_error({listener_conflict, AddrPort1, AddrPort2}) -> format("Overlapping listeners found at ~ts and ~ts", [format_addr_port(AddrPort1), format_addr_port(AddrPort2)]); format_error({invalid_syntax, Reason}) -> format("~ts", [Reason]); format_error({missing_module_dep, Mod, DepMod}) -> format("module ~ts depends on module ~ts, " "which is not found in the config", [Mod, DepMod]); format_error(eimp_error) -> format("ejabberd is built without image converter support", []); format_error({mqtt_codec, Reason}) -> mqtt_codec:format_error(Reason); format_error(Reason) -> yconf:format_error(Reason). -spec format_module(atom() | string()) -> string(). format_module(Mod) when is_atom(Mod) -> format_module(atom_to_list(Mod)); format_module(Mod) -> case Mod of "Elixir." ++ M -> M; M -> M end. format_module_type([listen, module]) -> "listening module"; format_module_type([listen, request_handlers]) -> "HTTP request handler"; format_module_type([modules]) -> "ejabberd module". format_known(_, Known) when length(Known) > 20 -> ""; format_known(Prefix, Known) -> [Prefix, " are: ", format_join(Known)]. format_join([]) -> "(empty)"; format_join([H|_] = L) when is_atom(H) -> format_join([atom_to_binary(A, utf8) || A <- L]); format_join(L) -> str:join(lists:sort(L), <<", ">>). %% All duplicated options having list-values are grouped %% into a single option with all list-values being concatenated -spec group_dups(list(T)) -> list(T). group_dups(Y1) -> lists:reverse( lists:foldl( fun({Option, Values}, Acc) when is_list(Values) -> case lists:keyfind(Option, 1, Acc) of {Option, Vals} when is_list(Vals) -> lists:keyreplace(Option, 1, Acc, {Option, Vals ++ Values}); _ -> [{Option, Values}|Acc] end; (Other, Acc) -> [Other|Acc] end, [], Y1)). %%%=================================================================== %%% Validators from yconf %%%=================================================================== pos_int() -> yconf:pos_int(). pos_int(Inf) -> yconf:pos_int(Inf). non_neg_int() -> yconf:non_neg_int(). non_neg_int(Inf) -> yconf:non_neg_int(Inf). int() -> yconf:int(). int(Min, Max) -> yconf:int(Min, Max). number(Min) -> yconf:number(Min). octal() -> yconf:octal(). binary() -> yconf:binary(). binary(Re) -> yconf:binary(Re). binary(Re, Opts) -> yconf:binary(Re, Opts). enum(L) -> yconf:enum(L). bool() -> yconf:bool(). atom() -> yconf:atom(). string() -> yconf:string(). string(Re) -> yconf:string(Re). string(Re, Opts) -> yconf:string(Re, Opts). any() -> yconf:any(). url() -> yconf:url(). url(Schemes) -> yconf:url(Schemes). file() -> yconf:file(). file(Type) -> yconf:file(Type). directory() -> yconf:directory(). directory(Type) -> yconf:directory(Type). ip() -> yconf:ip(). ipv4() -> yconf:ipv4(). ipv6() -> yconf:ipv6(). ip_mask() -> yconf:ip_mask(). port() -> yconf:port(). re() -> yconf:re(). re(Opts) -> yconf:re(Opts). glob() -> yconf:glob(). glob(Opts) -> yconf:glob(Opts). path() -> yconf:path(). binary_sep(Sep) -> yconf:binary_sep(Sep). timeout(Units) -> yconf:timeout(Units). timeout(Units, Inf) -> yconf:timeout(Units, Inf). base64() -> yconf:base64(). non_empty(F) -> yconf:non_empty(F). list(F) -> yconf:list(F). list(F, Opts) -> yconf:list(F, Opts). list_or_single(F) -> yconf:list_or_single(F). list_or_single(F, Opts) -> yconf:list_or_single(F, Opts). map(F1, F2) -> yconf:map(F1, F2). map(F1, F2, Opts) -> yconf:map(F1, F2, Opts). either(F1, F2) -> yconf:either(F1, F2). and_then(F1, F2) -> yconf:and_then(F1, F2). options(V) -> yconf:options(V). options(V, O) -> yconf:options(V, O). %%%=================================================================== %%% Custom validators %%%=================================================================== beam() -> beam([]). beam(Exports) -> and_then( non_empty(binary()), fun(<<"Elixir.", _/binary>> = Val) -> (yconf:beam(Exports))(Val); (<<C, _/binary>> = Val) when C >= $A, C =< $Z -> (yconf:beam(Exports))(<<"Elixir.", Val/binary>>); (Val) -> (yconf:beam(Exports))(Val) end). acl() -> either( atom(), acl:access_rules_validator()). shaper() -> either( atom(), ejabberd_shaper:shaper_rules_validator()). -spec url_or_file() -> yconf:validator({file | url, binary()}). url_or_file() -> either( and_then(url(), fun(URL) -> {url, URL} end), and_then(file(), fun(File) -> {file, File} end)). -spec lang() -> yconf:validator(binary()). lang() -> and_then( binary(), fun(Lang) -> try xmpp_lang:check(Lang) catch _:_ -> fail({bad_lang, Lang}) end end). -spec pem() -> yconf:validator(binary()). pem() -> and_then( path(), fun(Path) -> case pkix:is_pem_file(Path) of true -> Path; {false, Reason} -> fail({bad_pem, Reason, Path}) end end). -spec jid() -> yconf:validator(jid:jid()). jid() -> and_then( binary(), fun(Val) -> try jid:decode(Val) catch _:{bad_jid, _} = Reason -> fail(Reason) end end). -spec user() -> yconf:validator(binary()). user() -> and_then( binary(), fun(Val) -> case jid:nodeprep(Val) of error -> fail({bad_user, Val}); U -> U end end). -spec domain() -> yconf:validator(binary()). domain() -> and_then( non_empty(binary()), fun(Val) -> try jid:tolower(jid:decode(Val)) of {<<"">>, Domain, <<"">>} -> Domain; _ -> fail({bad_domain, Val}) catch _:{bad_jid, _} -> fail({bad_domain, Val}) end end). -spec resource() -> yconf:validator(binary()). resource() -> and_then( binary(), fun(Val) -> case jid:resourceprep(Val) of error -> fail({bad_resource, Val}); R -> R end end). -spec db_type(module()) -> yconf:validator(atom()). db_type(M) -> and_then( atom(), fun(T) -> case code:ensure_loaded(db_module(M, T)) of {module, _} -> T; {error, _} -> fail({bad_db_type, M, T}) end end). -spec queue_type() -> yconf:validator(ram | file). queue_type() -> enum([ram, file]). -spec ldap_filter() -> yconf:validator(binary()). ldap_filter() -> and_then( binary(), fun(Val) -> case eldap_filter:parse(Val) of {ok, _} -> Val; _ -> fail({bad_ldap_filter, Val}) end end). -ifdef(SIP). sip_uri() -> and_then( binary(), fun(Val) -> case esip:decode_uri(Val) of error -> fail({bad_sip_uri, Val}); URI -> URI end end). -endif. -spec host() -> yconf:validator(binary()). host() -> fun(Domain) -> Host = ejabberd_config:get_myname(), Hosts = ejabberd_config:get_option(hosts), Domain1 = (binary())(Domain), Domain2 = misc:expand_keyword(<<"@HOST@">>, Domain1, Host), Domain3 = (domain())(Domain2), case lists:member(Domain3, Hosts) of true -> fail({route_conflict, Domain3}); false -> Domain3 end end. -spec hosts() -> yconf:validator([binary()]). hosts() -> list(host(), [unique]). -spec vcard_temp() -> yconf:validator(). vcard_temp() -> and_then( vcard_validator( vcard_temp, undefined, [{version, undefined, binary()}, {fn, undefined, binary()}, {n, undefined, vcard_name()}, {nickname, undefined, binary()}, {photo, undefined, vcard_photo()}, {bday, undefined, binary()}, {adr, [], list(vcard_adr())}, {label, [], list(vcard_label())}, {tel, [], list(vcard_tel())}, {email, [], list(vcard_email())}, {jabberid, undefined, binary()}, {mailer, undefined, binary()}, {tz, undefined, binary()}, {geo, undefined, vcard_geo()}, {title, undefined, binary()}, {role, undefined, binary()}, {logo, undefined, vcard_logo()}, {org, undefined, vcard_org()}, {categories, [], list(binary())}, {note, undefined, binary()}, {prodid, undefined, binary()}, {rev, undefined, binary()}, {sort_string, undefined, binary()}, {sound, undefined, vcard_sound()}, {uid, undefined, binary()}, {url, undefined, binary()}, {class, undefined, enum([confidential, private, public])}, {key, undefined, vcard_key()}, {desc, undefined, binary()}]), fun(Tuple) -> list_to_tuple(tuple_to_list(Tuple) ++ [[]]) end). -spec vcard_name() -> yconf:validator(). vcard_name() -> vcard_validator( vcard_name, undefined, [{family, undefined, binary()}, {given, undefined, binary()}, {middle, undefined, binary()}, {prefix, undefined, binary()}, {suffix, undefined, binary()}]). -spec vcard_photo() -> yconf:validator(). vcard_photo() -> vcard_validator( vcard_photo, undefined, [{type, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). -spec vcard_adr() -> yconf:validator(). vcard_adr() -> vcard_validator( vcard_adr, [], [{home, false, bool()}, {work, false, bool()}, {postal, false, bool()}, {parcel, false, bool()}, {dom, false, bool()}, {intl, false, bool()}, {pref, false, bool()}, {pobox, undefined, binary()}, {extadd, undefined, binary()}, {street, undefined, binary()}, {locality, undefined, binary()}, {region, undefined, binary()}, {pcode, undefined, binary()}, {ctry, undefined, binary()}]). -spec vcard_label() -> yconf:validator(). vcard_label() -> vcard_validator( vcard_label, [], [{home, false, bool()}, {work, false, bool()}, {postal, false, bool()}, {parcel, false, bool()}, {dom, false, bool()}, {intl, false, bool()}, {pref, false, bool()}, {line, [], list(binary())}]). -spec vcard_tel() -> yconf:validator(). vcard_tel() -> vcard_validator( vcard_tel, [], [{home, false, bool()}, {work, false, bool()}, {voice, false, bool()}, {fax, false, bool()}, {pager, false, bool()}, {msg, false, bool()}, {cell, false, bool()}, {video, false, bool()}, {bbs, false, bool()}, {modem, false, bool()}, {isdn, false, bool()}, {pcs, false, bool()}, {pref, false, bool()}, {number, undefined, binary()}]). -spec vcard_email() -> yconf:validator(). vcard_email() -> vcard_validator( vcard_email, [], [{home, false, bool()}, {work, false, bool()}, {internet, false, bool()}, {pref, false, bool()}, {x400, false, bool()}, {userid, undefined, binary()}]). -spec vcard_geo() -> yconf:validator(). vcard_geo() -> vcard_validator( vcard_geo, undefined, [{lat, undefined, binary()}, {lon, undefined, binary()}]). -spec vcard_logo() -> yconf:validator(). vcard_logo() -> vcard_validator( vcard_logo, undefined, [{type, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). -spec vcard_org() -> yconf:validator(). vcard_org() -> vcard_validator( vcard_org, undefined, [{name, undefined, binary()}, {units, [], list(binary())}]). -spec vcard_sound() -> yconf:validator(). vcard_sound() -> vcard_validator( vcard_sound, undefined, [{phonetic, undefined, binary()}, {binval, undefined, base64()}, {extval, undefined, binary()}]). -spec vcard_key() -> yconf:validator(). vcard_key() -> vcard_validator( vcard_key, undefined, [{type, undefined, binary()}, {cred, undefined, binary()}]). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec db_module(module(), atom()) -> module(). db_module(M, Type) -> try list_to_atom(atom_to_list(M) ++ "_" ++ atom_to_list(Type)) catch _:system_limit -> fail({bad_length, 255}) end. format_addr_port({IP, Port}) -> IPStr = case tuple_size(IP) of 4 -> inet:ntoa(IP); 8 -> "[" ++ inet:ntoa(IP) ++ "]" end, IPStr ++ ":" ++ integer_to_list(Port). -spec format(iolist(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). -spec vcard_validator(atom(), term(), [{atom(), term(), validator()}]) -> validator(). vcard_validator(Name, Default, Schema) -> Defaults = [{Key, Val} || {Key, Val, _} <- Schema], and_then( options( maps:from_list([{Key, Fun} || {Key, _, Fun} <- Schema]), [{return, map}, {unique, true}]), fun(Options) -> merge(Defaults, Options, Name, Default) end). -spec merge([{atom(), term()}], #{atom() => term()}, atom(), T) -> tuple() | T. merge(_, Options, _, Default) when Options == #{} -> Default; merge(Defaults, Options, Name, _) -> list_to_tuple([Name|[maps:get(Key, Options, Val) || {Key, Val} <- Defaults]]). ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/elixir_logger_backend.erl��������������������������������������������������������0000644�0002322�0002322�00000010414�14154362354�021427� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : elixir_logger_backend.erl %%% Author : Mickael Remond <mremond@process-one.net> %%% Purpose : This module bridges lager logs to Elixir Logger. %%% Created : 9 March 2016 by Mickael Remond <mremond@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(elixir_logger_backend). -ifdef(ELIXIR_ENABLED). -behaviour(gen_event). -export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, code_change/3]). -record(state, {level = debug}). init(Opts) -> Level = proplists:get_value(level, Opts, debug), State = #state{level = Level}, {ok, State}. %% @private handle_event({log, LagerMsg}, State) -> #{mode := Mode, truncate := Truncate, level := MinLevel, utc_log := UTCLog} = 'Elixir.Logger.Config':'__data__'(), MsgLevel = severity_to_level(lager_msg:severity(LagerMsg)), case {lager_util:is_loggable(LagerMsg, lager_util:level_to_num(State#state.level), ?MODULE), 'Elixir.Logger':compare_levels(MsgLevel, MinLevel)} of {_, lt}-> {ok, State}; {true, _} -> Metadata = normalize_pid(lager_msg:metadata(LagerMsg)), Message = 'Elixir.Logger.Utils':truncate(lager_msg:message(LagerMsg), Truncate), Timestamp = timestamp(lager_msg:timestamp(LagerMsg), UTCLog), GroupLeader = case proplists:get_value(pid, Metadata, self()) of Pid when is_pid(Pid) -> erlang:process_info(self(), group_leader); _ -> {group_leader, self()} end, notify(Mode, {MsgLevel, GroupLeader, {'Elixir.Logger', Message, Timestamp, Metadata}}), {ok, State}; _ -> {ok, State} end; handle_event(_Msg, State) -> {ok, State}. %% @private %% TODO Handle loglevels handle_call(get_loglevel, State) -> {ok, lager_util:config_to_mask(State#state.level), State}; handle_call({set_loglevel, Config}, State) -> {ok, ok, State#state{level = Config}}. %% @private handle_info(_Msg, State) -> {ok, State}. %% @private terminate(_Reason, _State) -> ok. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. notify(sync, Msg) -> gen_event:sync_notify('Elixir.Logger', Msg); notify(async, Msg) -> gen_event:notify('Elixir.Logger', Msg). normalize_pid(Metadata) -> case proplists:get_value(pid, Metadata) of Pid when is_pid(Pid) -> Metadata; Pid when is_list(Pid) -> M1 = proplists:delete(pid, Metadata), case catch erlang:list_to_pid(Pid) of {'EXIT', _} -> M1; PidAsPid -> [{pid, PidAsPid}|M1] end; _ -> proplists:delete(pid, Metadata) end. %% Return timestamp with milliseconds timestamp(Time, UTCLog) -> {_, _, Micro} = erlang:timestamp(), {Date, {Hours, Minutes, Seconds}} = case UTCLog of true -> calendar:now_to_universal_time(Time); false -> calendar:now_to_local_time(Time) end, {Date, {Hours, Minutes, Seconds, Micro div 1000}}. severity_to_level(debug) -> debug; severity_to_level(info) -> info; severity_to_level(notice) -> info; severity_to_level(warning) -> warn; severity_to_level(error) -> error; severity_to_level(critical) -> error; severity_to_level(alert) -> error; severity_to_level(emergency) -> error. -endif. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/pubsub_index.erl�����������������������������������������������������������������0000644�0002322�0002322�00000004115�14154362354�017615� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : pubsub_index.erl %%% Author : Christophe Romain <christophe.romain@process-one.net> %%% Purpose : Provide uniq integer index for pubsub node %%% Created : 30 Apr 2009 by Christophe Romain <christophe.romain@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %% important note: %% new/1 and free/2 MUST be called inside a transaction bloc -module(pubsub_index). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -export([init/3, new/1, free/2]). init(_Host, _ServerHost, _Opts) -> ejabberd_mnesia:create(?MODULE, pubsub_index, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_index)}]). new(Index) -> case mnesia:read({pubsub_index, Index}) of [I] -> case I#pubsub_index.free of [] -> Id = I#pubsub_index.last + 1, mnesia:write(I#pubsub_index{last = Id}), Id; [Id | Free] -> mnesia:write(I#pubsub_index{free = Free}), Id end; _ -> mnesia:write(#pubsub_index{index = Index, last = 1, free = []}), 1 end. free(Index, Id) -> case mnesia:read({pubsub_index, Index}) of [I] -> Free = I#pubsub_index.free, mnesia:write(I#pubsub_index{free = [Id | Free]}); _ -> ok end. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_mix_mnesia.erl���������������������������������������������������������������0000644�0002322�0002322�00000014563�14154362354�020126� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Created : 1 Dec 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix_mnesia). -behaviour(mod_mix). %% API -export([init/2]). -export([set_channel/6, get_channels/2, get_channel/3, del_channel/3]). -export([set_participant/6, get_participant/4, get_participants/3, del_participant/4]). -export([subscribe/5, unsubscribe/4, unsubscribe/5, get_subscribed/4]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -record(mix_channel, {chan_serv :: {binary(), binary()}, service :: binary(), creator :: jid:jid(), hidden :: boolean(), hmac_key :: binary(), created_at :: erlang:timestamp()}). -record(mix_participant, {user_chan :: {binary(), binary(), binary(), binary()}, chan_serv :: {binary(), binary()}, jid :: jid:jid(), id :: binary(), nick :: binary(), created_at :: erlang:timestamp()}). -record(mix_subscription, {user_chan_node :: {binary(), binary(), binary(), binary(), binary()}, user_chan :: {binary(), binary(), binary(), binary()}, chan_serv_node :: {binary(), binary(), binary()}, chan_serv :: {binary(), binary()}, jid :: jid:jid()}). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> try {atomic, _} = ejabberd_mnesia:create( ?MODULE, mix_channel, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mix_channel)}, {index, [service]}]), {atomic, _} = ejabberd_mnesia:create( ?MODULE, mix_participant, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mix_participant)}, {index, [chan_serv]}]), {atomic, _} = ejabberd_mnesia:create( ?MODULE, mix_subscription, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mix_subscription)}, {index, [user_chan, chan_serv_node, chan_serv]}]), ok catch _:{badmatch, _} -> {error, db_failure} end. set_channel(_LServer, Channel, Service, CreatorJID, Hidden, Key) -> mnesia:dirty_write( #mix_channel{chan_serv = {Channel, Service}, service = Service, creator = jid:remove_resource(CreatorJID), hidden = Hidden, hmac_key = Key, created_at = erlang:timestamp()}). get_channels(_LServer, Service) -> Ret = mnesia:dirty_index_read(mix_channel, Service, #mix_channel.service), {ok, lists:filtermap( fun(#mix_channel{chan_serv = {Channel, _}, hidden = false}) -> {true, Channel}; (_) -> false end, Ret)}. get_channel(_LServer, Channel, Service) -> case mnesia:dirty_read(mix_channel, {Channel, Service}) of [#mix_channel{creator = JID, hidden = Hidden, hmac_key = Key}] -> {ok, {JID, Hidden, Key}}; [] -> {error, notfound} end. del_channel(_LServer, Channel, Service) -> Key = {Channel, Service}, L1 = mnesia:dirty_read(mix_channel, Key), L2 = mnesia:dirty_index_read(mix_participant, Key, #mix_participant.chan_serv), L3 = mnesia:dirty_index_read(mix_subscription, Key, #mix_subscription.chan_serv), lists:foreach(fun mnesia:dirty_delete_object/1, L1++L2++L3). set_participant(_LServer, Channel, Service, JID, ID, Nick) -> {User, Domain, _} = jid:tolower(JID), mnesia:dirty_write( #mix_participant{ user_chan = {User, Domain, Channel, Service}, chan_serv = {Channel, Service}, jid = jid:remove_resource(JID), id = ID, nick = Nick, created_at = erlang:timestamp()}). get_participant(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case mnesia:dirty_read(mix_participant, {User, Domain, Channel, Service}) of [#mix_participant{id = ID, nick = Nick}] -> {ok, {ID, Nick}}; [] -> {error, notfound} end. get_participants(_LServer, Channel, Service) -> Ret = mnesia:dirty_index_read(mix_participant, {Channel, Service}, #mix_participant.chan_serv), {ok, lists:map( fun(#mix_participant{jid = JID, id = ID, nick = Nick}) -> {JID, ID, Nick} end, Ret)}. del_participant(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), mnesia:dirty_delete(mix_participant, {User, Domain, Channel, Service}). subscribe(_LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), BJID = jid:remove_resource(JID), lists:foreach( fun(Node) -> mnesia:dirty_write( #mix_subscription{ user_chan_node = {User, Domain, Channel, Service, Node}, user_chan = {User, Domain, Channel, Service}, chan_serv_node = {Channel, Service, Node}, chan_serv = {Channel, Service}, jid = BJID}) end, Nodes). get_subscribed(_LServer, Channel, Service, Node) -> Ret = mnesia:dirty_index_read(mix_subscription, {Channel, Service, Node}, #mix_subscription.chan_serv_node), {ok, [JID || #mix_subscription{jid = JID} <- Ret]}. unsubscribe(_LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), Ret = mnesia:dirty_index_read(mix_subscription, {User, Domain, Channel, Service}, #mix_subscription.user_chan), lists:foreach(fun mnesia:dirty_delete_object/1, Ret). unsubscribe(_LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), lists:foreach( fun(Node) -> mnesia:dirty_delete(mix_subscription, {User, Domain, Channel, Service, Node}) end, Nodes). %%%=================================================================== %%% Internal functions %%%=================================================================== ���������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_bosh.erl����������������������������������������������������������������0000644�0002322�0002322�00000103464�14154362354�017706� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : ejabberd_bosh.erl %%% Author : Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% Purpose : Manage BOSH sockets %%% Created : 20 Jul 2011 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_bosh). -behaviour(xmpp_socket). -behaviour(p1_fsm). -protocol({xep, 124, '1.11'}). -protocol({xep, 206, '1.4'}). %% API -export([start/2, start/3, start_link/3]). -export([send_xml/2, setopts/2, controlling_process/2, reset_stream/1, change_shaper/2, close/1, sockname/1, peername/1, process_request/3, send/2, get_transport/1, get_owner/1]). %% gen_fsm callbacks -export([init/1, wait_for_session/2, wait_for_session/3, active/2, active/3, handle_event/3, print_state/1, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("bosh.hrl"). %%-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. -define(BOSH_VERSION, <<"1.11">>). -define(NS_BOSH, <<"urn:xmpp:xbosh">>). -define(NS_HTTP_BIND, <<"http://jabber.org/protocol/httpbind">>). -define(DEFAULT_WAIT, 300). -define(DEFAULT_HOLD, 1). -define(DEFAULT_POLLING, 2). -define(MAX_SHAPED_REQUESTS_QUEUE_LEN, 1000). -define(SEND_TIMEOUT, 15000). -type bosh_socket() :: {http_bind, pid(), {inet:ip_address(), inet:port_number()}}. -export_type([bosh_socket/0]). -record(state, {host = <<"">> :: binary(), sid = <<"">> :: binary(), el_ibuf :: p1_queue:queue(), el_obuf :: p1_queue:queue(), shaper_state = none :: ejabberd_shaper:shaper(), c2s_pid :: pid() | undefined, xmpp_ver = <<"">> :: binary(), inactivity_timer :: reference() | undefined, wait_timer :: reference() | undefined, wait_timeout = ?DEFAULT_WAIT :: pos_integer(), inactivity_timeout :: pos_integer(), prev_rid = 0 :: non_neg_integer(), prev_key = <<"">> :: binary(), prev_poll :: erlang:timestamp() | undefined, max_concat = unlimited :: unlimited | non_neg_integer(), responses = gb_trees:empty() :: gb_trees:tree(), receivers = gb_trees:empty() :: gb_trees:tree(), shaped_receivers :: p1_queue:queue(), ip :: inet:ip_address(), max_requests = 1 :: non_neg_integer()}). -record(body, {http_reason = <<"">> :: binary(), attrs = [] :: [{any(), any()}], els = [] :: [fxml_stream:xml_stream_el()], size = 0 :: non_neg_integer()}). start(#body{attrs = Attrs} = Body, IP, SID) -> XMPPDomain = get_attr(to, Attrs), SupervisorProc = gen_mod:get_module_proc(XMPPDomain, mod_bosh), case catch supervisor:start_child(SupervisorProc, [Body, IP, SID]) of {ok, Pid} -> {ok, Pid}; {'EXIT', {noproc, _}} -> check_bosh_module(XMPPDomain), {error, module_not_loaded}; Err -> ?ERROR_MSG("Failed to start BOSH session: ~p", [Err]), {error, Err} end. start(StateName, State) -> p1_fsm:start_link(?MODULE, [StateName, State], ?FSMOPTS). start_link(Body, IP, SID) -> p1_fsm:start_link(?MODULE, [Body, IP, SID], ?FSMOPTS). send({http_bind, FsmRef, IP}, Packet) -> send_xml({http_bind, FsmRef, IP}, Packet). send_xml({http_bind, FsmRef, _IP}, Packet) -> case catch p1_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}, ?SEND_TIMEOUT) of {'EXIT', {timeout, _}} -> {error, timeout}; {'EXIT', _} -> {error, einval}; Res -> Res end. setopts({http_bind, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of true -> p1_fsm:send_all_state_event(FsmRef, {activate, self()}); _ -> case lists:member({active, false}, Opts) of true -> case catch p1_fsm:sync_send_all_state_event(FsmRef, deactivate_socket) of {'EXIT', _} -> {error, einval}; Res -> Res end; _ -> ok end end. controlling_process(_Socket, _Pid) -> ok. reset_stream({http_bind, _FsmRef, _IP} = Socket) -> Socket. change_shaper({http_bind, FsmRef, _IP}, Shaper) -> p1_fsm:send_all_state_event(FsmRef, {change_shaper, Shaper}). close({http_bind, FsmRef, _IP}) -> catch p1_fsm:sync_send_all_state_event(FsmRef, close). sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. peername({http_bind, _FsmRef, IP}) -> {ok, IP}. get_transport(_Socket) -> http_bind. get_owner({http_bind, FsmRef, _IP}) -> FsmRef. process_request(Data, IP, Type) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), Opts = case Type of xml -> [{xml_socket, true} | Opts1]; json -> Opts1 end, MaxStanzaSize = case lists:keysearch(max_stanza_size, 1, Opts) of {value, {_, Size}} -> Size; _ -> infinity end, PayloadSize = iolist_size(Data), if PayloadSize > MaxStanzaSize -> http_error(403, <<"Request Too Large">>, Type); true -> case decode_body(Data, PayloadSize, Type) of {ok, #body{attrs = Attrs} = Body} -> SID = get_attr(sid, Attrs), To = get_attr(to, Attrs), if SID == <<"">>, To == <<"">> -> bosh_response_with_msg(#body{http_reason = <<"Missing 'to' attribute">>, attrs = [{type, <<"terminate">>}, {condition, <<"improper-addressing">>}]}, Type, Body); SID == <<"">> -> case start(Body, IP, make_sid()) of {ok, Pid} -> process_request(Pid, Body, IP, Type); _Err -> bosh_response_with_msg(#body{http_reason = <<"Failed to start BOSH session">>, attrs = [{type, <<"terminate">>}, {condition, <<"internal-server-error">>}]}, Type, Body) end; true -> case mod_bosh:find_session(SID) of {ok, Pid} -> process_request(Pid, Body, IP, Type); error -> bosh_response_with_msg(#body{http_reason = <<"Session ID mismatch">>, attrs = [{type, <<"terminate">>}, {condition, <<"item-not-found">>}]}, Type, Body) end end; {error, Reason} -> http_error(400, Reason, Type) end end. process_request(Pid, Req, _IP, Type) -> case catch p1_fsm:sync_send_event(Pid, Req, infinity) of #body{} = Resp -> bosh_response(Resp, Type); {'EXIT', {Reason, _}} when Reason == noproc; Reason == normal -> bosh_response(#body{http_reason = <<"BOSH session not found">>, attrs = [{type, <<"terminate">>}, {condition, <<"item-not-found">>}]}, Type); {'EXIT', _} -> bosh_response(#body{http_reason = <<"Unexpected error">>, attrs = [{type, <<"terminate">>}, {condition, <<"internal-server-error">>}]}, Type) end. init([#body{attrs = Attrs}, IP, SID]) -> Opts1 = ejabberd_c2s_config:get_c2s_limits(), Opts2 = [{xml_socket, true} | Opts1], Shaper = none, ShaperState = ejabberd_shaper:new(Shaper), Socket = make_socket(self(), IP), XMPPVer = get_attr('xmpp:version', Attrs), XMPPDomain = get_attr(to, Attrs), {InBuf, Opts} = case mod_bosh_opt:prebind(XMPPDomain) of true -> JID = make_random_jid(XMPPDomain), {buf_new(XMPPDomain), [{jid, JID} | Opts2]}; false -> {buf_in([make_xmlstreamstart(XMPPDomain, XMPPVer)], buf_new(XMPPDomain)), Opts2} end, case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of {ok, C2SPid} -> ejabberd_c2s:accept(C2SPid), Inactivity = mod_bosh_opt:max_inactivity(XMPPDomain) div 1000, MaxConcat = mod_bosh_opt:max_concat(XMPPDomain), ShapedReceivers = buf_new(XMPPDomain, ?MAX_SHAPED_REQUESTS_QUEUE_LEN), State = #state{host = XMPPDomain, sid = SID, ip = IP, xmpp_ver = XMPPVer, el_ibuf = InBuf, max_concat = MaxConcat, el_obuf = buf_new(XMPPDomain), inactivity_timeout = Inactivity, shaped_receivers = ShapedReceivers, shaper_state = ShaperState}, NewState = restart_inactivity_timer(State), case mod_bosh:open_session(SID, self()) of ok -> {ok, wait_for_session, NewState}; {error, Reason} -> {stop, Reason} end; {error, Reason} -> {stop, Reason}; ignore -> ignore end. wait_for_session(_Event, State) -> ?ERROR_MSG("Unexpected event in 'wait_for_session': ~p", [_Event]), {next_state, wait_for_session, State}. wait_for_session(#body{attrs = Attrs} = Req, From, State) -> RID = get_attr(rid, Attrs), ?DEBUG("Got request:~n** RequestID: ~p~n** Request: " "~p~n** From: ~p~n** State: ~p", [RID, Req, From, State]), Wait = min(get_attr(wait, Attrs, undefined), ?DEFAULT_WAIT), Hold = min(get_attr(hold, Attrs, undefined), ?DEFAULT_HOLD), NewKey = get_attr(newkey, Attrs), Type = get_attr(type, Attrs), Requests = Hold + 1, PollTime = if Wait == 0, Hold == 0 -> erlang:timestamp(); true -> undefined end, MaxPause = mod_bosh_opt:max_pause(State#state.host) div 1000, Resp = #body{attrs = [{sid, State#state.sid}, {wait, Wait}, {ver, ?BOSH_VERSION}, {polling, ?DEFAULT_POLLING}, {inactivity, State#state.inactivity_timeout}, {hold, Hold}, {'xmpp:restartlogic', true}, {requests, Requests}, {secure, true}, {maxpause, MaxPause}, {'xmlns:xmpp', ?NS_BOSH}, {'xmlns:stream', ?NS_STREAM}, {from, State#state.host}]}, {ShaperState, _} = ejabberd_shaper:update(State#state.shaper_state, Req#body.size), State1 = State#state{wait_timeout = Wait, prev_rid = RID, prev_key = NewKey, prev_poll = PollTime, shaper_state = ShaperState, max_requests = Requests}, Els = maybe_add_xmlstreamend(Req#body.els, Type), State2 = route_els(State1, Els), {State3, RespEls} = get_response_els(State2), State4 = stop_inactivity_timer(State3), case RespEls of [{xmlstreamstart, _, _} = El1] -> OutBuf = buf_in([El1], State4#state.el_obuf), State5 = restart_wait_timer(State4), Receivers = gb_trees:insert(RID, {From, Resp}, State5#state.receivers), {next_state, active, State5#state{receivers = Receivers, el_obuf = OutBuf}}; [] -> State5 = restart_wait_timer(State4), Receivers = gb_trees:insert(RID, {From, Resp}, State5#state.receivers), {next_state, active, State5#state{receivers = Receivers}}; _ -> reply_next_state(State4, Resp#body{els = RespEls}, RID, From) end; wait_for_session(_Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'wait_for_session': ~p", [_Event]), {reply, {error, badarg}, wait_for_session, State}. active({#body{} = Body, From}, State) -> active1(Body, From, State); active(_Event, State) -> ?ERROR_MSG("Unexpected event in 'active': ~p", [_Event]), {next_state, active, State}. active(#body{attrs = Attrs, size = Size} = Req, From, State) -> ?DEBUG("Got request:~n** Request: ~p~n** From: " "~p~n** State: ~p", [Req, From, State]), {ShaperState, Pause} = ejabberd_shaper:update(State#state.shaper_state, Size), State1 = State#state{shaper_state = ShaperState}, if Pause > 0 -> TRef = start_shaper_timer(Pause), try p1_queue:in({TRef, From, Req}, State1#state.shaped_receivers) of Q -> State2 = stop_inactivity_timer(State1), {next_state, active, State2#state{shaped_receivers = Q}} catch error:full -> misc:cancel_timer(TRef), RID = get_attr(rid, Attrs), reply_stop(State1, #body{http_reason = <<"Too many requests">>, attrs = [{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"policy-violation">>}]}, From, RID) end; true -> active1(Req, From, State1) end; active(_Event, _From, State) -> ?ERROR_MSG("Unexpected sync event in 'active': ~p", [_Event]), {reply, {error, badarg}, active, State}. active1(#body{attrs = Attrs} = Req, From, State) -> RID = get_attr(rid, Attrs), Key = get_attr(key, Attrs), IsValidKey = is_valid_key(State#state.prev_key, Key), IsOveractivity = is_overactivity(State#state.prev_poll), Type = get_attr(type, Attrs), if RID > State#state.prev_rid + State#state.max_requests -> reply_stop(State, #body{http_reason = <<"Request ID is out of range">>, attrs = [{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"item-not-found">>}]}, From, RID); RID > State#state.prev_rid + 1 -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:insert(RID, {From, Req}, State1#state.receivers), {next_state, active, State1#state{receivers = Receivers}}; RID =< State#state.prev_rid -> %% TODO: do we need to check 'key' here? It seems so... case gb_trees:lookup(RID, State#state.responses) of {value, PrevBody} -> {next_state, active, do_reply(State, From, PrevBody, RID)}; none -> State1 = drop_holding_receiver(State, RID), State2 = stop_inactivity_timer(State1), State3 = restart_wait_timer(State2), Receivers = gb_trees:insert(RID, {From, Req}, State3#state.receivers), {next_state, active, State3#state{receivers = Receivers}} end; not IsValidKey -> reply_stop(State, #body{http_reason = <<"Session key mismatch">>, attrs = [{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"item-not-found">>}]}, From, RID); IsOveractivity -> reply_stop(State, #body{http_reason = <<"Too many requests">>, attrs = [{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"policy-violation">>}]}, From, RID); true -> State1 = stop_inactivity_timer(State), State2 = stop_wait_timer(State1), Els = case get_attr('xmpp:restart', Attrs, false) of true -> XMPPDomain = get_attr(to, Attrs, State#state.host), XMPPVer = get_attr('xmpp:version', Attrs, State#state.xmpp_ver), [make_xmlstreamstart(XMPPDomain, XMPPVer)]; false -> Req#body.els end, State3 = route_els(State2, maybe_add_xmlstreamend(Els, Type)), {State4, RespEls} = get_response_els(State3), NewKey = get_attr(newkey, Attrs, Key), Pause = get_attr(pause, Attrs, undefined), NewPoll = case State#state.prev_poll of undefined -> undefined; _ -> erlang:timestamp() end, State5 = State4#state{prev_poll = NewPoll, prev_key = NewKey}, if Type == <<"terminate">> -> reply_stop(State5, #body{http_reason = <<"Session close">>, attrs = [{<<"type">>, <<"terminate">>}], els = RespEls}, From, RID); Pause /= undefined -> State6 = drop_holding_receiver(State5), State7 = restart_inactivity_timer(State6, Pause), InBuf = buf_in(RespEls, State7#state.el_ibuf), {next_state, active, State7#state{prev_rid = RID, el_ibuf = InBuf}}; RespEls == [] -> State6 = drop_holding_receiver(State5), State7 = stop_inactivity_timer(State6), State8 = restart_wait_timer(State7), Receivers = gb_trees:insert(RID, {From, #body{}}, State8#state.receivers), {next_state, active, State8#state{prev_rid = RID, receivers = Receivers}}; true -> State6 = drop_holding_receiver(State5), reply_next_state(State6#state{prev_rid = RID}, #body{els = RespEls}, RID, From) end end. handle_event({activate, C2SPid}, StateName, State) -> State1 = route_els(State#state{c2s_pid = C2SPid}), {next_state, StateName, State1}; handle_event({change_shaper, Shaper}, StateName, State) -> {next_state, StateName, State#state{shaper_state = Shaper}}; handle_event(_Event, StateName, State) -> ?ERROR_MSG("Unexpected event in '~ts': ~p", [StateName, _Event]), {next_state, StateName, State}. handle_sync_event({send_xml, {xmlstreamstart, _, _} = El}, _From, StateName, State) when State#state.xmpp_ver >= <<"1.0">> -> OutBuf = buf_in([El], State#state.el_obuf), {reply, ok, StateName, State#state{el_obuf = OutBuf}}; handle_sync_event({send_xml, El}, _From, StateName, State) -> OutBuf = buf_in([El], State#state.el_obuf), State1 = State#state{el_obuf = OutBuf}, case gb_trees:lookup(State1#state.prev_rid, State1#state.receivers) of {value, {From, Body}} -> {State2, Els} = get_response_els(State1), {reply, ok, StateName, reply(State2, Body#body{els = Els}, State2#state.prev_rid, From)}; none -> State2 = case p1_queue:out(State1#state.shaped_receivers) of {{value, {TRef, From, Body}}, Q} -> misc:cancel_timer(TRef), p1_fsm:send_event(self(), {Body, From}), State1#state{shaped_receivers = Q}; _ -> State1 end, {reply, ok, StateName, State2} end; handle_sync_event(close, _From, _StateName, State) -> {stop, normal, State}; handle_sync_event(deactivate_socket, _From, StateName, StateData) -> {reply, ok, StateName, StateData#state{c2s_pid = undefined}}; handle_sync_event(_Event, _From, StateName, State) -> ?ERROR_MSG("Unexpected sync event in '~ts': ~p", [StateName, _Event]), {reply, {error, badarg}, StateName, State}. handle_info({timeout, TRef, wait_timeout}, StateName, #state{wait_timer = TRef} = State) -> State2 = State#state{wait_timer = undefined}, {next_state, StateName, drop_holding_receiver(State2)}; handle_info({timeout, TRef, inactive}, _StateName, #state{inactivity_timer = TRef} = State) -> {stop, normal, State}; handle_info({timeout, TRef, shaper_timeout}, StateName, State) -> case p1_queue:out(State#state.shaped_receivers) of {{value, {TRef, From, Req}}, Q} -> p1_fsm:send_event(self(), {Req, From}), {next_state, StateName, State#state{shaped_receivers = Q}}; {{value, _}, _} -> ?ERROR_MSG("shaper_timeout mismatch:~n** TRef: ~p~n** " "State: ~p", [TRef, State]), {stop, normal, State}; _ -> {next_state, StateName, State} end; handle_info(_Info, StateName, State) -> ?ERROR_MSG("Unexpected info:~n** Msg: ~p~n** StateName: ~p", [_Info, StateName]), {next_state, StateName, State}. terminate(_Reason, _StateName, State) -> mod_bosh:close_session(State#state.sid), case State#state.c2s_pid of C2SPid when is_pid(C2SPid) -> p1_fsm:send_event(C2SPid, closed); _ -> ok end, bounce_receivers(State, closed), bounce_els_from_obuf(State). code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. print_state(State) -> State. route_els(#state{el_ibuf = Buf, c2s_pid = C2SPid} = State) -> NewBuf = p1_queue:dropwhile( fun(El) -> p1_fsm:send_event(C2SPid, El), true end, Buf), State#state{el_ibuf = NewBuf}. route_els(State, Els) -> case State#state.c2s_pid of C2SPid when is_pid(C2SPid) -> lists:foreach(fun (El) -> p1_fsm:send_event(C2SPid, El) end, Els), State; _ -> InBuf = buf_in(Els, State#state.el_ibuf), State#state{el_ibuf = InBuf} end. get_response_els(#state{el_obuf = OutBuf, max_concat = MaxConcat} = State) -> {Els, NewOutBuf} = buf_out(OutBuf, MaxConcat), {State#state{el_obuf = NewOutBuf}, Els}. reply(State, Body, RID, From) -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:delete_any(RID, State1#state.receivers), State2 = do_reply(State1, From, Body, RID), case catch gb_trees:take_smallest(Receivers) of {NextRID, {From1, Req}, Receivers1} when NextRID == RID + 1 -> p1_fsm:send_event(self(), {Req, From1}), State2#state{receivers = Receivers1}; _ -> State2#state{receivers = Receivers} end. reply_next_state(State, Body, RID, From) -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:delete_any(RID, State1#state.receivers), State2 = do_reply(State1, From, Body, RID), case catch gb_trees:take_smallest(Receivers) of {NextRID, {From1, Req}, Receivers1} when NextRID == RID + 1 -> active(Req, From1, State2#state{receivers = Receivers1}); _ -> {next_state, active, State2#state{receivers = Receivers}} end. reply_stop(State, Body, From, RID) -> {stop, normal, do_reply(State, From, Body, RID)}. drop_holding_receiver(State) -> drop_holding_receiver(State, State#state.prev_rid). drop_holding_receiver(State, RID) -> case gb_trees:lookup(RID, State#state.receivers) of {value, {From, Body}} -> State1 = restart_inactivity_timer(State), Receivers = gb_trees:delete_any(RID, State1#state.receivers), State2 = State1#state{receivers = Receivers}, do_reply(State2, From, Body, RID); none -> restart_inactivity_timer(State) end. do_reply(State, From, Body, RID) -> ?DEBUG("Send reply:~n** RequestID: ~p~n** Reply: " "~p~n** To: ~p~n** State: ~p", [RID, Body, From, State]), p1_fsm:reply(From, Body), Responses = gb_trees:delete_any(RID, State#state.responses), Responses1 = case gb_trees:size(Responses) of N when N < State#state.max_requests; N == 0 -> Responses; _ -> element(3, gb_trees:take_smallest(Responses)) end, Responses2 = gb_trees:insert(RID, Body, Responses1), State#state{responses = Responses2}. bounce_receivers(State, _Reason) -> Receivers = gb_trees:to_list(State#state.receivers), ShapedReceivers = lists:map(fun ({_, From, #body{attrs = Attrs} = Body}) -> RID = get_attr(rid, Attrs), {RID, {From, Body}} end, p1_queue:to_list(State#state.shaped_receivers)), lists:foldl(fun ({RID, {From, _Body}}, AccState) -> NewBody = #body{http_reason = <<"Session closed">>, attrs = [{type, <<"terminate">>}, {condition, <<"other-request">>}]}, do_reply(AccState, From, NewBody, RID) end, State, Receivers ++ ShapedReceivers). bounce_els_from_obuf(State) -> Opts = ejabberd_config:codec_options(), p1_queue:foreach( fun({xmlstreamelement, El}) -> try xmpp:decode(El, ?NS_CLIENT, Opts) of Pkt when ?is_stanza(Pkt) -> case {xmpp:get_from(Pkt), xmpp:get_to(Pkt)} of {#jid{}, #jid{}} -> ejabberd_router:route(Pkt); _ -> ok end; _ -> ok catch _:{xmpp_codec, _} -> ok end; (_) -> ok end, State#state.el_obuf). is_valid_key(<<"">>, <<"">>) -> true; is_valid_key(PrevKey, Key) -> str:sha(Key) == PrevKey. is_overactivity(undefined) -> false; is_overactivity(PrevPoll) -> PollPeriod = timer:now_diff(erlang:timestamp(), PrevPoll) div 1000000, if PollPeriod < (?DEFAULT_POLLING) -> true; true -> false end. make_xmlstreamstart(XMPPDomain, Version) -> VersionEl = case Version of <<"">> -> []; _ -> [{<<"version">>, Version}] end, {xmlstreamstart, <<"stream:stream">>, [{<<"to">>, XMPPDomain}, {<<"xmlns">>, ?NS_CLIENT}, {<<"xmlns:xmpp">>, ?NS_BOSH}, {<<"xmlns:stream">>, ?NS_STREAM} | VersionEl]}. maybe_add_xmlstreamend(Els, <<"terminate">>) -> Els ++ [{xmlstreamend, <<"stream:stream">>}]; maybe_add_xmlstreamend(Els, _) -> Els. encode_body(#body{attrs = Attrs, els = Els}, Type) -> Attrs1 = lists:map(fun ({K, V}) when is_atom(K) -> AmK = iolist_to_binary(atom_to_list(K)), case V of true -> {AmK, <<"true">>}; false -> {AmK, <<"false">>}; I when is_integer(I), I >= 0 -> {AmK, integer_to_binary(I)}; _ -> {AmK, V} end; ({K, V}) -> {K, V} end, Attrs), Attrs2 = [{<<"xmlns">>, ?NS_HTTP_BIND} | Attrs1], {Attrs3, XMLs} = lists:foldr(fun ({xmlstreamraw, XML}, {AttrsAcc, XMLBuf}) -> {AttrsAcc, [XML | XMLBuf]}; ({xmlstreamelement, #xmlel{name = <<"stream:error">>} = El}, {AttrsAcc, XMLBuf}) -> {[{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"remote-stream-error">>}, {<<"xmlns:stream">>, ?NS_STREAM} | AttrsAcc], [encode_element(El, Type) | XMLBuf]}; ({xmlstreamelement, #xmlel{name = <<"stream:features">>} = El}, {AttrsAcc, XMLBuf}) -> {lists:keystore(<<"xmlns:stream">>, 1, AttrsAcc, {<<"xmlns:stream">>, ?NS_STREAM}), [encode_element(El, Type) | XMLBuf]}; ({xmlstreamelement, #xmlel{name = Name, attrs = EAttrs} = El}, {AttrsAcc, XMLBuf}) when Name == <<"message">>; Name == <<"presence">>; Name == <<"iq">> -> NewAttrs = lists:keystore( <<"xmlns">>, 1, EAttrs, {<<"xmlns">>, ?NS_CLIENT}), NewEl = El#xmlel{attrs = NewAttrs}, {AttrsAcc, [encode_element(NewEl, Type) | XMLBuf]}; ({xmlstreamelement, El}, {AttrsAcc, XMLBuf}) -> {AttrsAcc, [encode_element(El, Type) | XMLBuf]}; ({xmlstreamend, _}, {AttrsAcc, XMLBuf}) -> {[{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"remote-stream-error">>} | AttrsAcc], XMLBuf}; ({xmlstreamstart, <<"stream:stream">>, SAttrs}, {AttrsAcc, XMLBuf}) -> StreamID = fxml:get_attr_s(<<"id">>, SAttrs), NewAttrs = case fxml:get_attr_s(<<"version">>, SAttrs) of <<"">> -> [{<<"authid">>, StreamID} | AttrsAcc]; V -> lists:keystore(<<"xmlns:xmpp">>, 1, [{<<"xmpp:version">>, V}, {<<"authid">>, StreamID} | AttrsAcc], {<<"xmlns:xmpp">>, ?NS_BOSH}) end, {NewAttrs, XMLBuf}; ({xmlstreamerror, _}, {AttrsAcc, XMLBuf}) -> {[{<<"type">>, <<"terminate">>}, {<<"condition">>, <<"remote-stream-error">>} | AttrsAcc], XMLBuf}; (_, Acc) -> Acc end, {Attrs2, []}, Els), case XMLs of [] when Type == xml -> [<<"<body">>, attrs_to_list(Attrs3), <<"/>">>]; _ when Type == xml -> [<<"<body">>, attrs_to_list(Attrs3), $>, XMLs, <<"</body>">>] end. encode_element(El, xml) -> fxml:element_to_binary(El); encode_element(El, json) -> El. decode_body(Data, Size, Type) -> case decode(Data, Type) of #xmlel{name = <<"body">>, attrs = Attrs, children = Els} -> case attrs_to_body_attrs(Attrs) of {error, _} = Err -> Err; BodyAttrs -> case get_attr(rid, BodyAttrs) of <<"">> -> {error, <<"Missing \"rid\" attribute">>}; _ -> Els1 = lists:flatmap(fun (#xmlel{} = El) -> [{xmlstreamelement, El}]; (_) -> [] end, Els), {ok, #body{attrs = BodyAttrs, size = Size, els = Els1}} end end; #xmlel{} -> {error, <<"Unexpected payload">>}; _ when Type == xml -> {error, <<"XML is not well-formed">>}; _ when Type == json -> {error, <<"JSON is not well-formed">>} end. decode(Data, xml) -> fxml_stream:parse_element(Data); decode(Data, json) -> Data. attrs_to_body_attrs(Attrs) -> lists:foldl(fun (_, {error, Reason}) -> {error, Reason}; ({Attr, Val}, Acc) -> try case Attr of <<"ver">> -> [{ver, Val} | Acc]; <<"xmpp:version">> -> [{'xmpp:version', Val} | Acc]; <<"type">> -> [{type, Val} | Acc]; <<"key">> -> [{key, Val} | Acc]; <<"newkey">> -> [{newkey, Val} | Acc]; <<"xmlns">> -> Val = (?NS_HTTP_BIND), Acc; <<"secure">> -> [{secure, to_bool(Val)} | Acc]; <<"xmpp:restart">> -> [{'xmpp:restart', to_bool(Val)} | Acc]; <<"to">> -> [{to, jid:nameprep(Val)} | Acc]; <<"wait">> -> [{wait, to_int(Val, 0)} | Acc]; <<"ack">> -> [{ack, to_int(Val, 0)} | Acc]; <<"sid">> -> [{sid, Val} | Acc]; <<"hold">> -> [{hold, to_int(Val, 0)} | Acc]; <<"rid">> -> [{rid, to_int(Val, 0)} | Acc]; <<"pause">> -> [{pause, to_int(Val, 0)} | Acc]; _ -> [{Attr, Val} | Acc] end catch _:_ -> {error, <<"Invalid \"", Attr/binary, "\" attribute">>} end end, [], Attrs). to_int(S, Min) -> case binary_to_integer(S) of I when I >= Min -> I; _ -> erlang:error(badarg) end. to_bool(<<"true">>) -> true; to_bool(<<"1">>) -> true; to_bool(<<"false">>) -> false; to_bool(<<"0">>) -> false. attrs_to_list(Attrs) -> [attr_to_list(A) || A <- Attrs]. attr_to_list({Name, Value}) -> [$\s, Name, $=, $', fxml:crypt(Value), $']. bosh_response(Body, Type) -> CType = case Type of xml -> ?CT_XML; json -> ?CT_JSON end, {200, Body#body.http_reason, ?HEADER(CType), encode_body(Body, Type)}. bosh_response_with_msg(Body, Type, RcvBody) -> ?DEBUG("Send error reply:~p~n** Receiced body: ~p", [Body, RcvBody]), bosh_response(Body, Type). http_error(Status, Reason, Type) -> CType = case Type of xml -> ?CT_XML; json -> ?CT_JSON end, {Status, Reason, ?HEADER(CType), <<"">>}. make_sid() -> str:sha(p1_rand:get_string()). -compile({no_auto_import, [{min, 2}]}). min(undefined, B) -> B; min(A, B) -> erlang:min(A, B). check_bosh_module(XmppDomain) -> case gen_mod:is_loaded(XmppDomain, mod_bosh) of true -> ok; false -> ?ERROR_MSG("You are trying to use BOSH (HTTP Bind) " "in host ~p, but the module mod_bosh " "is not started in that host. Configure " "your BOSH client to connect to the correct " "host, or add your desired host to the " "configuration, or check your 'modules' " "section in your ejabberd configuration " "file.", [XmppDomain]) end. get_attr(Attr, Attrs) -> get_attr(Attr, Attrs, <<"">>). get_attr(Attr, Attrs, Default) -> case lists:keysearch(Attr, 1, Attrs) of {value, {_, Val}} -> Val; _ -> Default end. buf_new(Host) -> buf_new(Host, unlimited). buf_new(Host, Limit) -> QueueType = mod_bosh_opt:queue_type(Host), p1_queue:new(QueueType, Limit). buf_in(Xs, Buf) -> lists:foldl(fun p1_queue:in/2, Buf, Xs). buf_out(Buf, Num) when is_integer(Num), Num > 0 -> buf_out(Buf, Num, []); buf_out(Buf, _) -> {p1_queue:to_list(Buf), p1_queue:clear(Buf)}. buf_out(Buf, 0, Els) -> {lists:reverse(Els), Buf}; buf_out(Buf, I, Els) -> case p1_queue:out(Buf) of {{value, El}, NewBuf} -> buf_out(NewBuf, I - 1, [El | Els]); {empty, _} -> buf_out(Buf, 0, Els) end. restart_timer(TRef, Timeout, Msg) -> misc:cancel_timer(TRef), erlang:start_timer(timer:seconds(Timeout), self(), Msg). restart_inactivity_timer(#state{inactivity_timeout = Timeout} = State) -> restart_inactivity_timer(State, Timeout). restart_inactivity_timer(#state{inactivity_timer = TRef} = State, Timeout) -> NewTRef = restart_timer(TRef, Timeout, inactive), State#state{inactivity_timer = NewTRef}. stop_inactivity_timer(#state{inactivity_timer = TRef} = State) -> misc:cancel_timer(TRef), State#state{inactivity_timer = undefined}. restart_wait_timer(#state{wait_timer = TRef, wait_timeout = Timeout} = State) -> NewTRef = restart_timer(TRef, Timeout, wait_timeout), State#state{wait_timer = NewTRef}. stop_wait_timer(#state{wait_timer = TRef} = State) -> misc:cancel_timer(TRef), State#state{wait_timer = undefined}. start_shaper_timer(Timeout) -> erlang:start_timer(Timeout, self(), shaper_timeout). make_random_jid(Host) -> User = p1_rand:get_string(), jid:make(User, Host, p1_rand:get_string()). make_socket(Pid, IP) -> {http_bind, Pid, IP}. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_doc.erl�����������������������������������������������������������������0000644�0002322�0002322�00000043517�14154362354�017522� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_doc.erl %%% Purpose : Options documentation generator %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_doc). %% API -export([man/0, man/1, have_a2x/0]). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== man() -> man(<<"en">>). man(Lang) when is_list(Lang) -> man(list_to_binary(Lang)); man(Lang) -> {ModDoc, SubModDoc} = lists:foldl( fun(M, {Mods, SubMods} = Acc) -> case lists:prefix("mod_", atom_to_list(M)) orelse lists:prefix("Elixir.Mod", atom_to_list(M)) of true -> try M:mod_doc() of #{desc := Descr} = Map -> DocOpts = maps:get(opts, Map, []), Example = maps:get(example, Map, []), {[{M, Descr, DocOpts, #{example => Example}}|Mods], SubMods}; #{opts := DocOpts} -> {ParentMod, Backend} = strip_backend_suffix(M), {Mods, dict:append(ParentMod, {M, Backend, DocOpts}, SubMods)} catch _:undef -> case erlang:function_exported( M, mod_options, 1) of true -> warn("module ~s is not documented", [M]); false -> ok end, Acc end; false -> Acc end end, {[], dict:new()}, ejabberd_config:beams(all)), Doc = lists:flatmap( fun(M) -> try M:doc() catch _:undef -> [] end end, ejabberd_config:callback_modules(all)), Options = ["TOP LEVEL OPTIONS", "-----------------", tr(Lang, ?T("This section describes top level options of ejabberd.")), io_lib:nl()] ++ lists:flatmap( fun(Opt) -> opt_to_man(Lang, Opt, 1) end, lists:keysort(1, Doc)), ModDoc1 = lists:map( fun({M, Descr, DocOpts, Ex}) -> case dict:find(M, SubModDoc) of {ok, Backends} -> {M, Descr, DocOpts, Backends, Ex}; error -> {M, Descr, DocOpts, [], Ex} end end, ModDoc), ModOptions = [io_lib:nl(), "MODULES", "-------", "[[modules]]", tr(Lang, ?T("This section describes options of all ejabberd modules.")), io_lib:nl()] ++ lists:flatmap( fun({M, Descr, DocOpts, Backends, Example}) -> ModName = atom_to_list(M), [io_lib:nl(), ModName, lists:duplicate(length(atom_to_list(M)), $~), "[[" ++ ModName ++ "]]", io_lib:nl()] ++ tr_multi(Lang, Descr) ++ [io_lib:nl()] ++ opts_to_man(Lang, [{M, '', DocOpts}|Backends]) ++ format_example(0, Lang, Example) end, lists:keysort(1, ModDoc1)), ListenOptions = [io_lib:nl(), "LISTENERS", "-------", "[[listeners]]", tr(Lang, ?T("This section describes options of all ejabberd listeners.")), io_lib:nl(), "TODO"], AsciiData = [[unicode:characters_to_binary(Line), io_lib:nl()] || Line <- man_header(Lang) ++ Options ++ [io_lib:nl()] ++ ModOptions ++ ListenOptions ++ man_footer(Lang)], warn_undocumented_modules(ModDoc1), warn_undocumented_options(Doc), write_man(AsciiData). %%%=================================================================== %%% Internal functions %%%=================================================================== opts_to_man(Lang, [{_, _, []}]) -> Text = tr(Lang, ?T("The module has no options.")), [Text, io_lib:nl()]; opts_to_man(Lang, Backends) -> lists:flatmap( fun({_, Backend, DocOpts}) when DocOpts /= [] -> Text = if Backend == '' -> tr(Lang, ?T("Available options")); true -> lists:flatten( io_lib:format( tr(Lang, ?T("Available options for '~s' backend")), [Backend])) end, [Text ++ ":", lists:duplicate(length(Text)+1, $^)| lists:flatmap( fun(Opt) -> opt_to_man(Lang, Opt, 1) end, lists:keysort(1, DocOpts))] ++ [io_lib:nl()]; (_) -> [] end, Backends). opt_to_man(Lang, {Option, Options}, Level) -> [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ format_example(Level, Lang, Options); opt_to_man(Lang, {Option, Options, Children}, Level) -> [format_option(Lang, Option, Options)|format_desc(Lang, Options)] ++ lists:append( [[H ++ ":"|T] || [H|T] <- lists:map( fun(Opt) -> opt_to_man(Lang, Opt, Level+1) end, lists:keysort(1, Children))]) ++ [io_lib:nl()|format_example(Level, Lang, Options)]. format_option(Lang, Option, #{note := Note, value := Val}) -> "\n\n_Note_ about the next option: " ++ Note ++ ":\n\n"++ "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++ tr(Lang, Val) ++ "]'::"; format_option(Lang, Option, #{value := Val}) -> "*" ++ atom_to_list(Option) ++ "*: 'pass:[" ++ tr(Lang, Val) ++ "]'::"; format_option(_Lang, Option, #{}) -> "*" ++ atom_to_list(Option) ++ "*::". format_desc(Lang, #{desc := Desc}) -> tr_multi(Lang, Desc). format_example(Level, Lang, #{example := [_|_] = Example}) -> case lists:all(fun is_list/1, Example) of true -> if Level == 0 -> ["*Example*:", "^^^^^^^^^^"]; true -> ["+", "*Example*:", "+"] end ++ format_yaml(Example); false when Level == 0 -> ["Examples:", "^^^^^^^^^"] ++ lists:flatmap( fun({Text, Lines}) -> [tr(Lang, Text)] ++ format_yaml(Lines) end, Example); false -> lists:flatmap( fun(Block) -> ["+", "''''", "+"|Block] end, lists:map( fun({Text, Lines}) -> [tr(Lang, Text), "+"] ++ format_yaml(Lines) end, Example)) end; format_example(_, _, _) -> []. format_yaml(Lines) -> ["==========================", "[source,yaml]", "----"|Lines] ++ ["----", "=========================="]. man_header(Lang) -> ["ejabberd.yml(5)", "===============", ":doctype: manpage", ":version: " ++ binary_to_list(ejabberd_config:version()), io_lib:nl(), "NAME", "----", "ejabberd.yml - " ++ tr(Lang, ?T("main configuration file for ejabberd.")), io_lib:nl(), "SYNOPSIS", "--------", "ejabberd.yml", io_lib:nl(), "DESCRIPTION", "-----------", tr(Lang, ?T("The configuration file is written in " "https://en.wikipedia.org/wiki/YAML[YAML] language.")), io_lib:nl(), tr(Lang, ?T("WARNING: YAML is indentation sensitive, so make sure you respect " "indentation, or otherwise you will get pretty cryptic " "configuration errors.")), io_lib:nl(), tr(Lang, ?T("Logically, configuration options are splitted into 3 main categories: " "'Modules', 'Listeners' and everything else called 'Top Level' options. " "Thus this document is splitted into 3 main chapters describing each " "category separately. So, the contents of ejabberd.yml will typically " "look like this:")), io_lib:nl(), "==========================", "[source,yaml]", "----", "hosts:", " - example.com", " - domain.tld", "loglevel: info", "...", "listen:", " -", " port: 5222", " module: ejabberd_c2s", " ...", "modules:", " mod_roster: {}", " ...", "----", "==========================", io_lib:nl(), tr(Lang, ?T("Any configuration error (such as syntax error, unknown option " "or invalid option value) is fatal in the sense that ejabberd will " "refuse to load the whole configuration file and will not start or will " "abort configuration reload.")), io_lib:nl(), tr(Lang, ?T("All options can be changed in runtime by running 'ejabberdctl " "reload-config' command. Configuration reload is atomic: either all options " "are accepted and applied simultaneously or the new configuration is " "refused without any impact on currently running configuration.")), io_lib:nl(), tr(Lang, ?T("Some options can be specified for particular virtual host(s) only " "using 'host_config' or 'append_host_config' options. Such options " "are called 'local'. Examples are 'modules', 'auth_method' and 'default_db'. " "The options that cannot be defined per virtual host are called 'global'. " "Examples are 'loglevel', 'certfiles' and 'listen'. It is a configuration " "mistake to put 'global' options under 'host_config' or 'append_host_config' " "section - ejabberd will refuse to load such configuration.")), io_lib:nl(), str:format( tr(Lang, ?T("It is not recommended to write ejabberd.yml from scratch. Instead it is " "better to start from \"default\" configuration file available at ~s. " "Once you get ejabberd running you can start changing configuration " "options to meet your requirements.")), [default_config_url()]), io_lib:nl(), str:format( tr(Lang, ?T("Note that this document is intended to provide comprehensive description of " "all configuration options that can be consulted to understand the meaning " "of a particular option, its format and possible values. It will be quite " "hard to understand how to configure ejabberd by reading this document only " "- for this purpose the reader is recommended to read online Configuration " "Guide available at ~s.")), [configuration_guide_url()]), io_lib:nl()]. man_footer(Lang) -> {Year, _, _} = date(), [io_lib:nl(), "AUTHOR", "------", "https://www.process-one.net[ProcessOne].", io_lib:nl(), "VERSION", "-------", str:format( tr(Lang, ?T("This document describes the configuration file of ejabberd ~ts. " "Configuration options of other ejabberd versions " "may differ significantly.")), [ejabberd_config:version()]), io_lib:nl(), "REPORTING BUGS", "--------------", tr(Lang, ?T("Report bugs to <https://github.com/processone/ejabberd/issues>")), io_lib:nl(), "SEE ALSO", "---------", tr(Lang, ?T("Default configuration file")) ++ ": " ++ default_config_url(), io_lib:nl(), tr(Lang, ?T("Main site")) ++ ": <https://ejabberd.im>", io_lib:nl(), tr(Lang, ?T("Documentation")) ++ ": <https://docs.ejabberd.im>", io_lib:nl(), tr(Lang, ?T("Configuration Guide")) ++ ": " ++ configuration_guide_url(), io_lib:nl(), tr(Lang, ?T("Source code")) ++ ": <https://github.com/processone/ejabberd>", io_lib:nl(), "COPYING", "-------", "Copyright (c) 2002-" ++ integer_to_list(Year) ++ " https://www.process-one.net[ProcessOne]."]. tr(Lang, {Format, Args}) -> unicode:characters_to_list( str:format( translate:translate(Lang, iolist_to_binary(Format)), Args)); tr(Lang, Txt) -> unicode:characters_to_list(translate:translate(Lang, iolist_to_binary(Txt))). tr_multi(Lang, Txt) when is_binary(Txt) -> tr_multi(Lang, [Txt]); tr_multi(Lang, {Format, Args}) -> tr_multi(Lang, [{Format, Args}]); tr_multi(Lang, Lines) when is_list(Lines) -> [tr(Lang, Txt) || Txt <- Lines]. write_man(AsciiData) -> case file:get_cwd() of {ok, Cwd} -> AsciiDocFile = filename:join(Cwd, "ejabberd.yml.5.txt"), ManPage = filename:join(Cwd, "ejabberd.yml.5"), case file:write_file(AsciiDocFile, AsciiData) of ok -> Ret = run_a2x(Cwd, AsciiDocFile), %%file:delete(AsciiDocFile), case Ret of ok -> {ok, lists:flatten( io_lib:format( "The manpage saved as ~ts", [ManPage]))}; {error, Error} -> {error, lists:flatten( io_lib:format( "Failed to generate manpage: ~ts", [Error]))} end; {error, Reason} -> {error, lists:flatten( io_lib:format( "Failed to write to ~ts: ~s", [AsciiDocFile, file:format_error(Reason)]))} end; {error, Reason} -> {error, lists:flatten( io_lib:format("Failed to get current directory: ~s", [file:format_error(Reason)]))} end. have_a2x() -> case os:find_executable("a2x") of false -> false; Path -> {true, Path} end. run_a2x(Cwd, AsciiDocFile) -> case have_a2x() of false -> {error, "a2x was not found: do you have 'asciidoc' installed?"}; {true, Path} -> Cmd = lists:flatten( io_lib:format("~ts -f manpage ~ts -D ~ts", [Path, AsciiDocFile, Cwd])), case os:cmd(Cmd) of "" -> ok; Ret -> {error, Ret} end end. warn_undocumented_modules(Docs) -> lists:foreach( fun({M, _, DocOpts, Backends, _}) -> warn_undocumented_module(M, DocOpts), lists:foreach( fun({SubM, _, SubOpts}) -> warn_undocumented_module(SubM, SubOpts) end, Backends) end, Docs). warn_undocumented_module(M, DocOpts) -> try M:mod_options(ejabberd_config:get_myname()) of Defaults -> lists:foreach( fun(OptDefault) -> Opt = case OptDefault of O when is_atom(O) -> O; {O, _} -> O end, case lists:keymember(Opt, 1, DocOpts) of false -> warn("~s: option ~s is not documented", [M, Opt]); true -> ok end end, Defaults) catch _:undef -> ok end. warn_undocumented_options(Docs) -> Opts = lists:flatmap( fun(M) -> try M:options() of Defaults -> lists:map( fun({O, _}) -> O; (O) when is_atom(O) -> O end, Defaults) catch _:undef -> [] end end, ejabberd_config:callback_modules(all)), lists:foreach( fun(Opt) -> case lists:keymember(Opt, 1, Docs) of false -> warn("option ~s is not documented", [Opt]); true -> ok end end, Opts). warn(Format, Args) -> io:format(standard_error, "Warning: " ++ Format ++ "~n", Args). strip_backend_suffix(M) -> [H|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), {list_to_atom(string:join(lists:reverse(T), "_")), list_to_atom(H)}. default_config_url() -> "<https://github.com/processone/ejabberd/blob/" ++ binary_to_list(binary:part(ejabberd_config:version(), {0,5})) ++ "/ejabberd.yml.example>". configuration_guide_url() -> "<https://docs.ejabberd.im/admin/configuration>". ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_http_api_opt.erl�������������������������������������������������������������0000644�0002322�0002322�00000000605�14154362354�020457� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_http_api_opt). -export([admin_ip_access/1]). -spec admin_ip_access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). admin_ip_access(Opts) when is_map(Opts) -> gen_mod:get_opt(admin_ip_access, Opts); admin_ip_access(Host) -> gen_mod:get_module_opt(Host, mod_http_api, admin_ip_access). ���������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_mix_opt.erl������������������������������������������������������������������0000644�0002322�0002322�00000002306�14154362354�017444� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_mix_opt). -export([access_create/1]). -export([db_type/1]). -export([host/1]). -export([hosts/1]). -export([name/1]). -spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_create(Opts) when is_map(Opts) -> gen_mod:get_opt(access_create, Opts); access_create(Host) -> gen_mod:get_module_opt(Host, mod_mix, access_create). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mix, db_type). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_mix, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_mix, hosts). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_mix, name). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_admin.erl���������������������������������������������������������������0000644�0002322�0002322�00000077574�14154362354�020057� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : ejabberd_admin.erl %%% Author : Mickael Remond <mremond@process-one.net> %%% Purpose : Administrative functions and commands %%% Created : 7 May 2006 by Mickael Remond <mremond@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_admin). -author('mickael.remond@process-one.net'). -behaviour(gen_server). -export([start_link/0, %% Server status/0, reopen_log/0, rotate_log/0, set_loglevel/1, stop_kindly/2, send_service_message_all_mucs/2, registered_vhosts/0, reload_config/0, dump_config/1, convert_to_yaml/2, %% Cluster join_cluster/1, leave_cluster/1, list_cluster/0, %% Erlang update_list/0, update/1, %% Accounts register/3, unregister/2, registered_users/1, %% Migration jabberd1.4 import_file/1, import_dir/1, %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia set_master/1, backup_mnesia/1, restore_mnesia/1, dump_mnesia/1, dump_table/2, load_mnesia/1, mnesia_info/0, mnesia_table_info/1, install_fallback_mnesia/1, dump_to_textfile/1, dump_to_textfile/2, mnesia_change_nodename/4, restore/1, % Still used by some modules clear_cache/0, gc/0, get_commands_spec/0 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include("ejabberd_commands.hrl"). -record(state, {}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> process_flag(trap_exit, true), ejabberd_commands:register_commands(get_commands_spec()), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_commands:unregister_commands(get_commands_spec()). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%% %%% ejabberd commands %%% get_commands_spec() -> [ %% The commands status, stop and restart are implemented also in ejabberd_ctl %% They are defined here so that other interfaces can use them too #ejabberd_commands{name = status, tags = [server], desc = "Get status of the ejabberd server", module = ?MODULE, function = status, result_desc = "Result tuple", result_example = {ok, <<"The node ejabberd@localhost is started with status: started" "ejabberd X.X is running in that node">>}, args = [], result = {res, restuple}}, #ejabberd_commands{name = stop, tags = [server], desc = "Stop ejabberd gracefully", module = init, function = stop, args = [], result = {res, rescode}}, #ejabberd_commands{name = restart, tags = [server], desc = "Restart ejabberd gracefully", module = init, function = restart, args = [], result = {res, rescode}}, #ejabberd_commands{name = reopen_log, tags = [logs], desc = "Reopen the log files after being renamed", longdesc = "This can be useful when an external tool is " "used for log rotation. See " "https://docs.ejabberd.im/admin/guide/troubleshooting/#log-files", policy = admin, module = ?MODULE, function = reopen_log, args = [], result = {res, rescode}}, #ejabberd_commands{name = rotate_log, tags = [logs], desc = "Rotate the log files", module = ?MODULE, function = rotate_log, args = [], result = {res, rescode}}, #ejabberd_commands{name = stop_kindly, tags = [server], desc = "Inform users and rooms, wait, and stop the server", longdesc = "Provide the delay in seconds, and the " "announcement quoted, for example: \n" "ejabberdctl stop_kindly 60 " "\\\"The server will stop in one minute.\\\"", module = ?MODULE, function = stop_kindly, args_desc = ["Seconds to wait", "Announcement to send, with quotes"], args_example = [60, <<"Server will stop now.">>], args = [{delay, integer}, {announcement, string}], result = {res, rescode}}, #ejabberd_commands{name = get_loglevel, tags = [logs], desc = "Get the current loglevel", module = ejabberd_logger, function = get, result_desc = "Tuple with the log level number, its keyword and description", result_example = warning, args = [], result = {levelatom, atom}}, #ejabberd_commands{name = set_loglevel, tags = [logs], desc = "Set the loglevel", module = ?MODULE, function = set_loglevel, args_desc = ["Desired logging level: none | emergency | alert | critical " "| error | warning | notice | info | debug"], args_example = ["debug"], args = [{loglevel, string}], result = {res, rescode}}, #ejabberd_commands{name = update_list, tags = [server], desc = "List modified modules that can be updated", module = ?MODULE, function = update_list, args = [], result_example = ["mod_configure", "mod_vcard"], result = {modules, {list, {module, string}}}}, #ejabberd_commands{name = update, tags = [server], desc = "Update the given module, or use the keyword: all", module = ?MODULE, function = update, args_example = ["mod_vcard"], args = [{module, string}], result = {res, restuple}}, #ejabberd_commands{name = register, tags = [accounts], desc = "Register a user", policy = admin, module = ?MODULE, function = register, args_desc = ["Username", "Local vhost served by ejabberd", "Password"], args_example = [<<"bob">>, <<"example.com">>, <<"SomEPass44">>], args = [{user, binary}, {host, binary}, {password, binary}], result = {res, restuple}}, #ejabberd_commands{name = unregister, tags = [accounts], desc = "Unregister a user", policy = admin, module = ?MODULE, function = unregister, args_desc = ["Username", "Local vhost served by ejabberd"], args_example = [<<"bob">>, <<"example.com">>], args = [{user, binary}, {host, binary}], result = {res, restuple}}, #ejabberd_commands{name = registered_users, tags = [accounts], desc = "List all registered users in HOST", module = ?MODULE, function = registered_users, args_desc = ["Local vhost"], args_example = [<<"example.com">>], result_desc = "List of registered accounts usernames", result_example = [<<"user1">>, <<"user2">>], args = [{host, binary}], result = {users, {list, {username, string}}}}, #ejabberd_commands{name = registered_vhosts, tags = [server], desc = "List all registered vhosts in SERVER", module = ?MODULE, function = registered_vhosts, result_desc = "List of available vhosts", result_example = [<<"example.com">>, <<"anon.example.com">>], args = [], result = {vhosts, {list, {vhost, string}}}}, #ejabberd_commands{name = reload_config, tags = [config], desc = "Reload config file in memory", module = ?MODULE, function = reload_config, args = [], result = {res, rescode}}, #ejabberd_commands{name = join_cluster, tags = [cluster], desc = "Join this node into the cluster handled by Node", longdesc = "This command works only with ejabberdctl, " "not mod_http_api or other code that runs inside the " "same ejabberd node that will be joined.", module = ?MODULE, function = join_cluster, args_desc = ["Nodename of the node to join"], args_example = [<<"ejabberd1@machine7">>], args = [{node, binary}], result = {res, rescode}}, #ejabberd_commands{name = leave_cluster, tags = [cluster], desc = "Remove and shutdown Node from the running cluster", longdesc = "This command can be run from any running " "node of the cluster, even the node to be removed. " "In the removed node, this command works only when " "using ejabberdctl, not mod_http_api or other code that " "runs inside the same ejabberd node that will leave.", module = ?MODULE, function = leave_cluster, args_desc = ["Nodename of the node to kick from the cluster"], args_example = [<<"ejabberd1@machine8">>], args = [{node, binary}], result = {res, rescode}}, #ejabberd_commands{name = list_cluster, tags = [cluster], desc = "List nodes that are part of the cluster handled by Node", module = ?MODULE, function = list_cluster, result_example = [ejabberd1@machine7, ejabberd1@machine8], args = [], result = {nodes, {list, {node, atom}}}}, #ejabberd_commands{name = import_file, tags = [mnesia], desc = "Import user data from jabberd14 spool file", module = ?MODULE, function = import_file, args_desc = ["Full path to the jabberd14 spool file"], args_example = ["/var/lib/ejabberd/jabberd14.spool"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = import_dir, tags = [mnesia], desc = "Import users data from jabberd14 spool dir", module = ?MODULE, function = import_dir, args_desc = ["Full path to the jabberd14 spool directory"], args_example = ["/var/lib/ejabberd/jabberd14/"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", module = ejabberd_piefxis, function = import_file, args_desc = ["Full path to the PIEFXIS file"], args_example = ["/var/lib/ejabberd/example.com.xml"], args = [{file, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis, tags = [mnesia], desc = "Export data of all users in the server to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_server, args_desc = ["Full path to a directory"], args_example = ["/var/lib/ejabberd/"], args = [{dir, binary}], result = {res, rescode}}, #ejabberd_commands{name = export_piefxis_host, tags = [mnesia], desc = "Export data of users in a host to PIEFXIS files (XEP-0227)", module = ejabberd_piefxis, function = export_host, args_desc = ["Full path to a directory", "Vhost to export"], args_example = ["/var/lib/ejabberd/", "example.com"], args = [{dir, binary}, {host, binary}], result = {res, rescode}}, #ejabberd_commands{name = delete_mnesia, tags = [mnesia], desc = "Delete elements in Mnesia database for a given vhost", module = ejd2sql, function = delete, args_desc = ["Vhost which content will be deleted in Mnesia database"], args_example = ["example.com"], args = [{host, string}], result = {res, rescode}}, #ejabberd_commands{name = convert_to_scram, tags = [sql], desc = "Convert the passwords of users to SCRAM", module = ejabberd_auth, function = convert_to_scram, args_desc = ["Vhost which users' passwords will be scrammed"], args_example = ["example.com"], args = [{host, binary}], result = {res, rescode}}, #ejabberd_commands{name = import_prosody, tags = [mnesia, sql], desc = "Import data from Prosody", longdesc = "Note: this requires ejabberd compiled with --enable-lua " "and include the optional 'luerl' library.", module = prosody2ejabberd, function = from_dir, args_desc = ["Full path to the Prosody data directory"], args_example = ["/var/lib/prosody/datadump/"], args = [{dir, string}], result = {res, rescode}}, #ejabberd_commands{name = convert_to_yaml, tags = [config], desc = "Convert the input file from Erlang to YAML format", module = ?MODULE, function = convert_to_yaml, args_desc = ["Full path to the original configuration file", "And full path to final file"], args_example = ["/etc/ejabberd/ejabberd.cfg", "/etc/ejabberd/ejabberd.yml"], args = [{in, string}, {out, string}], result = {res, rescode}}, #ejabberd_commands{name = dump_config, tags = [config], desc = "Dump configuration in YAML format as seen by ejabberd", module = ?MODULE, function = dump_config, args_desc = ["Full path to output file"], args_example = ["/tmp/ejabberd.yml"], args = [{out, string}], result = {res, rescode}}, #ejabberd_commands{name = delete_expired_messages, tags = [purge], desc = "Delete expired offline messages from database", module = ?MODULE, function = delete_expired_messages, args = [], result = {res, rescode}}, #ejabberd_commands{name = delete_old_messages, tags = [purge], desc = "Delete offline messages older than DAYS", module = ?MODULE, function = delete_old_messages, args_desc = ["Number of days"], args_example = [31], args = [{days, integer}], result = {res, rescode}}, #ejabberd_commands{name = export2sql, tags = [mnesia], desc = "Export virtual host information from Mnesia tables to SQL file", longdesc = "Configure the modules to use SQL, then call this command. " "After correctly exported the database of a vhost, " "you may want to delete from mnesia with " "the http://./#delete-mnesia[delete_mnesia] command.", module = ejd2sql, function = export, args_desc = ["Vhost", "Full path to the destination SQL file"], args_example = ["example.com", "/var/lib/ejabberd/example.com.sql"], args = [{host, string}, {file, string}], result = {res, rescode}}, #ejabberd_commands{name = set_master, tags = [cluster], desc = "Set master node of the clustered Mnesia tables", longdesc = "If you provide as nodename \"self\", this " "node will be set as its own master.", module = ?MODULE, function = set_master, args_desc = ["Name of the erlang node that will be considered master of this node"], args_example = ["ejabberd@machine7"], args = [{nodename, string}], result = {res, restuple}}, #ejabberd_commands{name = mnesia_change_nodename, tags = [mnesia], desc = "Change the erlang node name in a backup file", module = ?MODULE, function = mnesia_change_nodename, args_desc = ["Name of the old erlang node", "Name of the new node", "Path to old backup file", "Path to the new backup file"], args_example = ["ejabberd@machine1", "ejabberd@machine2", "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], args = [{oldnodename, string}, {newnodename, string}, {oldbackup, string}, {newbackup, string}], result = {res, restuple}}, #ejabberd_commands{name = backup, tags = [mnesia], desc = "Backup the Mnesia database to a binary file", module = ?MODULE, function = backup_mnesia, args_desc = ["Full path for the destination backup file"], args_example = ["/var/lib/ejabberd/database.backup"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = restore, tags = [mnesia], desc = "Restore the Mnesia database from a binary backup file", longdesc = "This restores immediately from a " "binary backup file the internal Mnesia " "database. This will consume a lot of memory if " "you have a large database, you may prefer " "'install_fallback'.", module = ?MODULE, function = restore_mnesia, args_desc = ["Full path to the backup file"], args_example = ["/var/lib/ejabberd/database.backup"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = dump, tags = [mnesia], desc = "Dump the Mnesia database to a text file", module = ?MODULE, function = dump_mnesia, args_desc = ["Full path for the text file"], args_example = ["/var/lib/ejabberd/database.txt"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = dump_table, tags = [mnesia], desc = "Dump a Mnesia table to a text file", module = ?MODULE, function = dump_table, args_desc = ["Full path for the text file", "Table name"], args_example = ["/var/lib/ejabberd/table-muc-registered.txt", "muc_registered"], args = [{file, string}, {table, string}], result = {res, restuple}}, #ejabberd_commands{name = load, tags = [mnesia], desc = "Restore Mnesia database from a text dump file", longdesc = "Restore immediately. This is not " "recommended for big databases, as it will " "consume much time, memory and processor. In " "that case it's preferable to use 'backup' and " "'install_fallback'.", module = ?MODULE, function = load_mnesia, args_desc = ["Full path to the text file"], args_example = ["/var/lib/ejabberd/database.txt"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = mnesia_info, tags = [mnesia], desc = "Dump info on global Mnesia state", module = ?MODULE, function = mnesia_info, args = [], result = {res, string}}, #ejabberd_commands{name = mnesia_table_info, tags = [mnesia], desc = "Dump info on Mnesia table state", module = ?MODULE, function = mnesia_table_info, args_desc = ["Mnesia table name"], args_example = ["roster"], args = [{table, string}], result = {res, string}}, #ejabberd_commands{name = install_fallback, tags = [mnesia], desc = "Install Mnesia database from a binary backup file", longdesc = "The binary backup file is " "installed as fallback: it will be used to " "restore the database at the next ejabberd " "start. This means that, after running this " "command, you have to restart ejabberd. This " "command requires less memory than 'restore'.", module = ?MODULE, function = install_fallback_mnesia, args_desc = ["Full path to the fallback file"], args_example = ["/var/lib/ejabberd/database.fallback"], args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = clear_cache, tags = [server], desc = "Clear database cache on all nodes", module = ?MODULE, function = clear_cache, args = [], result = {res, rescode}}, #ejabberd_commands{name = gc, tags = [server], desc = "Force full garbage collection", note = "added in 20.01", module = ?MODULE, function = gc, args = [], result = {res, rescode}}, #ejabberd_commands{name = man, tags = [documentation], desc = "Generate Unix manpage for current ejabberd version", note = "added in 20.01", module = ejabberd_doc, function = man, args = [], result = {res, restuple}} ]. %%% %%% Server management %%% status() -> {InternalStatus, ProvidedStatus} = init:get_status(), String1 = io_lib:format("The node ~p is ~p. Status: ~p", [node(), InternalStatus, ProvidedStatus]), {Is_running, String2} = case lists:keysearch(ejabberd, 1, application:which_applications()) of false -> {ejabberd_not_running, "ejabberd is not running in that node."}; {value, {_, _, Version}} -> {ok, io_lib:format("ejabberd ~s is running in that node", [Version])} end, {Is_running, String1 ++ String2}. reopen_log() -> ejabberd_hooks:run(reopen_log_hook, []). rotate_log() -> ejabberd_hooks:run(rotate_log_hook, []). set_loglevel(LogLevel) -> try binary_to_existing_atom(iolist_to_binary(LogLevel), latin1) of Level -> case lists:member(Level, ejabberd_logger:loglevels()) of true -> ejabberd_logger:set(Level); false -> {error, "Invalid log level"} end catch _:_ -> {error, "Invalid log level"} end. %%% %%% Stop Kindly %%% stop_kindly(DelaySeconds, AnnouncementTextString) -> Subject = (str:format("Server stop in ~p seconds!", [DelaySeconds])), WaitingDesc = (str:format("Waiting ~p seconds", [DelaySeconds])), AnnouncementText = list_to_binary(AnnouncementTextString), Steps = [ {"Stopping ejabberd port listeners", ejabberd_listener, stop_listeners, []}, {"Sending announcement to connected users", mod_announce, send_announcement_to_all, [ejabberd_config:get_myname(), Subject, AnnouncementText]}, {"Sending service message to MUC rooms", ejabberd_admin, send_service_message_all_mucs, [Subject, AnnouncementText]}, {WaitingDesc, timer, sleep, [DelaySeconds * 1000]}, {"Stopping ejabberd", application, stop, [ejabberd]}, {"Stopping Mnesia", mnesia, stop, []}, {"Stopping Erlang node", init, stop, []} ], NumberLast = length(Steps), TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}), lists:foldl( fun({Desc, Mod, Func, Args}, NumberThis) -> SecondsDiff = calendar:datetime_to_gregorian_seconds({date(), time()}) - TimestampStart, io:format("[~p/~p ~ps] ~ts... ", [NumberThis, NumberLast, SecondsDiff, Desc]), Result = (catch apply(Mod, Func, Args)), io:format("~p~n", [Result]), NumberThis+1 end, 1, Steps), ok. send_service_message_all_mucs(Subject, AnnouncementText) -> Message = str:format("~s~n~s", [Subject, AnnouncementText]), lists:foreach( fun(ServerHost) -> MUCHosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), lists:foreach( fun(MUCHost) -> mod_muc:broadcast_service_message(ServerHost, MUCHost, Message) end, MUCHosts) end, ejabberd_option:hosts()). %%% %%% ejabberd_update %%% update_list() -> {ok, _Dir, UpdatedBeams, _Script, _LowLevelScript, _Check} = ejabberd_update:update_info(), [atom_to_list(Beam) || Beam <- UpdatedBeams]. update("all") -> [update_module(ModStr) || ModStr <- update_list()], {ok, []}; update(ModStr) -> update_module(ModStr). update_module(ModuleNameBin) when is_binary(ModuleNameBin) -> update_module(binary_to_list(ModuleNameBin)); update_module(ModuleNameString) -> ModuleName = list_to_atom(ModuleNameString), case ejabberd_update:update([ModuleName]) of {ok, _Res} -> {ok, []}; {error, Reason} -> {error, Reason} end. %%% %%% Account management %%% register(User, Host, Password) -> case is_my_host(Host) of true -> case ejabberd_auth:try_register(User, Host, Password) of ok -> {ok, io_lib:format("User ~s@~s successfully registered", [User, Host])}; {error, exists} -> Msg = io_lib:format("User ~s@~s already registered", [User, Host]), {error, conflict, 10090, Msg}; {error, Reason} -> String = io_lib:format("Can't register user ~s@~s at node ~p: ~s", [User, Host, node(), mod_register:format_error(Reason)]), {error, cannot_register, 10001, String} end; false -> {error, cannot_register, 10001, "Unknown virtual host"} end. unregister(User, Host) -> case is_my_host(Host) of true -> ejabberd_auth:remove_user(User, Host), {ok, ""}; false -> {error, "Unknown virtual host"} end. registered_users(Host) -> case is_my_host(Host) of true -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort(Users), lists:map(fun({U, _S}) -> U end, SUsers); false -> {error, "Unknown virtual host"} end. registered_vhosts() -> ejabberd_option:hosts(). reload_config() -> case ejabberd_config:reload() of ok -> ok; Err -> Reason = ejabberd_config:format_error(Err), {error, Reason} end. dump_config(Path) -> case ejabberd_config:dump(Path) of ok -> ok; Err -> Reason = ejabberd_config:format_error(Err), {error, Reason} end. convert_to_yaml(In, Out) -> case ejabberd_config:convert_to_yaml(In, Out) of ok -> {ok, ""}; Err -> Reason = ejabberd_config:format_error(Err), {error, Reason} end. %%% %%% Cluster management %%% join_cluster(NodeBin) -> ejabberd_cluster:join(list_to_atom(binary_to_list(NodeBin))). leave_cluster(NodeBin) -> ejabberd_cluster:leave(list_to_atom(binary_to_list(NodeBin))). list_cluster() -> ejabberd_cluster:get_nodes(). %%% %%% Migration management %%% import_file(Path) -> case jd2ejd:import_file(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't import jabberd14 spool file ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_import_file, String} end. import_dir(Path) -> case jd2ejd:import_dir(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't import jabberd14 spool dir ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_import_dir, String} end. %%% %%% Purge DB %%% delete_expired_messages() -> lists:foreach( fun(Host) -> {atomic, ok} = mod_offline:remove_expired_messages(Host) end, ejabberd_option:hosts()). delete_old_messages(Days) -> lists:foreach( fun(Host) -> {atomic, _} = mod_offline:remove_old_messages(Days, Host) end, ejabberd_option:hosts()). %%% %%% Mnesia management %%% set_master("self") -> set_master(node()); set_master(NodeString) when is_list(NodeString) -> set_master(list_to_atom(NodeString)); set_master(Node) when is_atom(Node) -> case mnesia:set_master_nodes([Node]) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't set master node ~p at node ~p:~n~p", [Node, node(), Reason]), {error, String} end. backup_mnesia(Path) -> case mnesia:backup(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't store backup in ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_backup, String} end. restore_mnesia(Path) -> case ejabberd_admin:restore(Path) of {atomic, _} -> {ok, ""}; {aborted,{no_exists,Table}} -> String = io_lib:format("Can't restore backup from ~p at node ~p: Table ~p does not exist.", [filename:absname(Path), node(), Table]), {table_not_exists, String}; {aborted,enoent} -> String = io_lib:format("Can't restore backup from ~p at node ~p: File not found.", [filename:absname(Path), node()]), {file_not_found, String} end. %% Mnesia database restore %% This function is called from ejabberd_ctl, ejabberd_web_admin and %% mod_configure/adhoc restore(Path) -> mnesia:restore(Path, [{keep_tables,keep_tables()}, {default_op, skip_tables}]). %% This function return a list of tables that should be kept from a previous %% version backup. %% Obsolete tables or tables created by module who are no longer used are not %% restored and are ignored. keep_tables() -> lists:flatten([acl, passwd, config, keep_modules_tables()]). %% Returns the list of modules tables in use, according to the list of actually %% loaded modules keep_modules_tables() -> lists:map(fun(Module) -> module_tables(Module) end, gen_mod:loaded_modules(ejabberd_config:get_myname())). %% TODO: This mapping should probably be moved to a callback function in each %% module. %% Mapping between modules and their tables module_tables(mod_announce) -> [motd, motd_users]; module_tables(mod_last) -> [last_activity]; module_tables(mod_muc) -> [muc_room, muc_registered]; module_tables(mod_offline) -> [offline_msg]; module_tables(mod_privacy) -> [privacy]; module_tables(mod_private) -> [private_storage]; module_tables(mod_pubsub) -> [pubsub_node]; module_tables(mod_roster) -> [roster]; module_tables(mod_shared_roster) -> [sr_group, sr_user]; module_tables(mod_vcard) -> [vcard, vcard_search]; module_tables(_Other) -> []. get_local_tables() -> Tabs1 = lists:delete(schema, mnesia:system_info(local_tables)), Tabs = lists:filter( fun(T) -> case mnesia:table_info(T, storage_type) of disc_copies -> true; disc_only_copies -> true; _ -> false end end, Tabs1), Tabs. dump_mnesia(Path) -> Tabs = get_local_tables(), dump_tables(Path, Tabs). dump_table(Path, STable) -> Table = list_to_atom(STable), dump_tables(Path, [Table]). dump_tables(Path, Tables) -> case dump_to_textfile(Path, Tables) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't store dump in ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_dump, String} end. dump_to_textfile(File) -> Tabs = get_local_tables(), dump_to_textfile(File, Tabs). dump_to_textfile(File, Tabs) -> dump_to_textfile(mnesia:system_info(is_running), Tabs, file:open(File, [write])). dump_to_textfile(yes, Tabs, {ok, F}) -> Defs = lists:map( fun(T) -> {T, [{record_name, mnesia:table_info(T, record_name)}, {attributes, mnesia:table_info(T, attributes)}]} end, Tabs), io:format(F, "~p.~n", [{tables, Defs}]), lists:foreach(fun(T) -> dump_tab(F, T) end, Tabs), file:close(F); dump_to_textfile(_, _, {ok, F}) -> file:close(F), {error, mnesia_not_running}; dump_to_textfile(_, _, {error, Reason}) -> {error, Reason}. dump_tab(F, T) -> W = mnesia:table_info(T, wild_pattern), {atomic,All} = mnesia:transaction( fun() -> mnesia:match_object(T, W, read) end), lists:foreach( fun(Term) -> io:format(F,"~p.~n", [setelement(1, Term, T)]) end, All). load_mnesia(Path) -> case mnesia:load_textfile(Path) of {atomic, ok} -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't load dump in ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_load, String} end. mnesia_info() -> lists:flatten(io_lib:format("~p", [mnesia:system_info(all)])). mnesia_table_info(Table) -> ATable = list_to_atom(Table), lists:flatten(io_lib:format("~p", [mnesia:table_info(ATable, all)])). install_fallback_mnesia(Path) -> case mnesia:install_fallback(Path) of ok -> {ok, ""}; {error, Reason} -> String = io_lib:format("Can't install fallback from ~p at node ~p: ~p", [filename:absname(Path), node(), Reason]), {cannot_fallback, String} end. mnesia_change_nodename(FromString, ToString, Source, Target) -> From = list_to_atom(FromString), To = list_to_atom(ToString), Switch = fun (Node) when Node == From -> io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), To; (Node) when Node == To -> %% throw({error, already_exists}); io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), Node; (Node) -> io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), Node end, Convert = fun ({schema, db_nodes, Nodes}, Acc) -> io:format(" +++ db_nodes ~p~n", [Nodes]), {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; ({schema, version, Version}, Acc) -> io:format(" +++ version: ~p~n", [Version]), {[{schema, version, Version}], Acc}; ({schema, cookie, Cookie}, Acc) -> io:format(" +++ cookie: ~p~n", [Cookie]), {[{schema, cookie, Cookie}], Acc}; ({schema, Tab, CreateList}, Acc) -> io:format("~n * Checking table: '~p'~n", [Tab]), Keys = [ram_copies, disc_copies, disc_only_copies], OptSwitch = fun({Key, Val}) -> case lists:member(Key, Keys) of true -> io:format(" + Checking key: '~p'~n", [Key]), {Key, lists:map(Switch, Val)}; false-> {Key, Val} end end, Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, Res; (Other, Acc) -> {[Other], Acc} end, mnesia:traverse_backup(Source, Target, Convert, switched). clear_cache() -> Nodes = ejabberd_cluster:get_nodes(), lists:foreach(fun(T) -> ets_cache:clear(T, Nodes) end, ets_cache:all()). gc() -> lists:foreach(fun erlang:garbage_collect/1, processes()). -spec is_my_host(binary()) -> boolean(). is_my_host(Host) -> try ejabberd_router:is_my_host(Host) catch _:{invalid_domain, _} -> false end. ������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_vcard_ldap.erl���������������������������������������������������������������0000644�0002322�0002322�00000050704�14154362354�020071� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : mod_vcard_ldap.erl %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 29 Jul 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_vcard_ldap). -behaviour(gen_server). -behaviour(mod_vcard). %% API -export([start_link/2]). -export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, remove_user/2, import/3, search_fields/1, search_reported/1, mod_opt_type/1, mod_options/1, mod_doc/0]). -export([is_search_supported/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include("eldap.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(PROCNAME, ejabberd_mod_vcard_ldap). -record(state, {serverhost = <<"">> :: binary(), myhosts = [] :: [binary()], eldap_id = <<"">> :: binary(), search = false :: boolean(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), tls_options = [] :: list(), dn = <<"">> :: binary(), base = <<"">> :: binary(), password = <<"">> :: binary(), uids = [] :: [{binary(), binary()}], vcard_map = [] :: [{binary(), [{binary(), [binary()]}]}], vcard_map_attrs = [] :: [binary()], user_filter = <<"">> :: binary(), search_filter :: eldap:filter(), search_fields = [] :: [{binary(), binary()}], search_reported = [] :: [{binary(), binary()}], search_reported_attrs = [] :: [binary()], deref_aliases = never :: never | searching | finding | always, matches = 0 :: non_neg_integer()}). %%%=================================================================== %%% API %%%=================================================================== start_link(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). init(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:terminate_child(ejabberd_backend_sup, Proc), supervisor:delete_child(ejabberd_backend_sup, Proc), ok. is_search_supported(_LServer) -> true. get_vcard(LUser, LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), VCardMap = State#state.vcard_map, case find_ldap_user(LUser, State) of #eldap_entry{attributes = Attributes} -> VCard = ldap_attributes_to_vcard(Attributes, VCardMap, {LUser, LServer}), {ok, [xmpp:encode(VCard)]}; _ -> {ok, []} end. set_vcard(_LUser, _LServer, _VCard, _VCardSearch) -> {atomic, not_implemented}. search_fields(LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), State#state.search_fields. search_reported(LServer) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), State#state.search_reported. search(LServer, Data, _AllowReturnAll, MaxMatch) -> {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), Base = State#state.base, SearchFilter = State#state.search_filter, Eldap_ID = State#state.eldap_id, UIDs = State#state.uids, ReportedAttrs = State#state.search_reported_attrs, Filter = eldap:'and'([SearchFilter, eldap_utils:make_filter(Data, UIDs)]), case eldap_pool:search(Eldap_ID, [{base, Base}, {filter, Filter}, {limit, MaxMatch}, {deref_aliases, State#state.deref_aliases}, {attributes, ReportedAttrs}]) of #eldap_search_result{entries = E} -> search_items(E, State); _ -> [] end. search_items(Entries, State) -> LServer = State#state.serverhost, SearchReported = State#state.search_reported, VCardMap = State#state.vcard_map, UIDs = State#state.uids, Attributes = lists:map(fun (E) -> #eldap_entry{attributes = Attrs} = E, Attrs end, Entries), lists:filtermap( fun(Attrs) -> case eldap_utils:find_ldap_attrs(UIDs, Attrs) of {U, UIDAttrFormat} -> case eldap_utils:get_user_part(U, UIDAttrFormat) of {ok, Username} -> case ejabberd_auth:user_exists(Username, LServer) of true -> RFields = lists:map( fun({_, VCardName}) -> {VCardName, map_vcard_attr(VCardName, Attrs, VCardMap, {Username, ejabberd_config:get_myname()})} end, SearchReported), J = <<Username/binary, $@, LServer/binary>>, {true, [{<<"jid">>, J} | RFields]}; _ -> false end; _ -> false end; <<"">> -> false end end, Attributes). remove_user(_User, _Server) -> {atomic, not_implemented}. import(_, _, _) -> ok. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host, Opts]) -> process_flag(trap_exit, true), State = parse_options(Host, Opts), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), {ok, State}. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== find_ldap_user(User, State) -> Base = State#state.base, RFC2254_Filter = State#state.user_filter, Eldap_ID = State#state.eldap_id, VCardAttrs = State#state.vcard_map_attrs, case eldap_filter:parse(RFC2254_Filter, [{<<"%u">>, User}]) of {ok, EldapFilter} -> case eldap_pool:search(Eldap_ID, [{base, Base}, {filter, EldapFilter}, {deref_aliases, State#state.deref_aliases}, {attributes, VCardAttrs}]) of #eldap_search_result{entries = [E | _]} -> E; _ -> false end; _ -> false end. ldap_attributes_to_vcard(Attributes, VCardMap, UD) -> Attrs = lists:map( fun({VCardName, _}) -> {VCardName, map_vcard_attr(VCardName, Attributes, VCardMap, UD)} end, VCardMap), lists:foldl(fun ldap_attribute_to_vcard/2, #vcard_temp{}, Attrs). -spec ldap_attribute_to_vcard({binary(), binary()}, vcard_temp()) -> vcard_temp(). ldap_attribute_to_vcard({Attr, Value}, V) -> Ts = V#vcard_temp.tel, Es = V#vcard_temp.email, N = case V#vcard_temp.n of undefined -> #vcard_name{}; _ -> V#vcard_temp.n end, O = case V#vcard_temp.org of undefined -> #vcard_org{}; _ -> V#vcard_temp.org end, A = case V#vcard_temp.adr of [] -> #vcard_adr{}; As -> hd(As) end, case str:to_lower(Attr) of <<"fn">> -> V#vcard_temp{fn = Value}; <<"nickname">> -> V#vcard_temp{nickname = Value}; <<"title">> -> V#vcard_temp{title = Value}; <<"bday">> -> V#vcard_temp{bday = Value}; <<"url">> -> V#vcard_temp{url = Value}; <<"desc">> -> V#vcard_temp{desc = Value}; <<"role">> -> V#vcard_temp{role = Value}; <<"tel">> -> V#vcard_temp{tel = [#vcard_tel{number = Value}|Ts]}; <<"email">> -> V#vcard_temp{email = [#vcard_email{userid = Value}|Es]}; <<"photo">> -> V#vcard_temp{photo = #vcard_photo{binval = Value, type = photo_type(Value)}}; <<"family">> -> V#vcard_temp{n = N#vcard_name{family = Value}}; <<"given">> -> V#vcard_temp{n = N#vcard_name{given = Value}}; <<"middle">> -> V#vcard_temp{n = N#vcard_name{middle = Value}}; <<"orgname">> -> V#vcard_temp{org = O#vcard_org{name = Value}}; <<"orgunit">> -> V#vcard_temp{org = O#vcard_org{units = [Value]}}; <<"locality">> -> V#vcard_temp{adr = [A#vcard_adr{locality = Value}]}; <<"street">> -> V#vcard_temp{adr = [A#vcard_adr{street = Value}]}; <<"ctry">> -> V#vcard_temp{adr = [A#vcard_adr{ctry = Value}]}; <<"region">> -> V#vcard_temp{adr = [A#vcard_adr{region = Value}]}; <<"pcode">> -> V#vcard_temp{adr = [A#vcard_adr{pcode = Value}]}; _ -> V end. -spec photo_type(binary()) -> binary(). photo_type(Value) -> Type = eimp:get_type(Value), <<"image/", (atom_to_binary(Type, latin1))/binary>>. map_vcard_attr(VCardName, Attributes, Pattern, UD) -> Res = lists:filter( fun({Name, _}) -> eldap_utils:case_insensitive_match(Name, VCardName) end, Pattern), case Res of [{_, [{Str, Attrs}|_]}] -> process_pattern(Str, UD, [eldap_utils:get_ldap_attr(X, Attributes) || X <- Attrs]); _ -> <<"">> end. process_pattern(Str, {User, Domain}, AttrValues) -> eldap_filter:do_sub(Str, [{<<"%u">>, User}, {<<"%d">>, Domain}] ++ [{<<"%s">>, V, 1} || V <- AttrValues]). default_vcard_map() -> [{<<"NICKNAME">>, [{<<"%u">>, []}]}, {<<"FN">>, [{<<"%s">>, [<<"displayName">>]}]}, {<<"FAMILY">>, [{<<"%s">>, [<<"sn">>]}]}, {<<"GIVEN">>, [{<<"%s">>, [<<"givenName">>]}]}, {<<"MIDDLE">>, [{<<"%s">>, [<<"initials">>]}]}, {<<"ORGNAME">>, [{<<"%s">>, [<<"o">>]}]}, {<<"ORGUNIT">>, [{<<"%s">>, [<<"ou">>]}]}, {<<"CTRY">>, [{<<"%s">>, [<<"c">>]}]}, {<<"LOCALITY">>, [{<<"%s">>, [<<"l">>]}]}, {<<"STREET">>, [{<<"%s">>, [<<"street">>]}]}, {<<"REGION">>, [{<<"%s">>, [<<"st">>]}]}, {<<"PCODE">>, [{<<"%s">>, [<<"postalCode">>]}]}, {<<"TITLE">>, [{<<"%s">>, [<<"title">>]}]}, {<<"URL">>, [{<<"%s">>, [<<"labeleduri">>]}]}, {<<"DESC">>, [{<<"%s">>, [<<"description">>]}]}, {<<"TEL">>, [{<<"%s">>, [<<"telephoneNumber">>]}]}, {<<"EMAIL">>, [{<<"%s">>, [<<"mail">>]}]}, {<<"BDAY">>, [{<<"%s">>, [<<"birthDay">>]}]}, {<<"ROLE">>, [{<<"%s">>, [<<"employeeType">>]}]}, {<<"PHOTO">>, [{<<"%s">>, [<<"jpegPhoto">>]}]}]. default_search_fields() -> [{?T("User"), <<"%u">>}, {?T("Full Name"), <<"displayName">>}, {?T("Given Name"), <<"givenName">>}, {?T("Middle Name"), <<"initials">>}, {?T("Family Name"), <<"sn">>}, {?T("Nickname"), <<"%u">>}, {?T("Birthday"), <<"birthDay">>}, {?T("Country"), <<"c">>}, {?T("City"), <<"l">>}, {?T("Email"), <<"mail">>}, {?T("Organization Name"), <<"o">>}, {?T("Organization Unit"), <<"ou">>}]. default_search_reported() -> [{?T("Full Name"), <<"FN">>}, {?T("Given Name"), <<"FIRST">>}, {?T("Middle Name"), <<"MIDDLE">>}, {?T("Family Name"), <<"LAST">>}, {?T("Nickname"), <<"NICK">>}, {?T("Birthday"), <<"BDAY">>}, {?T("Country"), <<"CTRY">>}, {?T("City"), <<"LOCALITY">>}, {?T("Email"), <<"EMAIL">>}, {?T("Organization Name"), <<"ORGNAME">>}, {?T("Organization Unit"), <<"ORGUNIT">>}]. parse_options(Host, Opts) -> MyHosts = gen_mod:get_opt_hosts(Opts), Search = mod_vcard_opt:search(Opts), Matches = mod_vcard_opt:matches(Opts), Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?PROCNAME)), Cfg = ?eldap_config(mod_vcard_ldap_opt, Opts), UIDsTemp = mod_vcard_ldap_opt:ldap_uids(Opts), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), UserFilter = case mod_vcard_ldap_opt:ldap_filter(Opts) of <<"">> -> SubFilter; F -> <<"(&", SubFilter/binary, F/binary, ")">> end, {ok, SearchFilter} = eldap_filter:parse(eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}])), VCardMap = mod_vcard_ldap_opt:ldap_vcard_map(Opts), SearchFields = mod_vcard_ldap_opt:ldap_search_fields(Opts), SearchReported = mod_vcard_ldap_opt:ldap_search_reported(Opts), UIDAttrs = [UAttr || {UAttr, _} <- UIDs], VCardMapAttrs = lists:usort( lists:flatten( lists:map( fun({_, Map}) -> [Attrs || {_, Attrs} <- Map] end, VCardMap) ++ UIDAttrs)), SearchReportedAttrs = lists:usort( lists:flatten( lists:map( fun ({_, N}) -> case lists:keyfind(N, 1, VCardMap) of {_, Map} -> [Attrs || {_, Attrs} <- Map]; false -> [] end end, SearchReported) ++ UIDAttrs)), #state{serverhost = Host, myhosts = MyHosts, eldap_id = Eldap_ID, search = Search, servers = Cfg#eldap_config.servers, backups = Cfg#eldap_config.backups, port = Cfg#eldap_config.port, tls_options = Cfg#eldap_config.tls_options, dn = Cfg#eldap_config.dn, password = Cfg#eldap_config.password, base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, uids = UIDs, vcard_map = VCardMap, vcard_map_attrs = VCardMapAttrs, user_filter = UserFilter, search_filter = SearchFilter, search_fields = SearchFields, search_reported = SearchReported, search_reported_attrs = SearchReportedAttrs, matches = Matches}. mod_opt_type(ldap_search_fields) -> econf:map( econf:binary(), econf:binary()); mod_opt_type(ldap_search_reported) -> econf:map( econf:binary(), econf:binary()); mod_opt_type(ldap_vcard_map) -> econf:map( econf:binary(), econf:map( econf:binary(), econf:list( econf:binary()))); mod_opt_type(ldap_backups) -> econf:list(econf:domain(), [unique]); mod_opt_type(ldap_base) -> econf:binary(); mod_opt_type(ldap_deref_aliases) -> econf:enum([never, searching, finding, always]); mod_opt_type(ldap_encrypt) -> econf:enum([tls, starttls, none]); mod_opt_type(ldap_filter) -> econf:ldap_filter(); mod_opt_type(ldap_password) -> econf:binary(); mod_opt_type(ldap_port) -> econf:port(); mod_opt_type(ldap_rootdn) -> econf:binary(); mod_opt_type(ldap_servers) -> econf:list(econf:domain(), [unique]); mod_opt_type(ldap_tls_cacertfile) -> econf:pem(); mod_opt_type(ldap_tls_certfile) -> econf:pem(); mod_opt_type(ldap_tls_depth) -> econf:non_neg_int(); mod_opt_type(ldap_tls_verify) -> econf:enum([hard, soft, false]); mod_opt_type(ldap_uids) -> econf:either( econf:list( econf:and_then( econf:binary(), fun(U) -> {U, <<"%u">>} end)), econf:map(econf:binary(), econf:binary(), [unique])). -spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | {atom(), any()}]. mod_options(Host) -> [{ldap_search_fields, default_search_fields()}, {ldap_search_reported, default_search_reported()}, {ldap_vcard_map, default_vcard_map()}, {ldap_backups, ejabberd_option:ldap_backups(Host)}, {ldap_base, ejabberd_option:ldap_base(Host)}, {ldap_uids, ejabberd_option:ldap_uids(Host)}, {ldap_deref_aliases, ejabberd_option:ldap_deref_aliases(Host)}, {ldap_encrypt, ejabberd_option:ldap_encrypt(Host)}, {ldap_password, ejabberd_option:ldap_password(Host)}, {ldap_port, ejabberd_option:ldap_port(Host)}, {ldap_rootdn, ejabberd_option:ldap_rootdn(Host)}, {ldap_servers, ejabberd_option:ldap_servers(Host)}, {ldap_filter, ejabberd_option:ldap_filter(Host)}, {ldap_tls_certfile, ejabberd_option:ldap_tls_certfile(Host)}, {ldap_tls_cacertfile, ejabberd_option:ldap_tls_cacertfile(Host)}, {ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)}, {ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)}]. mod_doc() -> #{opts => [{ldap_search_fields, #{value => "{Name: Attribute, ...}", desc => ?T("This option defines the search form and the LDAP " "attributes to search within. 'Name' is the name of a " "search form field which will be automatically " "translated by using the translation files " "(see 'msgs/*.msg' for available words). " "'Attribute' is the LDAP attribute or the pattern '%u'."), example => [{?T("The default is:"), ["User: \"%u\"", "\"Full Name\": displayName", "\"Given Name\": givenName", "\"Middle Name\": initials", "\"Family Name\": sn", "Nickname: \"%u\"", "Birthday: birthDay", "Country: c", "City: l", "Email: mail", "\"Organization Name\": o", "\"Organization Unit\": ou"] }]}}, {ldap_search_reported, #{value => "{SearchField: VcardField}, ...}", desc => ?T("This option defines which search fields should be " "reported. 'SearchField' is the name of a search form " "field which will be automatically translated by using " "the translation files (see 'msgs/*.msg' for available " "words). 'VcardField' is the vCard field name defined " "in the 'ldap_vcard_map' option."), example => [{?T("The default is:"), ["\"Full Name\": FN", "\"Given Name\": FIRST", "\"Middle Name\": MIDDLE", "\"Family Name\": LAST", "\"Nickname\": NICKNAME", "\"Birthday\": BDAY", "\"Country\": CTRY", "\"City\": LOCALITY", "\"Email\": EMAIL", "\"Organization Name\": ORGNAME", "\"Organization Unit\": ORGUNIT"] }]}}, {ldap_vcard_map, #{value => "{Name: {Pattern, LDAPattributes}, ...}", desc => ?T("With this option you can set the table that maps LDAP " "attributes to vCard fields. 'Name' is the type name of " "the vCard as defined in " "https://tools.ietf.org/html/rfc2426[RFC 2426]. " "'Pattern' is a string which contains " "pattern variables '%u', '%d' or '%s'. " "'LDAPattributes' is the list containing LDAP attributes. " "The pattern variables '%s' will be sequentially replaced " "with the values of LDAP attributes from " "'List_of_LDAP_attributes', '%u' will be replaced with " "the user part of a JID, and '%d' will be replaced with " "the domain part of a JID."), example => [{?T("The default is:"), ["NICKNAME: {\"%u\": []}", "FN: {\"%s\": [displayName]}", "LAST: {\"%s\": [sn]}", "FIRST: {\"%s\": [givenName]}", "MIDDLE: {\"%s\": [initials]}", "ORGNAME: {\"%s\": [o]}", "ORGUNIT: {\"%s\": [ou]}", "CTRY: {\"%s\": [c]}", "LOCALITY: {\"%s\": [l]}", "STREET: {\"%s\": [street]}", "REGION: {\"%s\": [st]}", "PCODE: {\"%s\": [postalCode]}", "TITLE: {\"%s\": [title]}", "URL: {\"%s\": [labeleduri]}", "DESC: {\"%s\": [description]}", "TEL: {\"%s\": [telephoneNumber]}", "EMAIL: {\"%s\": [mail]}", "BDAY: {\"%s\": [birthDay]}", "ROLE: {\"%s\": [employeeType]}", "PHOTO: {\"%s\": [jpegPhoto]}"] }]}}] ++ [{Opt, #{desc => {?T("Same as top-level _`~s`_ option, but " "applied to this module only."), [Opt]}}} || Opt <- [ldap_base, ldap_servers, ldap_uids, ldap_deref_aliases, ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, ldap_filter, ldap_tls_certfile, ldap_tls_cacertfile, ldap_tls_depth, ldap_tls_verify, ldap_backups]]}. ������������������������������������������������������������ejabberd-21.12/src/mod_vcard_sql.erl����������������������������������������������������������������0000644�0002322�0002322�00000030134�14154362354�017743� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : mod_vcard_sql.erl %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_vcard_sql). -behaviour(mod_vcard). %% API -export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, remove_user/2, search_fields/1, search_reported/1, import/3, export/1]). -export([is_search_supported/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_vcard.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. stop(_Host) -> ok. is_search_supported(_LServer) -> true. get_vcard(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(vcard)s from vcard" " where username=%(LUser)s and %(LServer)H")) of {selected, [{SVCARD}]} -> case fxml_stream:parse_element(SVCARD) of {error, _Reason} -> error; VCARD -> {ok, [VCARD]} end; {selected, []} -> {ok, []}; _ -> error end. set_vcard(LUser, LServer, VCARD, #vcard_search{user = {User, _}, fn = FN, lfn = LFN, family = Family, lfamily = LFamily, given = Given, lgiven = LGiven, middle = Middle, lmiddle = LMiddle, nickname = Nickname, lnickname = LNickname, bday = BDay, lbday = LBDay, ctry = CTRY, lctry = LCTRY, locality = Locality, llocality = LLocality, email = EMail, lemail = LEMail, orgname = OrgName, lorgname = LOrgName, orgunit = OrgUnit, lorgunit = LOrgUnit}) -> SVCARD = fxml:element_to_binary(VCARD), ejabberd_sql:sql_transaction( LServer, fun() -> ?SQL_UPSERT(LServer, "vcard", ["!username=%(LUser)s", "!server_host=%(LServer)s", "vcard=%(SVCARD)s"]), ?SQL_UPSERT(LServer, "vcard_search", ["username=%(User)s", "!lusername=%(LUser)s", "!server_host=%(LServer)s", "fn=%(FN)s", "lfn=%(LFN)s", "family=%(Family)s", "lfamily=%(LFamily)s", "given=%(Given)s", "lgiven=%(LGiven)s", "middle=%(Middle)s", "lmiddle=%(LMiddle)s", "nickname=%(Nickname)s", "lnickname=%(LNickname)s", "bday=%(BDay)s", "lbday=%(LBDay)s", "ctry=%(CTRY)s", "lctry=%(LCTRY)s", "locality=%(Locality)s", "llocality=%(LLocality)s", "email=%(EMail)s", "lemail=%(LEMail)s", "orgname=%(OrgName)s", "lorgname=%(LOrgName)s", "orgunit=%(OrgUnit)s", "lorgunit=%(LOrgUnit)s"]) end). search(LServer, Data, AllowReturnAll, MaxMatch) -> MatchSpec = make_matchspec(LServer, Data), if (MatchSpec == <<"">>) and not AllowReturnAll -> []; true -> Limit = case MaxMatch of infinity -> <<"">>; Val -> [<<" LIMIT ">>, integer_to_binary(Val)] end, case catch ejabberd_sql:sql_query( LServer, [<<"select username, fn, family, given, " "middle, nickname, bday, ctry, " "locality, email, orgname, orgunit " "from vcard_search ">>, MatchSpec, Limit, <<";">>]) of {selected, [<<"username">>, <<"fn">>, <<"family">>, <<"given">>, <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, <<"locality">>, <<"email">>, <<"orgname">>, <<"orgunit">>], Rs} when is_list(Rs) -> [row_to_item(LServer, R) || R <- Rs]; Error -> ?ERROR_MSG("~p", [Error]), [] end end. search_fields(_LServer) -> [{?T("User"), <<"user">>}, {?T("Full Name"), <<"fn">>}, {?T("Name"), <<"first">>}, {?T("Middle Name"), <<"middle">>}, {?T("Family Name"), <<"last">>}, {?T("Nickname"), <<"nick">>}, {?T("Birthday"), <<"bday">>}, {?T("Country"), <<"ctry">>}, {?T("City"), <<"locality">>}, {?T("Email"), <<"email">>}, {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. search_reported(_LServer) -> [{?T("Jabber ID"), <<"jid">>}, {?T("Full Name"), <<"fn">>}, {?T("Name"), <<"first">>}, {?T("Middle Name"), <<"middle">>}, {?T("Family Name"), <<"last">>}, {?T("Nickname"), <<"nick">>}, {?T("Birthday"), <<"bday">>}, {?T("Country"), <<"ctry">>}, {?T("City"), <<"locality">>}, {?T("Email"), <<"email">>}, {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. remove_user(LUser, LServer) -> ejabberd_sql:sql_transaction( LServer, fun() -> ejabberd_sql:sql_query_t( ?SQL("delete from vcard" " where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query_t( ?SQL("delete from vcard_search" " where lusername=%(LUser)s and %(LServer)H")) end). export(_Server) -> [{vcard, fun(Host, #vcard{us = {LUser, LServer}, vcard = VCARD}) when LServer == Host -> SVCARD = fxml:element_to_binary(VCARD), [?SQL("delete from vcard" " where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT("vcard", ["username=%(LUser)s", "server_host=%(LServer)s", "vcard=%(SVCARD)s"])]; (_Host, _R) -> [] end}, {vcard_search, fun(Host, #vcard_search{user = {User, LServer}, luser = LUser, fn = FN, lfn = LFN, family = Family, lfamily = LFamily, given = Given, lgiven = LGiven, middle = Middle, lmiddle = LMiddle, nickname = Nickname, lnickname = LNickname, bday = BDay, lbday = LBDay, ctry = CTRY, lctry = LCTRY, locality = Locality, llocality = LLocality, email = EMail, lemail = LEMail, orgname = OrgName, lorgname = LOrgName, orgunit = OrgUnit, lorgunit = LOrgUnit}) when LServer == Host -> [?SQL("delete from vcard_search" " where lusername=%(LUser)s and %(LServer)H;"), ?SQL_INSERT("vcard_search", ["username=%(User)s", "lusername=%(LUser)s", "server_host=%(LServer)s", "fn=%(FN)s", "lfn=%(LFN)s", "family=%(Family)s", "lfamily=%(LFamily)s", "given=%(Given)s", "lgiven=%(LGiven)s", "middle=%(Middle)s", "lmiddle=%(LMiddle)s", "nickname=%(Nickname)s", "lnickname=%(LNickname)s", "bday=%(BDay)s", "lbday=%(LBDay)s", "ctry=%(CTRY)s", "lctry=%(LCTRY)s", "locality=%(Locality)s", "llocality=%(LLocality)s", "email=%(EMail)s", "lemail=%(LEMail)s", "orgname=%(OrgName)s", "lorgname=%(LOrgName)s", "orgunit=%(OrgUnit)s", "lorgunit=%(LOrgUnit)s"])]; (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== make_matchspec(LServer, Data) -> filter_fields(Data, <<"">>, LServer). filter_fields([], Match, LServer) -> case ejabberd_sql:use_new_schema() of true -> SQLType = ejabberd_option:sql_type(LServer), SServer = ejabberd_sql:to_string_literal(SQLType, LServer), case Match of <<"">> -> [<<"where server_host=">>, SServer]; _ -> [<<" where server_host=">>, SServer, <<" and ">>, Match] end; false -> case Match of <<"">> -> <<"">>; _ -> [<<" where ">>, Match] end end; filter_fields([{SVar, [Val]} | Ds], Match, LServer) when is_binary(Val) and (Val /= <<"">>) -> LVal = mod_vcard:string2lower(Val), NewMatch = case SVar of <<"user">> -> make_val(LServer, Match, <<"lusername">>, LVal); <<"fn">> -> make_val(LServer, Match, <<"lfn">>, LVal); <<"last">> -> make_val(LServer, Match, <<"lfamily">>, LVal); <<"first">> -> make_val(LServer, Match, <<"lgiven">>, LVal); <<"middle">> -> make_val(LServer, Match, <<"lmiddle">>, LVal); <<"nick">> -> make_val(LServer, Match, <<"lnickname">>, LVal); <<"bday">> -> make_val(LServer, Match, <<"lbday">>, LVal); <<"ctry">> -> make_val(LServer, Match, <<"lctry">>, LVal); <<"locality">> -> make_val(LServer, Match, <<"llocality">>, LVal); <<"email">> -> make_val(LServer, Match, <<"lemail">>, LVal); <<"orgname">> -> make_val(LServer, Match, <<"lorgname">>, LVal); <<"orgunit">> -> make_val(LServer, Match, <<"lorgunit">>, LVal); _ -> Match end, filter_fields(Ds, NewMatch, LServer); filter_fields([_ | Ds], Match, LServer) -> filter_fields(Ds, Match, LServer). make_val(LServer, Match, Field, Val) -> Condition = case str:suffix(<<"*">>, Val) of true -> Val1 = str:substr(Val, 1, byte_size(Val) - 1), SVal = <<(ejabberd_sql:escape( ejabberd_sql:escape_like_arg_circumflex( Val1)))/binary, "%">>, [Field, <<" LIKE '">>, SVal, <<"' ESCAPE '^'">>]; _ -> SQLType = ejabberd_option:sql_type(LServer), SVal = ejabberd_sql:to_string_literal(SQLType, Val), [Field, <<" = ">>, SVal] end, case Match of <<"">> -> Condition; _ -> [Match, <<" and ">>, Condition] end. row_to_item(LServer, [Username, FN, Family, Given, Middle, Nickname, BDay, CTRY, Locality, EMail, OrgName, OrgUnit]) -> [{<<"jid">>, <<Username/binary, $@, LServer/binary>>}, {<<"fn">>, FN}, {<<"last">>, Family}, {<<"first">>, Given}, {<<"middle">>, Middle}, {<<"nick">>, Nickname}, {<<"bday">>, BDay}, {<<"ctry">>, CTRY}, {<<"locality">>, Locality}, {<<"email">>, EMail}, {<<"orgname">>, OrgName}, {<<"orgunit">>, OrgUnit}]. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_stream_mgmt_opt.erl����������������������������������������������������������0000644�0002322�0002322�00000004564�14154362354�021176� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_stream_mgmt_opt). -export([ack_timeout/1]). -export([cache_life_time/1]). -export([cache_size/1]). -export([max_ack_queue/1]). -export([max_resume_timeout/1]). -export([queue_type/1]). -export([resend_on_timeout/1]). -export([resume_timeout/1]). -spec ack_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). ack_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(ack_timeout, Opts); ack_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, ack_timeout). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_life_time). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, cache_size). -spec max_ack_queue(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_ack_queue(Opts) when is_map(Opts) -> gen_mod:get_opt(max_ack_queue, Opts); max_ack_queue(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, max_ack_queue). -spec max_resume_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). max_resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(max_resume_timeout, Opts); max_resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, max_resume_timeout). -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, queue_type). -spec resend_on_timeout(gen_mod:opts() | global | binary()) -> 'false' | 'if_offline' | 'true'. resend_on_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resend_on_timeout, Opts); resend_on_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, resend_on_timeout). -spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resume_timeout, Opts); resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_stream_mgmt, resume_timeout). ��������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_caps_mnesia.erl��������������������������������������������������������������0000644�0002322�0002322�00000005647�14154362354�020262� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : mod_caps_mnesia.erl %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 13 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_caps_mnesia). -behaviour(mod_caps). %% API -export([init/2, caps_read/2, caps_write/3, import/3]). -export([need_transform/1, transform/1]). -include("mod_caps.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, caps_features, [{disc_only_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, caps_features)}]). caps_read(_LServer, Node) -> case mnesia:dirty_read({caps_features, Node}) of [#caps_features{features = Features}] -> {ok, Features}; _ -> error end. caps_write(_LServer, Node, Features) -> mnesia:dirty_write(#caps_features{node_pair = Node, features = Features}). import(_LServer, NodePair, [I]) when is_integer(I) -> mnesia:dirty_write( #caps_features{node_pair = NodePair, features = I}); import(_LServer, NodePair, Features) -> mnesia:dirty_write( #caps_features{node_pair = NodePair, features = Features}). need_transform(#caps_features{node_pair = {N, P}, features = Fs}) -> case is_list(N) orelse is_list(P) orelse (is_list(Fs) andalso lists:any(fun is_list/1, Fs)) of true -> ?INFO_MSG("Mnesia table 'caps_features' will be " "converted to binary", []), true; false -> false end. transform(#caps_features{node_pair = {N, P}, features = Fs} = R) -> NewFs = if is_integer(Fs) -> Fs; true -> [iolist_to_binary(F) || F <- Fs] end, R#caps_features{node_pair = {iolist_to_binary(N), iolist_to_binary(P)}, features = NewFs}. %%%=================================================================== %%% Internal functions %%%=================================================================== �����������������������������������������������������������������������������������������ejabberd-21.12/src/mod_http_upload.erl��������������������������������������������������������������0000644�0002322�0002322�00000131411�14154362354�020310� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : mod_http_upload.erl %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Purpose : HTTP File Upload (XEP-0363) %%% Created : 20 Aug 2015 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% %%% ejabberd, Copyright (C) 2015-2021 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(mod_http_upload). -author('holger@zedat.fu-berlin.de'). -behaviour(gen_server). -behaviour(gen_mod). -protocol({xep, 363, '0.1'}). -define(SERVICE_REQUEST_TIMEOUT, 5000). % 5 seconds. -define(CALL_TIMEOUT, 60000). % 1 minute. -define(SLOT_TIMEOUT, timer:hours(5)). -define(DEFAULT_CONTENT_TYPE, <<"application/octet-stream">>). -define(CONTENT_TYPES, [{<<".avi">>, <<"video/avi">>}, {<<".bmp">>, <<"image/bmp">>}, {<<".bz2">>, <<"application/x-bzip2">>}, {<<".gif">>, <<"image/gif">>}, {<<".gz">>, <<"application/x-gzip">>}, {<<".jpeg">>, <<"image/jpeg">>}, {<<".jpg">>, <<"image/jpeg">>}, {<<".m4a">>, <<"audio/mp4">>}, {<<".mp3">>, <<"audio/mpeg">>}, {<<".mp4">>, <<"video/mp4">>}, {<<".mpeg">>, <<"video/mpeg">>}, {<<".mpg">>, <<"video/mpeg">>}, {<<".ogg">>, <<"application/ogg">>}, {<<".pdf">>, <<"application/pdf">>}, {<<".png">>, <<"image/png">>}, {<<".rtf">>, <<"application/rtf">>}, {<<".svg">>, <<"image/svg+xml">>}, {<<".tiff">>, <<"image/tiff">>}, {<<".txt">>, <<"text/plain">>}, {<<".wav">>, <<"audio/wav">>}, {<<".webp">>, <<"image/webp">>}, {<<".xz">>, <<"application/x-xz">>}, {<<".zip">>, <<"application/zip">>}]). %% gen_mod/supervisor callbacks. -export([start/2, stop/1, reload/3, depends/2, mod_doc/0, mod_opt_type/1, mod_options/1]). %% gen_server callbacks. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% ejabberd_http callback. -export([process/2]). %% ejabberd_hooks callback. -export([remove_user/2]). %% Utility functions. -export([get_proc_name/2, expand_home/1, expand_host/2]). -include("ejabberd_http.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -record(state, {server_host = <<>> :: binary(), hosts = [] :: [binary()], name = <<>> :: binary(), access = none :: atom(), max_size = infinity :: pos_integer() | infinity, secret_length = 40 :: pos_integer(), jid_in_url = sha1 :: sha1 | node, file_mode :: integer() | undefined, dir_mode :: integer() | undefined, docroot = <<>> :: binary(), put_url = <<>> :: binary(), get_url = <<>> :: binary(), service_url :: binary() | undefined, thumbnail = false :: boolean(), custom_headers = [] :: [{binary(), binary()}], slots = #{} :: slots(), external_secret = <<>> :: binary()}). -record(media_info, {path :: binary(), type :: atom(), height :: integer(), width :: integer()}). -type state() :: #state{}. -type slot() :: [binary(), ...]. -type slots() :: #{slot() => {pos_integer(), reference()}}. -type media_info() :: #media_info{}. %%-------------------------------------------------------------------- %% gen_mod/supervisor callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, term()}. start(ServerHost, Opts) -> Proc = get_proc_name(ServerHost, ?MODULE), case gen_mod:start_child(?MODULE, ServerHost, Opts, Proc) of {ok, _} = Ret -> Ret; {error, {already_started, _}} = Err -> ?ERROR_MSG("Multiple virtual hosts can't use a single 'put_url' " "without the @HOST@ keyword", []), Err; Err -> Err end. -spec stop(binary()) -> ok | {error, any()}. stop(ServerHost) -> Proc = get_proc_name(ServerHost, ?MODULE), gen_mod:stop_child(Proc). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok | {ok, pid()} | {error, term()}. reload(ServerHost, NewOpts, OldOpts) -> NewURL = mod_http_upload_opt:put_url(NewOpts), OldURL = mod_http_upload_opt:put_url(OldOpts), OldProc = get_proc_name(ServerHost, ?MODULE, OldURL), NewProc = get_proc_name(ServerHost, ?MODULE, NewURL), if OldProc /= NewProc -> gen_mod:stop_child(OldProc), start(ServerHost, NewOpts); true -> gen_server:cast(NewProc, {reload, NewOpts, OldOpts}) end. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(name) -> econf:binary(); mod_opt_type(access) -> econf:acl(); mod_opt_type(max_size) -> econf:pos_int(infinity); mod_opt_type(secret_length) -> econf:int(8, 1000); mod_opt_type(jid_in_url) -> econf:enum([sha1, node]); mod_opt_type(file_mode) -> econf:octal(); mod_opt_type(dir_mode) -> econf:octal(); mod_opt_type(docroot) -> econf:binary(); mod_opt_type(put_url) -> econf:url(); mod_opt_type(get_url) -> econf:url(); mod_opt_type(service_url) -> econf:url(); mod_opt_type(custom_headers) -> econf:map(econf:binary(), econf:binary()); mod_opt_type(rm_on_unregister) -> econf:bool(); mod_opt_type(thumbnail) -> econf:and_then( econf:bool(), fun(true) -> case eimp:supported_formats() of [] -> econf:fail(eimp_error); [_|_] -> true end; (false) -> false end); mod_opt_type(external_secret) -> econf:binary(); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(vcard) -> econf:vcard_temp(). -spec mod_options(binary()) -> [{thumbnail, boolean()} | {atom(), any()}]. mod_options(Host) -> [{host, <<"upload.", Host/binary>>}, {hosts, []}, {name, ?T("HTTP File Upload")}, {vcard, undefined}, {access, local}, {max_size, 104857600}, {secret_length, 40}, {jid_in_url, sha1}, {file_mode, undefined}, {dir_mode, undefined}, {docroot, <<"@HOME@/upload">>}, {put_url, <<"https://", Host/binary, ":5443/upload">>}, {get_url, undefined}, {service_url, undefined}, {external_secret, <<"">>}, {custom_headers, []}, {rm_on_unregister, true}, {thumbnail, false}]. mod_doc() -> #{desc => [?T("This module allows for requesting permissions to " "upload a file via HTTP as described in " "https://xmpp.org/extensions/xep-0363.html" "[XEP-0363: HTTP File Upload]. If the request is accepted, " "the client receives a URL for uploading the file and " "another URL from which that file can later be downloaded."), "", ?T("In order to use this module, it must be enabled " "in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers].")], opts => [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"upload.\". " "The keyword '@HOST@' is replaced with the real virtual host name.")}}, {name, #{value => ?T("Name"), desc => ?T("A name of the service in the Service Discovery. " "This will only be displayed by special XMPP clients. " "The default value is \"HTTP File Upload\".")}}, {access, #{value => ?T("AccessName"), desc => ?T("This option defines the access rule to limit who is " "permitted to use the HTTP upload service. " "The default value is 'local'. If no access rule of " "that name exists, no user will be allowed to use the service.")}}, {max_size, #{value => ?T("Size"), desc => ?T("This option limits the acceptable file size. " "Either a number of bytes (larger than zero) or " "'infinity' must be specified. " "The default value is '104857600'.")}}, {secret_length, #{value => ?T("Length"), desc => ?T("This option defines the length of the random " "string included in the GET and PUT URLs generated " "by 'mod_http_upload'. The minimum length is 8 characters, " "but it is recommended to choose a larger value. " "The default value is '40'.")}}, {jid_in_url, #{value => "node | sha1", desc => ?T("When this option is set to 'node', the node identifier " "of the user's JID (i.e., the user name) is included in " "the GET and PUT URLs generated by 'mod_http_upload'. " "Otherwise, a SHA-1 hash of the user's bare JID is " "included instead. The default value is 'sha1'.")}}, {thumbnail, #{value => "true | false", desc => ?T("This option specifies whether ejabberd should create " "thumbnails of uploaded images. If a thumbnail is created, " "a <thumbnail/> element that contains the download <uri/> " "and some metadata is returned with the PUT response. " "The default value is 'false'.")}}, {file_mode, #{value => ?T("Permission"), desc => ?T("This option defines the permission bits of uploaded files. " "The bits are specified as an octal number (see the chmod(1) " "manual page) within double quotes. For example: \"0644\". " "The default is undefined, which means no explicit permissions " "will be set.")}}, {dir_mode, #{value => ?T("Permission"), desc => ?T("This option defines the permission bits of the 'docroot' " "directory and any directories created during file uploads. " "The bits are specified as an octal number (see the chmod(1) " "manual page) within double quotes. For example: \"0755\". " "The default is undefined, which means no explicit permissions " "will be set.")}}, {docroot, #{value => ?T("Path"), desc => ?T("Uploaded files are stored below the directory specified " "(as an absolute path) with this option. The keyword " "@HOME@ is replaced with the home directory of the user " "running ejabberd, and the keyword @HOST@ with the virtual " "host name. The default value is \"@HOME@/upload\".")}}, {put_url, #{value => ?T("URL"), desc => ?T("This option specifies the initial part of the PUT URLs " "used for file uploads. The keyword @HOST@ is replaced " "with the virtual host name. NOTE: different virtual " "hosts cannot use the same PUT URL. " "The default value is \"https://@HOST@:5443/upload\".")}}, {get_url, #{value => ?T("URL"), desc => ?T("This option specifies the initial part of the GET URLs " "used for downloading the files. The default value is 'undefined'. " "When this option is 'undefined', this option is set " "to the same value as 'put_url'. The keyword @HOST@ is " "replaced with the virtual host name. NOTE: if GET requests " "are handled by 'mod_http_upload', the 'get_url' must match the " "'put_url'. Setting it to a different value only makes " "sense if an external web server or _`mod_http_fileserver`_ " "is used to serve the uploaded files.")}}, {service_url, #{desc => ?T("Deprecated.")}}, {custom_headers, #{value => "{Name: Value}", desc => ?T("This option specifies additional header fields to be " "included in all HTTP responses. By default no custom " "headers are included.")}}, {external_secret, #{value => ?T("Text"), desc => ?T("This option makes it possible to offload all HTTP " "Upload processing to a separate HTTP server. " "Both ejabberd and the HTTP server should share this " "secret and behave exactly as described at " "https://modules.prosody.im/mod_http_upload_external.html" "[Prosody's mod_http_upload_external] in the " "'Implementation' section. There is no default value.")}}, {rm_on_unregister, #{value => "true | false", desc => ?T("This option specifies whether files uploaded by a user " "should be removed when that user is unregistered. " "The default value is 'true'.")}}, {vcard, #{value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML representation " "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => [{?T("For example, the following XML representation of vCard:"), ["<vCard xmlns='vcard-temp'>", " <FN>Conferences</FN>", " <ADR>", " <WORK/>", " <STREET>Elm Street</STREET>", " </ADR>", "</vCard>"]}, {?T("will be translated to:"), ["vcard:", " fn: Conferences", " adr:", " -", " work: true", " street: Elm Street"]}]}}], example => ["listen:", " ...", " -", " port: 5443", " module: ejabberd_http", " tls: true", " request_handlers:", " ...", " /upload: mod_http_upload", " ...", " ...", "", "modules:", " ...", " mod_http_upload:", " docroot: /ejabberd/upload", " put_url: \"https://@HOST@:5443/upload\"", " ..."]}. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(list()) -> {ok, state()}. init([ServerHost|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), Hosts = gen_mod:get_opt_hosts(Opts), case mod_http_upload_opt:rm_on_unregister(Opts) of true -> ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50); false -> ok end, State = init_state(ServerHost, Hosts, Opts), {ok, State}. -spec handle_call(_, {pid(), _}, state()) -> {reply, {ok, pos_integer(), binary(), pos_integer() | undefined, pos_integer() | undefined}, state()} | {reply, {error, atom()}, state()} | {noreply, state()}. handle_call({use_slot, Slot, Size}, _From, #state{file_mode = FileMode, dir_mode = DirMode, get_url = GetPrefix, thumbnail = Thumbnail, custom_headers = CustomHeaders, docroot = DocRoot} = State) -> case get_slot(Slot, State) of {ok, {Size, TRef}} -> misc:cancel_timer(TRef), NewState = del_slot(Slot, State), Path = str:join([DocRoot | Slot], <<$/>>), {reply, {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders}, NewState}; {ok, {_WrongSize, _TRef}} -> {reply, {error, size_mismatch}, State}; error -> {reply, {error, invalid_slot}, State} end; handle_call(get_conf, _From, #state{docroot = DocRoot, custom_headers = CustomHeaders} = State) -> {reply, {ok, DocRoot, CustomHeaders}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(_, state()) -> {noreply, state()}. handle_cast({reload, NewOpts, OldOpts}, #state{server_host = ServerHost} = State) -> case {mod_http_upload_opt:rm_on_unregister(NewOpts), mod_http_upload_opt:rm_on_unregister(OldOpts)} of {true, false} -> ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50); {false, true} -> ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50); _ -> ok end, NewHosts = gen_mod:get_opt_hosts(NewOpts), OldHosts = gen_mod:get_opt_hosts(OldOpts), lists:foreach(fun ejabberd_router:unregister_route/1, OldHosts -- NewHosts), NewState = init_state(State#state{hosts = NewHosts -- OldHosts}, NewOpts), {noreply, NewState}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. -spec handle_info(timeout | _, state()) -> {noreply, state()}. handle_info({route, #iq{lang = Lang} = Packet}, State) -> try xmpp:decode_els(Packet) of IQ -> {Reply, NewState} = case process_iq(IQ, State) of R when is_record(R, iq) -> {R, State}; {R, S} -> {R, S}; not_request -> {none, State} end, if Reply /= none -> ejabberd_router:route(Reply); true -> ok end, {noreply, NewState} catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Packet, Err), {noreply, State} end; handle_info({timeout, _TRef, Slot}, State) -> NewState = del_slot(Slot, State), {noreply, NewState}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, hosts = Hosts}) -> ?DEBUG("Stopping HTTP upload process for ~ts: ~p", [ServerHost, Reason]), ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), lists:foreach(fun ejabberd_router:unregister_route/1, Hosts). -spec code_change({down, _} | _, state(), _) -> {ok, state()}. code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> ?DEBUG("Updating HTTP upload process for ~ts", [ServerHost]), {ok, State}. %%-------------------------------------------------------------------- %% ejabberd_http callback. %%-------------------------------------------------------------------- -spec process([binary()], #request{}) -> {pos_integer(), [{binary(), binary()}], binary()}. process(LocalPath, #request{method = Method, host = Host, ip = IP}) when length(LocalPath) < 3, Method == 'PUT' orelse Method == 'GET' orelse Method == 'HEAD' -> ?DEBUG("Rejecting ~ts request from ~ts for ~ts: Too few path components", [Method, encode_addr(IP), Host]), http_response(404); process(_LocalPath, #request{method = 'PUT', host = Host, ip = IP, length = Length} = Request) -> {Proc, Slot} = parse_http_request(Request), try gen_server:call(Proc, {use_slot, Slot, Length}, ?CALL_TIMEOUT) of {ok, Path, FileMode, DirMode, GetPrefix, Thumbnail, CustomHeaders} -> ?DEBUG("Storing file from ~ts for ~ts: ~ts", [encode_addr(IP), Host, Path]), case store_file(Path, Request, FileMode, DirMode, GetPrefix, Slot, Thumbnail) of ok -> http_response(201, CustomHeaders); {ok, Headers, OutData} -> http_response(201, ejabberd_http:apply_custom_headers(Headers, CustomHeaders), OutData); {error, closed} -> ?DEBUG("Cannot store file ~ts from ~ts for ~ts: connection closed", [Path, encode_addr(IP), Host]), http_response(404); {error, Error} -> ?ERROR_MSG("Cannot store file ~ts from ~ts for ~ts: ~ts", [Path, encode_addr(IP), Host, format_error(Error)]), http_response(500) end; {error, size_mismatch} -> ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Unexpected size (~B)", [lists:last(Slot), encode_addr(IP), Host, Length]), http_response(413); {error, invalid_slot} -> ?WARNING_MSG("Rejecting file ~ts from ~ts for ~ts: Invalid slot", [lists:last(Slot), encode_addr(IP), Host]), http_response(403) catch exit:{noproc, _} -> ?WARNING_MSG("Cannot handle PUT request from ~ts for ~ts: " "Upload not configured for this host", [encode_addr(IP), Host]), http_response(404); _:Error -> ?ERROR_MSG("Cannot handle PUT request from ~ts for ~ts: ~p", [encode_addr(IP), Host, Error]), http_response(500) end; process(_LocalPath, #request{method = Method, host = Host, ip = IP} = Request) when Method == 'GET'; Method == 'HEAD' -> {Proc, [_UserDir, _RandDir, FileName] = Slot} = parse_http_request(Request), try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of {ok, DocRoot, CustomHeaders} -> Path = str:join([DocRoot | Slot], <<$/>>), case file:open(Path, [read]) of {ok, Fd} -> file:close(Fd), ?INFO_MSG("Serving ~ts to ~ts", [Path, encode_addr(IP)]), ContentType = guess_content_type(FileName), Headers1 = case ContentType of <<"image/", _SubType/binary>> -> []; <<"text/", _SubType/binary>> -> []; _ -> [{<<"Content-Disposition">>, <<"attachment; filename=", $", FileName/binary, $">>}] end, Headers2 = [{<<"Content-Type">>, ContentType} | Headers1], Headers3 = ejabberd_http:apply_custom_headers(Headers2, CustomHeaders), http_response(200, Headers3, {file, Path}); {error, eacces} -> ?WARNING_MSG("Cannot serve ~ts to ~ts: Permission denied", [Path, encode_addr(IP)]), http_response(403); {error, enoent} -> ?WARNING_MSG("Cannot serve ~ts to ~ts: No such file", [Path, encode_addr(IP)]), http_response(404); {error, eisdir} -> ?WARNING_MSG("Cannot serve ~ts to ~ts: Is a directory", [Path, encode_addr(IP)]), http_response(404); {error, Error} -> ?WARNING_MSG("Cannot serve ~ts to ~ts: ~ts", [Path, encode_addr(IP), format_error(Error)]), http_response(500) end catch exit:{noproc, _} -> ?WARNING_MSG("Cannot handle ~ts request from ~ts for ~ts: " "Upload not configured for this host", [Method, encode_addr(IP), Host]), http_response(404); _:Error -> ?ERROR_MSG("Cannot handle ~ts request from ~ts for ~ts: ~p", [Method, encode_addr(IP), Host, Error]), http_response(500) end; process(_LocalPath, #request{method = 'OPTIONS', host = Host, ip = IP} = Request) -> ?DEBUG("Responding to OPTIONS request from ~ts for ~ts", [encode_addr(IP), Host]), {Proc, _Slot} = parse_http_request(Request), try gen_server:call(Proc, get_conf, ?CALL_TIMEOUT) of {ok, _DocRoot, CustomHeaders} -> AllowHeader = {<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}, http_response(200, ejabberd_http:apply_custom_headers([AllowHeader], CustomHeaders)) catch exit:{noproc, _} -> ?WARNING_MSG("Cannot handle OPTIONS request from ~ts for ~ts: " "Upload not configured for this host", [encode_addr(IP), Host]), http_response(404); _:Error -> ?ERROR_MSG("Cannot handle OPTIONS request from ~ts for ~ts: ~p", [encode_addr(IP), Host, Error]), http_response(500) end; process(_LocalPath, #request{method = Method, host = Host, ip = IP}) -> ?DEBUG("Rejecting ~ts request from ~ts for ~ts", [Method, encode_addr(IP), Host]), http_response(405, [{<<"Allow">>, <<"OPTIONS, HEAD, GET, PUT">>}]). %%-------------------------------------------------------------------- %% State initialization %%-------------------------------------------------------------------- -spec init_state(binary(), [binary()], gen_mod:opts()) -> state(). init_state(ServerHost, Hosts, Opts) -> init_state(#state{server_host = ServerHost, hosts = Hosts}, Opts). -spec init_state(state(), gen_mod:opts()) -> state(). init_state(#state{server_host = ServerHost, hosts = Hosts} = State, Opts) -> Name = mod_http_upload_opt:name(Opts), Access = mod_http_upload_opt:access(Opts), MaxSize = mod_http_upload_opt:max_size(Opts), SecretLength = mod_http_upload_opt:secret_length(Opts), JIDinURL = mod_http_upload_opt:jid_in_url(Opts), DocRoot = mod_http_upload_opt:docroot(Opts), FileMode = mod_http_upload_opt:file_mode(Opts), DirMode = mod_http_upload_opt:dir_mode(Opts), PutURL = mod_http_upload_opt:put_url(Opts), GetURL = case mod_http_upload_opt:get_url(Opts) of undefined -> PutURL; URL -> URL end, ServiceURL = mod_http_upload_opt:service_url(Opts), Thumbnail = mod_http_upload_opt:thumbnail(Opts), ExternalSecret = mod_http_upload_opt:external_secret(Opts), CustomHeaders = mod_http_upload_opt:custom_headers(Opts), DocRoot1 = expand_home(str:strip(DocRoot, right, $/)), DocRoot2 = expand_host(DocRoot1, ServerHost), case DirMode of undefined -> ok; Mode -> file:change_mode(DocRoot2, Mode) end, lists:foreach( fun(Host) -> ejabberd_router:register_route(Host, ServerHost) end, Hosts), State#state{server_host = ServerHost, hosts = Hosts, name = Name, access = Access, max_size = MaxSize, secret_length = SecretLength, jid_in_url = JIDinURL, file_mode = FileMode, dir_mode = DirMode, thumbnail = Thumbnail, docroot = DocRoot2, put_url = expand_host(str:strip(PutURL, right, $/), ServerHost), get_url = expand_host(str:strip(GetURL, right, $/), ServerHost), service_url = ServiceURL, external_secret = ExternalSecret, custom_headers = CustomHeaders}. %%-------------------------------------------------------------------- %% Exported utility functions. %%-------------------------------------------------------------------- -spec get_proc_name(binary(), atom()) -> atom(). get_proc_name(ServerHost, ModuleName) -> PutURL = mod_http_upload_opt:put_url(ServerHost), get_proc_name(ServerHost, ModuleName, PutURL). -spec get_proc_name(binary(), atom(), binary()) -> atom(). get_proc_name(ServerHost, ModuleName, PutURL) -> %% Once we depend on OTP >= 20.0, we can use binaries with http_uri. {ok, _Scheme, Host0, _Port, Path0} = misc:uri_parse(expand_host(PutURL, ServerHost)), Host = jid:nameprep(iolist_to_binary(Host0)), Path = str:strip(iolist_to_binary(Path0), right, $/), ProcPrefix = <<Host/binary, Path/binary>>, gen_mod:get_module_proc(ProcPrefix, ModuleName). -spec expand_home(binary()) -> binary(). expand_home(Input) -> {ok, [[Home]]} = init:get_argument(home), misc:expand_keyword(<<"@HOME@">>, Input, Home). -spec expand_host(binary(), binary()) -> binary(). expand_host(Input, Host) -> misc:expand_keyword(<<"@HOST@">>, Input, Host). %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- %% XMPP request handling. -spec process_iq(iq(), state()) -> {iq(), state()} | iq() | not_request. process_iq(#iq{type = get, lang = Lang, sub_els = [#disco_info{}]} = IQ, #state{server_host = ServerHost, name = Name}) -> AddInfo = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<"">>, <<"">>]), xmpp:make_iq_result(IQ, iq_disco_info(ServerHost, Lang, Name, AddInfo)); process_iq(#iq{type = get, sub_els = [#disco_items{}]} = IQ, _State) -> xmpp:make_iq_result(IQ, #disco_items{}); process_iq(#iq{type = get, sub_els = [#vcard_temp{}], lang = Lang} = IQ, #state{server_host = ServerHost}) -> VCard = case mod_http_upload_opt:vcard(ServerHost) of undefined -> #vcard_temp{fn = <<"ejabberd/mod_http_upload">>, url = ejabberd_config:get_uri(), desc = misc:get_descr( Lang, ?T("ejabberd HTTP Upload service"))}; V -> V end, xmpp:make_iq_result(IQ, VCard); process_iq(#iq{type = get, sub_els = [#upload_request{filename = File, size = Size, 'content-type' = CType, xmlns = XMLNS}]} = IQ, State) -> process_slot_request(IQ, File, Size, CType, XMLNS, State); process_iq(#iq{type = get, sub_els = [#upload_request_0{filename = File, size = Size, 'content-type' = CType, xmlns = XMLNS}]} = IQ, State) -> process_slot_request(IQ, File, Size, CType, XMLNS, State); process_iq(#iq{type = T, lang = Lang} = IQ, _State) when T == get; T == set -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); process_iq(#iq{}, _State) -> not_request. -spec process_slot_request(iq(), binary(), pos_integer(), binary(), binary(), state()) -> {iq(), state()} | iq(). process_slot_request(#iq{lang = Lang, from = From} = IQ, File, Size, CType, XMLNS, #state{server_host = ServerHost, access = Access} = State) -> case acl:match_rule(ServerHost, Access, From) of allow -> ContentType = yield_content_type(CType), case create_slot(State, From, File, Size, ContentType, XMLNS, Lang) of {ok, Slot} -> Query = make_query_string(Slot, Size, State), NewState = add_slot(Slot, Size, State), NewSlot = mk_slot(Slot, State, XMLNS, Query), {xmpp:make_iq_result(IQ, NewSlot), NewState}; {ok, PutURL, GetURL} -> Slot = mk_slot(PutURL, GetURL, XMLNS, <<"">>), xmpp:make_iq_result(IQ, Slot); {error, Error} -> xmpp:make_error(IQ, Error) end; deny -> ?DEBUG("Denying HTTP upload slot request from ~ts", [jid:encode(From)]), Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. -spec create_slot(state(), jid(), binary(), pos_integer(), binary(), binary(), binary()) -> {ok, slot()} | {ok, binary(), binary()} | {error, xmpp_element()}. create_slot(#state{service_url = undefined, max_size = MaxSize}, JID, File, Size, _ContentType, XMLNS, Lang) when MaxSize /= infinity, Size > MaxSize -> Text = {?T("File larger than ~w bytes"), [MaxSize]}, ?WARNING_MSG("Rejecting file ~ts from ~ts (too large: ~B bytes)", [File, jid:encode(JID), Size]), Error = xmpp:err_not_acceptable(Text, Lang), Els = xmpp:get_els(Error), Els1 = [#upload_file_too_large{'max-file-size' = MaxSize, xmlns = XMLNS} | Els], Error1 = xmpp:set_els(Error, Els1), {error, Error1}; create_slot(#state{service_url = undefined, jid_in_url = JIDinURL, secret_length = SecretLength, server_host = ServerHost, docroot = DocRoot}, JID, File, Size, _ContentType, _XMLNS, Lang) -> UserStr = make_user_string(JID, JIDinURL), UserDir = <<DocRoot/binary, $/, UserStr/binary>>, case ejabberd_hooks:run_fold(http_upload_slot_request, ServerHost, allow, [ServerHost, JID, UserDir, Size, Lang]) of allow -> RandStr = p1_rand:get_alphanum_string(SecretLength), FileStr = make_file_string(File), ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", [jid:encode(JID), File, Size]), {ok, [UserStr, RandStr, FileStr]}; deny -> {error, xmpp:err_service_unavailable()}; #stanza_error{} = Error -> {error, Error} end; create_slot(#state{service_url = ServiceURL}, #jid{luser = U, lserver = S} = JID, File, Size, ContentType, _XMLNS, Lang) -> Options = [{body_format, binary}, {full_result, false}], HttpOptions = [{timeout, ?SERVICE_REQUEST_TIMEOUT}], SizeStr = integer_to_binary(Size), JidStr = jid:encode({U, S, <<"">>}), GetRequest = <<ServiceURL/binary, "?jid=", (misc:url_encode(JidStr))/binary, "&name=", (misc:url_encode(File))/binary, "&size=", (misc:url_encode(SizeStr))/binary, "&content_type=", (misc:url_encode(ContentType))/binary>>, case httpc:request(get, {binary_to_list(GetRequest), []}, HttpOptions, Options) of {ok, {Code, Body}} when Code >= 200, Code =< 299 -> case binary:split(Body, <<$\n>>, [global, trim]) of [<<"http", _/binary>> = PutURL, <<"http", _/binary>> = GetURL] -> ?INFO_MSG("Got HTTP upload slot for ~ts (file: ~ts, size: ~B)", [jid:encode(JID), File, Size]), {ok, PutURL, GetURL}; Lines -> ?ERROR_MSG("Can't parse data received for ~ts from <~ts>: ~p", [jid:encode(JID), ServiceURL, Lines]), Txt = ?T("Failed to parse HTTP response"), {error, xmpp:err_service_unavailable(Txt, Lang)} end; {ok, {402, _Body}} -> ?WARNING_MSG("Got status code 402 for ~ts from <~ts>", [jid:encode(JID), ServiceURL]), {error, xmpp:err_resource_constraint()}; {ok, {403, _Body}} -> ?WARNING_MSG("Got status code 403 for ~ts from <~ts>", [jid:encode(JID), ServiceURL]), {error, xmpp:err_not_allowed()}; {ok, {413, _Body}} -> ?WARNING_MSG("Got status code 413 for ~ts from <~ts>", [jid:encode(JID), ServiceURL]), {error, xmpp:err_not_acceptable()}; {ok, {Code, _Body}} -> ?ERROR_MSG("Unexpected status code for ~ts from <~ts>: ~B", [jid:encode(JID), ServiceURL, Code]), {error, xmpp:err_service_unavailable()}; {error, Reason} -> ?ERROR_MSG("Error requesting upload slot for ~ts from <~ts>: ~p", [jid:encode(JID), ServiceURL, Reason]), {error, xmpp:err_service_unavailable()} end. -spec add_slot(slot(), pos_integer(), state()) -> state(). add_slot(Slot, Size, #state{external_secret = <<>>, slots = Slots} = State) -> TRef = erlang:start_timer(?SLOT_TIMEOUT, self(), Slot), NewSlots = maps:put(Slot, {Size, TRef}, Slots), State#state{slots = NewSlots}; add_slot(_Slot, _Size, State) -> State. -spec get_slot(slot(), state()) -> {ok, {pos_integer(), reference()}} | error. get_slot(Slot, #state{slots = Slots}) -> maps:find(Slot, Slots). -spec del_slot(slot(), state()) -> state(). del_slot(Slot, #state{slots = Slots} = State) -> NewSlots = maps:remove(Slot, Slots), State#state{slots = NewSlots}. -spec mk_slot(slot(), state(), binary(), binary()) -> upload_slot(); (binary(), binary(), binary(), binary()) -> upload_slot(). mk_slot(Slot, #state{put_url = PutPrefix, get_url = GetPrefix}, XMLNS, Query) -> PutURL = str:join([PutPrefix | Slot], <<$/>>), GetURL = str:join([GetPrefix | Slot], <<$/>>), mk_slot(PutURL, GetURL, XMLNS, Query); mk_slot(PutURL, GetURL, XMLNS, Query) -> PutURL1 = <<(misc:url_encode(PutURL))/binary, Query/binary>>, GetURL1 = misc:url_encode(GetURL), case XMLNS of ?NS_HTTP_UPLOAD_0 -> #upload_slot_0{get = GetURL1, put = PutURL1, xmlns = XMLNS}; _ -> #upload_slot{get = GetURL1, put = PutURL1, xmlns = XMLNS} end. -spec make_user_string(jid(), sha1 | node) -> binary(). make_user_string(#jid{luser = U, lserver = S}, sha1) -> str:sha(<<U/binary, $@, S/binary>>); make_user_string(#jid{luser = U}, node) -> replace_special_chars(U). -spec make_file_string(binary()) -> binary(). make_file_string(File) -> replace_special_chars(File). -spec make_query_string(slot(), non_neg_integer(), state()) -> binary(). make_query_string(Slot, Size, #state{external_secret = Key}) when Key /= <<>> -> UrlPath = str:join(Slot, <<$/>>), SizeStr = integer_to_binary(Size), Data = <<UrlPath/binary, " ", SizeStr/binary>>, HMAC = str:to_hexlist(misc:crypto_hmac(sha256, Key, Data)), <<"?v=", HMAC/binary>>; make_query_string(_Slot, _Size, _State) -> <<>>. -spec replace_special_chars(binary()) -> binary(). replace_special_chars(S) -> re:replace(S, <<"[^\\p{Xan}_.-]">>, <<$_>>, [unicode, global, {return, binary}]). -spec yield_content_type(binary()) -> binary(). yield_content_type(<<"">>) -> ?DEFAULT_CONTENT_TYPE; yield_content_type(Type) -> Type. -spec encode_addr(inet:ip_address() | {inet:ip_address(), inet:port_number()} | undefined) -> binary(). encode_addr(IP) -> ejabberd_config:may_hide_data(misc:ip_to_list(IP)). -spec iq_disco_info(binary(), binary(), binary(), [xdata()]) -> disco_info(). iq_disco_info(Host, Lang, Name, AddInfo) -> Form = case mod_http_upload_opt:max_size(Host) of infinity -> AddInfo; MaxSize -> lists:foldl( fun(NS, Acc) -> Fs = http_upload:encode( [{'max-file-size', MaxSize}], NS, Lang), [#xdata{type = result, fields = Fs}|Acc] end, AddInfo, [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD]) end, #disco_info{identities = [#identity{category = <<"store">>, type = <<"file">>, name = translate:translate(Lang, Name)}], features = [?NS_HTTP_UPLOAD, ?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD_OLD, ?NS_VCARD, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS], xdata = Form}. %% HTTP request handling. -spec parse_http_request(#request{}) -> {atom(), slot()}. parse_http_request(#request{host = Host0, path = Path}) -> Host = jid:nameprep(Host0), PrefixLength = length(Path) - 3, {ProcURL, Slot} = if PrefixLength > 0 -> Prefix = lists:sublist(Path, PrefixLength), {str:join([Host | Prefix], $/), lists:nthtail(PrefixLength, Path)}; true -> {Host, Path} end, {gen_mod:get_module_proc(ProcURL, ?MODULE), Slot}. -spec store_file(binary(), http_request(), integer() | undefined, integer() | undefined, binary(), slot(), boolean()) -> ok | {ok, [{binary(), binary()}], binary()} | {error, term()}. store_file(Path, Request, FileMode, DirMode, GetPrefix, Slot, Thumbnail) -> case do_store_file(Path, Request, FileMode, DirMode) of ok when Thumbnail -> case read_image(Path) of {ok, Data, MediaInfo} -> case convert(Data, MediaInfo) of {ok, #media_info{path = OutPath} = OutMediaInfo} -> [UserDir, RandDir | _] = Slot, FileName = filename:basename(OutPath), URL = str:join([GetPrefix, UserDir, RandDir, FileName], <<$/>>), ThumbEl = thumb_el(OutMediaInfo, URL), {ok, [{<<"Content-Type">>, <<"text/xml; charset=utf-8">>}], fxml:element_to_binary(ThumbEl)}; pass -> ok end; pass -> ok end; ok -> ok; Err -> Err end. -spec do_store_file(file:filename_all(), http_request(), integer() | undefined, integer() | undefined) -> ok | {error, term()}. do_store_file(Path, Request, FileMode, DirMode) -> try ok = filelib:ensure_dir(Path), ok = ejabberd_http:recv_file(Request, Path), if is_integer(FileMode) -> ok = file:change_mode(Path, FileMode); FileMode == undefined -> ok end, if is_integer(DirMode) -> RandDir = filename:dirname(Path), UserDir = filename:dirname(RandDir), ok = file:change_mode(RandDir, DirMode), ok = file:change_mode(UserDir, DirMode); DirMode == undefined -> ok end catch _:{badmatch, {error, Error}} -> {error, Error} end. -spec guess_content_type(binary()) -> binary(). guess_content_type(FileName) -> mod_http_fileserver:content_type(FileName, ?DEFAULT_CONTENT_TYPE, ?CONTENT_TYPES). -spec http_response(100..599) -> {pos_integer(), [{binary(), binary()}], binary()}. http_response(Code) -> http_response(Code, []). -spec http_response(100..599, [{binary(), binary()}]) -> {pos_integer(), [{binary(), binary()}], binary()}. http_response(Code, ExtraHeaders) -> Message = <<(code_to_message(Code))/binary, $\n>>, http_response(Code, ExtraHeaders, Message). -type http_body() :: binary() | {file, file:filename_all()}. -spec http_response(100..599, [{binary(), binary()}], http_body()) -> {pos_integer(), [{binary(), binary()}], http_body()}. http_response(Code, ExtraHeaders, Body) -> Headers = case proplists:is_defined(<<"Content-Type">>, ExtraHeaders) of true -> ExtraHeaders; false -> [{<<"Content-Type">>, <<"text/plain">>} | ExtraHeaders] end, {Code, Headers, Body}. -spec code_to_message(100..599) -> binary(). code_to_message(201) -> <<"Upload successful.">>; code_to_message(403) -> <<"Forbidden.">>; code_to_message(404) -> <<"Not found.">>; code_to_message(405) -> <<"Method not allowed.">>; code_to_message(413) -> <<"File size doesn't match requested size.">>; code_to_message(500) -> <<"Internal server error.">>; code_to_message(_Code) -> <<"">>. -spec format_error(atom()) -> string(). format_error(Reason) -> case file:format_error(Reason) of "unknown POSIX error" -> case inet:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); Txt -> Txt end; Txt -> Txt end. %%-------------------------------------------------------------------- %% Image manipulation stuff. %%-------------------------------------------------------------------- -spec read_image(binary()) -> {ok, binary(), media_info()} | pass. read_image(Path) -> case file:read_file(Path) of {ok, Data} -> case eimp:identify(Data) of {ok, Info} -> {ok, Data, #media_info{ path = Path, type = proplists:get_value(type, Info), width = proplists:get_value(width, Info), height = proplists:get_value(height, Info)}}; {error, Why} -> ?DEBUG("Cannot identify type of ~ts: ~ts", [Path, eimp:format_error(Why)]), pass end; {error, Reason} -> ?DEBUG("Failed to read file ~ts: ~ts", [Path, format_error(Reason)]), pass end. -spec convert(binary(), media_info()) -> {ok, media_info()} | pass. convert(InData, #media_info{path = Path, type = T, width = W, height = H} = Info) -> if W * H >= 25000000 -> ?DEBUG("The image ~ts is more than 25 Mpix", [Path]), pass; W =< 300, H =< 300 -> {ok, Info}; true -> Dir = filename:dirname(Path), Ext = atom_to_binary(T, latin1), FileName = <<(p1_rand:get_string())/binary, $., Ext/binary>>, OutPath = filename:join(Dir, FileName), {W1, H1} = if W > H -> {300, round(H*300/W)}; H > W -> {round(W*300/H), 300}; true -> {300, 300} end, OutInfo = #media_info{path = OutPath, type = T, width = W1, height = H1}, case eimp:convert(InData, T, [{scale, {W1, H1}}]) of {ok, OutData} -> case file:write_file(OutPath, OutData) of ok -> {ok, OutInfo}; {error, Why} -> ?ERROR_MSG("Failed to write to ~ts: ~ts", [OutPath, format_error(Why)]), pass end; {error, Why} -> ?ERROR_MSG("Failed to convert ~ts to ~ts: ~ts", [Path, OutPath, eimp:format_error(Why)]), pass end end. -spec thumb_el(media_info(), binary()) -> xmlel(). thumb_el(#media_info{type = T, height = H, width = W}, URI) -> MimeType = <<"image/", (atom_to_binary(T, latin1))/binary>>, Thumb = #thumbnail{'media-type' = MimeType, uri = URI, height = H, width = W}, xmpp:encode(Thumb). %%-------------------------------------------------------------------- %% Remove user. %%-------------------------------------------------------------------- -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> ServerHost = jid:nameprep(Server), DocRoot = mod_http_upload_opt:docroot(ServerHost), JIDinURL = mod_http_upload_opt:jid_in_url(ServerHost), DocRoot1 = expand_host(expand_home(DocRoot), ServerHost), UserStr = make_user_string(jid:make(User, Server), JIDinURL), UserDir = str:join([DocRoot1, UserStr], <<$/>>), case misc:delete_dir(UserDir) of ok -> ?INFO_MSG("Removed HTTP upload directory of ~ts@~ts", [User, Server]); {error, enoent} -> ?DEBUG("Found no HTTP upload directory of ~ts@~ts", [User, Server]); {error, Error} -> ?ERROR_MSG("Cannot remove HTTP upload directory of ~ts@~ts: ~ts", [User, Server, format_error(Error)]) end, ok. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/node_pep_sql.erl�����������������������������������������������������������������0000644�0002322�0002322�00000021611�14154362354�017576� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : node_pep_sql.erl %%% Author : Christophe Romain <christophe.romain@process-one.net> %%% Purpose : Standard PubSub PEP plugin with ODBC backend %%% Created : 1 Dec 2007 by Christophe Romain <christophe.romain@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module <strong>{@module}</strong> is the pep PubSub plugin. %%% <p>PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.</p> -module(node_pep_sql). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include("ejabberd_sql_pt.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/2, remove_extra_items/3, remove_expired_items/2, 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, depends/3, get_entity_subscriptions_for_send_last/2, get_last_items/3, get_only_item/2]). depends(_Host, _ServerHost, _Opts) -> [{mod_caps, hard}]. init(Host, ServerHost, Opts) -> node_flat_sql:init(Host, ServerHost, Opts), ok. terminate(Host, ServerHost) -> node_flat_sql:terminate(Host, ServerHost), ok. options() -> [{sql, true}, {rsm, true} | node_pep:options()]. features() -> [<<"rsm">> | node_pep:features()]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_pep:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> node_flat_sql:create_node(Nidx, Owner), {result, {default, broadcast}}. delete_node(Nodes) -> node_flat_sql:delete_node(Nodes). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> node_flat_sql:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> case node_flat_sql:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of {error, Error} -> {error, Error}; {result, _} -> {result, default} end. publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> node_flat_sql:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts). remove_extra_items(Nidx, MaxItems) -> node_flat_sql:remove_extra_items(Nidx, MaxItems). remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat_sql:remove_extra_items(Nidx, MaxItems, ItemIds). remove_expired_items(Nidx, Seconds) -> node_flat_sql:remove_expired_items(Nidx, Seconds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat_sql:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> node_flat_sql:purge_node(Nidx, Owner). get_entity_affiliations(_Host, Owner) -> OwnerKey = jid:tolower(jid:remove_resource(Owner)), node_flat_sql:get_entity_affiliations(OwnerKey, Owner). get_node_affiliations(Nidx) -> node_flat_sql:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> node_flat_sql:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> node_flat_sql:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(_Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of GenKey -> GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " "where i.nodeid = n.nodeid and " "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); _ -> SJ = node_flat_sql:encode_jid(SubKey), ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " "where i.nodeid = n.nodeid and " "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") end, {result, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> lists:foldl( fun({H, N, T, I, J, S}, Acc) -> O = node_flat_sql:decode_jid(H), Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), Jid = node_flat_sql:decode_jid(J), lists:foldl( fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid} | Acc2] end, Acc, node_flat_sql:decode_subscriptions(S)) end, [], RItems); _ -> [] end}. get_entity_subscriptions_for_send_last(_Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), HLike = <<"%@", (node_flat_sql:encode_host_like(element(2, SubKey)))/binary>>, GJ = node_flat_sql:encode_jid(GenKey), Query = case SubKey of GenKey -> GJLike = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n, pubsub_node_option o " "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " "name='send_last_published_item' and val='on_sub_and_presence' and " "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host like %(HLike)s %ESCAPE"); _ -> SJ = node_flat_sql:encode_jid(SubKey), ?SQL("select @(host)s, @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n, pubsub_node_option o " "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " "name='send_last_published_item' and val='on_sub_and_presence' and " "jid in (%(SJ)s,%(GJ)s) and host like %(HLike)s %ESCAPE") end, {result, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> lists:foldl( fun ({H, N, T, I, J, S}, Acc) -> O = node_flat_sql:decode_jid(H), Node = nodetree_tree_sql:raw_to_node(O, {N, <<"">>, T, I}), Jid = node_flat_sql:decode_jid(J), lists:foldl( fun ({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}| Acc2] end, Acc, node_flat_sql:decode_subscriptions(S)) end, [], RItems); _ -> [] end}. get_node_subscriptions(Nidx) -> node_flat_sql:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> node_flat_sql:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> node_flat_sql:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> node_flat_sql:get_pending_nodes(Host, Owner). get_states(Nidx) -> node_flat_sql:get_states(Nidx). get_state(Nidx, JID) -> node_flat_sql:get_state(Nidx, JID). set_state(State) -> node_flat_sql:set_state(State). get_items(Nidx, From, RSM) -> node_flat_sql:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> node_flat_sql:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_last_items(Nidx, JID, Count) -> node_flat_sql:get_last_items(Nidx, JID, Count). get_only_item(Nidx, JID) -> node_flat_sql:get_only_item(Nidx, JID). get_item(Nidx, ItemId) -> node_flat_sql:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> node_flat_sql:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> node_flat_sql:set_item(Item). get_item_name(Host, Node, Id) -> node_flat_sql:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat_sql:node_to_path(Node). path_to_node(Path) -> node_flat_sql:path_to_node(Path). �����������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_stream_mgmt.erl��������������������������������������������������������������0000644�0002322�0002322�00000110404�14154362354�020303� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Created : 25 Dec 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_stream_mgmt). -behaviour(gen_mod). -author('holger@zedat.fu-berlin.de'). -protocol({xep, 198, '1.5.2'}). %% gen_mod API -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% hooks -export([c2s_stream_started/2, c2s_stream_features/2, c2s_authenticated_packet/2, c2s_unauthenticated_packet/2, c2s_unbinded_packet/2, c2s_closed/2, c2s_terminated/2, c2s_handle_send/3, c2s_handle_info/2, c2s_handle_call/3, c2s_handle_recv/3]). %% adjust pending session timeout / access queue -export([get_resume_timeout/1, set_resume_timeout/2, queue_find/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include_lib("p1_utils/include/p1_queue.hrl"). -include("translate.hrl"). -define(STREAM_MGMT_CACHE, stream_mgmt_cache). -define(is_sm_packet(Pkt), is_record(Pkt, sm_enable) or is_record(Pkt, sm_resume) or is_record(Pkt, sm_a) or is_record(Pkt, sm_r)). -type state() :: ejabberd_c2s:state(). -type queue() :: p1_queue:queue({non_neg_integer(), erlang:timestamp(), xmpp_element() | xmlel()}). -type id() :: binary(). -type error_reason() :: session_not_found | session_timed_out | session_is_dead | session_has_exited | session_was_killed | session_copy_timed_out | invalid_previd. %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> init_cache(Opts), ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 50), ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, c2s_stream_features, 50), ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), ejabberd_hooks:add(c2s_unbinded_packet, Host, ?MODULE, c2s_unbinded_packet, 50), ejabberd_hooks:add(c2s_authenticated_packet, Host, ?MODULE, c2s_authenticated_packet, 50), ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, c2s_handle_send, 50), ejabberd_hooks:add(c2s_handle_recv, Host, ?MODULE, c2s_handle_recv, 50), ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE, c2s_handle_call, 50), ejabberd_hooks:add(c2s_closed, Host, ?MODULE, c2s_closed, 50), ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, c2s_terminated, 50). stop(Host) -> ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 50), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, c2s_stream_features, 50), ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), ejabberd_hooks:delete(c2s_unbinded_packet, Host, ?MODULE, c2s_unbinded_packet, 50), ejabberd_hooks:delete(c2s_authenticated_packet, Host, ?MODULE, c2s_authenticated_packet, 50), ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, c2s_handle_send, 50), ejabberd_hooks:delete(c2s_handle_recv, Host, ?MODULE, c2s_handle_recv, 50), ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE, c2s_handle_call, 50), ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, c2s_closed, 50), ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE, c2s_terminated, 50). reload(_Host, NewOpts, _OldOpts) -> init_cache(NewOpts), ?WARNING_MSG("Module ~ts is reloaded, but new configuration will take " "effect for newly created client connections only", [?MODULE]). depends(_Host, _Opts) -> []. c2s_stream_started(#{lserver := LServer} = State, _StreamStart) -> State1 = maps:remove(mgmt_options, State), ResumeTimeout = get_configured_resume_timeout(LServer), MaxResumeTimeout = get_max_resume_timeout(LServer, ResumeTimeout), State1#{mgmt_state => inactive, mgmt_queue_type => get_queue_type(LServer), mgmt_max_queue => get_max_ack_queue(LServer), mgmt_timeout => ResumeTimeout, mgmt_max_timeout => MaxResumeTimeout, mgmt_ack_timeout => get_ack_timeout(LServer), mgmt_resend => get_resend_on_timeout(LServer), mgmt_stanzas_in => 0, mgmt_stanzas_out => 0, mgmt_stanzas_req => 0}; c2s_stream_started(State, _StreamStart) -> State. c2s_stream_features(Acc, Host) -> case gen_mod:is_loaded(Host, ?MODULE) of true -> [#feature_sm{xmlns = ?NS_STREAM_MGMT_2}, #feature_sm{xmlns = ?NS_STREAM_MGMT_3}|Acc]; false -> Acc end. c2s_unauthenticated_packet(#{lang := Lang} = State, Pkt) when ?is_sm_packet(Pkt) -> %% XEP-0198 says: "For client-to-server connections, the client MUST NOT %% attempt to enable stream management until after it has completed Resource %% Binding unless it is resuming a previous session". However, it also %% says: "Stream management errors SHOULD be considered recoverable", so we %% won't bail out. Err = #sm_failed{reason = 'not-authorized', text = xmpp:mk_text(?T("Unauthorized"), Lang), xmlns = ?NS_STREAM_MGMT_3}, {stop, send(State, Err)}; c2s_unauthenticated_packet(State, _Pkt) -> State. c2s_unbinded_packet(State, #sm_resume{} = Pkt) -> case handle_resume(State, Pkt) of {ok, ResumedState} -> {stop, ResumedState}; {error, State1} -> {stop, State1} end; c2s_unbinded_packet(State, Pkt) when ?is_sm_packet(Pkt) -> c2s_unauthenticated_packet(State, Pkt); c2s_unbinded_packet(State, _Pkt) -> State. c2s_authenticated_packet(#{mgmt_state := MgmtState} = State, Pkt) when ?is_sm_packet(Pkt) -> if MgmtState == pending; MgmtState == active -> {stop, perform_stream_mgmt(Pkt, State)}; true -> {stop, negotiate_stream_mgmt(Pkt, State)} end; c2s_authenticated_packet(State, Pkt) -> update_num_stanzas_in(State, Pkt). c2s_handle_recv(#{mgmt_state := MgmtState, lang := Lang} = State, El, {error, Why}) -> Xmlns = xmpp:get_ns(El), IsStanza = xmpp:is_stanza(El), if Xmlns == ?NS_STREAM_MGMT_2; Xmlns == ?NS_STREAM_MGMT_3 -> Txt = xmpp:io_format_error(Why), Err = #sm_failed{reason = 'bad-request', text = xmpp:mk_text(Txt, Lang), xmlns = Xmlns}, send(State, Err); IsStanza andalso (MgmtState == pending orelse MgmtState == active) -> State1 = update_num_stanzas_in(State, El), case xmpp:get_type(El) of <<"result">> -> State1; <<"error">> -> State1; _ -> State1#{mgmt_force_enqueue => true} end; true -> State end; c2s_handle_recv(State, _, _) -> State. c2s_handle_send(#{mgmt_state := MgmtState, mod := Mod, lang := Lang} = State, Pkt, SendResult) when MgmtState == pending; MgmtState == active; MgmtState == resumed -> IsStanza = xmpp:is_stanza(Pkt), case Pkt of _ when IsStanza -> case need_to_enqueue(State, Pkt) of {true, State1} -> case mgmt_queue_add(State1, Pkt) of #{mgmt_max_queue := exceeded} = State2 -> State3 = State2#{mgmt_resend => false}, Err = xmpp:serr_policy_violation( ?T("Too many unacked stanzas"), Lang), send(State3, Err); State2 when SendResult == ok -> send_rack(State2); State2 -> State2 end; {false, State1} -> State1 end; #stream_error{} -> case MgmtState of resumed -> State; active -> State; pending -> Mod:stop_async(self()), {stop, State#{stop_reason => {stream, {out, Pkt}}}} end; _ -> State end; c2s_handle_send(State, _Pkt, _Result) -> State. c2s_handle_call(#{mgmt_id := MgmtID, mgmt_queue := Queue, mod := Mod} = State, {resume_session, MgmtID}, From) -> State1 = State#{mgmt_queue => p1_queue:file_to_ram(Queue)}, Mod:reply(From, {resume, State1}), {stop, State#{mgmt_state => resumed, mgmt_queue => p1_queue:clear(Queue)}}; c2s_handle_call(#{mod := Mod} = State, {resume_session, _}, From) -> Mod:reply(From, {error, session_not_found}), {stop, State}; c2s_handle_call(State, _Call, _From) -> State. c2s_handle_info(#{mgmt_ack_timer := TRef, jid := JID, mod := Mod} = State, {timeout, TRef, ack_timeout}) -> ?DEBUG("Timed out waiting for stream management acknowledgement of ~ts", [jid:encode(JID)]), State1 = Mod:close(State), State2 = State1#{stop_reason => {socket, ack_timeout}}, {stop, transition_to_pending(State2, ack_timeout)}; c2s_handle_info(#{mgmt_state := pending, lang := Lang, mgmt_pending_timer := TRef, jid := JID, mod := Mod} = State, {timeout, TRef, pending_timeout}) -> ?DEBUG("Timed out waiting for resumption of stream for ~ts", [jid:encode(JID)]), Txt = ?T("Timed out waiting for stream resumption"), Err = xmpp:serr_connection_timeout(Txt, Lang), Mod:stop_async(self()), {stop, State#{mgmt_state => timeout, stop_reason => {stream, {out, Err}}}}; c2s_handle_info(State, {_Ref, {resume, #{jid := JID} = OldState}}) -> %% This happens if the resume_session/1 request timed out; the new session %% now receives the late response. ?DEBUG("Received old session state for ~ts after failed resumption", [jid:encode(JID)]), route_unacked_stanzas(OldState#{mgmt_resend => false}), {stop, State}; c2s_handle_info(State, {timeout, _, Timeout}) when Timeout == ack_timeout; Timeout == pending_timeout -> %% Late arrival of an already cancelled timer: we just ignore it. %% This might happen because misc:cancel_timer/1 doesn't guarantee %% timer cancelation in the case when p1_server is used. {stop, State}; c2s_handle_info(State, _) -> State. c2s_closed(State, {stream, _}) -> State; c2s_closed(#{mgmt_state := active} = State, Reason) -> {stop, transition_to_pending(State, Reason)}; c2s_closed(State, _Reason) -> State. c2s_terminated(#{mgmt_state := resumed, sid := SID, jid := JID} = State, _Reason) -> ?DEBUG("Closing former stream of resumed session for ~ts", [jid:encode(JID)]), {U, S, R} = jid:tolower(JID), ejabberd_sm:close_session(SID, U, S, R), route_late_queue_after_resume(State), ejabberd_c2s:bounce_message_queue(SID, JID), {stop, State}; c2s_terminated(#{mgmt_state := MgmtState, mgmt_stanzas_in := In, mgmt_id := MgmtID, jid := JID} = State, _Reason) -> case MgmtState of timeout -> store_stanzas_in(jid:tolower(JID), MgmtID, In); _ -> ok end, route_unacked_stanzas(State), State; c2s_terminated(State, _Reason) -> State. %%%=================================================================== %%% Adjust pending session timeout / access queue %%%=================================================================== -spec get_resume_timeout(state()) -> non_neg_integer(). get_resume_timeout(#{mgmt_timeout := Timeout}) -> Timeout. -spec set_resume_timeout(state(), non_neg_integer()) -> state(). set_resume_timeout(#{mgmt_timeout := Timeout} = State, Timeout) -> State; set_resume_timeout(State, Timeout) -> State1 = restart_pending_timer(State, Timeout), State1#{mgmt_timeout => Timeout}. -spec queue_find(fun((stanza()) -> boolean()), queue()) -> stanza() | none. queue_find(Pred, Queue) -> case p1_queue:out(Queue) of {{value, {_, _, Pkt}}, Queue1} -> case Pred(Pkt) of true -> Pkt; false -> queue_find(Pred, Queue1) end; {empty, _Queue1} -> none end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec negotiate_stream_mgmt(xmpp_element(), state()) -> state(). negotiate_stream_mgmt(Pkt, #{lang := Lang} = State) -> Xmlns = xmpp:get_ns(Pkt), case Pkt of #sm_enable{} -> handle_enable(State#{mgmt_xmlns => Xmlns}, Pkt); _ when is_record(Pkt, sm_a); is_record(Pkt, sm_r); is_record(Pkt, sm_resume) -> Txt = ?T("Stream management is not enabled"), Err = #sm_failed{reason = 'unexpected-request', text = xmpp:mk_text(Txt, Lang), xmlns = Xmlns}, send(State, Err) end. -spec perform_stream_mgmt(xmpp_element(), state()) -> state(). perform_stream_mgmt(Pkt, #{mgmt_xmlns := Xmlns, lang := Lang} = State) -> case xmpp:get_ns(Pkt) of Xmlns -> case Pkt of #sm_r{} -> handle_r(State); #sm_a{} -> handle_a(State, Pkt); _ when is_record(Pkt, sm_enable); is_record(Pkt, sm_resume) -> Txt = ?T("Stream management is already enabled"), send(State, #sm_failed{reason = 'unexpected-request', text = xmpp:mk_text(Txt, Lang), xmlns = Xmlns}) end; _ -> Txt = ?T("Unsupported version"), send(State, #sm_failed{reason = 'unexpected-request', text = xmpp:mk_text(Txt, Lang), xmlns = Xmlns}) end. -spec handle_enable(state(), sm_enable()) -> state(). handle_enable(#{mgmt_timeout := DefaultTimeout, mgmt_queue_type := QueueType, mgmt_max_timeout := MaxTimeout, mgmt_xmlns := Xmlns, jid := JID} = State, #sm_enable{resume = Resume, max = Max}) -> State1 = State#{mgmt_id => make_id()}, Timeout = if Resume == false -> 0; Max /= undefined, Max > 0, Max*1000 =< MaxTimeout -> Max*1000; true -> DefaultTimeout end, Res = if Timeout > 0 -> ?DEBUG("Stream management with resumption enabled for ~ts", [jid:encode(JID)]), #sm_enabled{xmlns = Xmlns, id = encode_id(State1), resume = true, max = Timeout div 1000}; true -> ?DEBUG("Stream management without resumption enabled for ~ts", [jid:encode(JID)]), #sm_enabled{xmlns = Xmlns} end, State2 = State1#{mgmt_state => active, mgmt_queue => p1_queue:new(QueueType), mgmt_timeout => Timeout}, send(State2, Res). -spec handle_r(state()) -> state(). handle_r(#{mgmt_xmlns := Xmlns, mgmt_stanzas_in := H} = State) -> Res = #sm_a{xmlns = Xmlns, h = H}, send(State, Res). -spec handle_a(state(), sm_a()) -> state(). handle_a(State, #sm_a{h = H}) -> State1 = check_h_attribute(State, H), resend_rack(State1). -spec handle_resume(state(), sm_resume()) -> {ok, state()} | {error, state()}. handle_resume(#{user := User, lserver := LServer, lang := Lang, socket := Socket} = State, #sm_resume{h = H, previd = PrevID, xmlns = Xmlns}) -> R = case inherit_session_state(State, PrevID) of {ok, InheritedState} -> {ok, InheritedState, H}; {error, Err, InH} -> {error, #sm_failed{reason = 'item-not-found', text = xmpp:mk_text(format_error(Err), Lang), h = InH, xmlns = Xmlns}, Err}; {error, Err} -> {error, #sm_failed{reason = 'item-not-found', text = xmpp:mk_text(format_error(Err), Lang), xmlns = Xmlns}, Err} end, case R of {ok, #{jid := JID} = ResumedState, NumHandled} -> State1 = check_h_attribute(ResumedState, NumHandled), #{mgmt_xmlns := AttrXmlns, mgmt_stanzas_in := AttrH} = State1, State2 = send(State1, #sm_resumed{xmlns = AttrXmlns, h = AttrH, previd = PrevID}), State3 = resend_unacked_stanzas(State2), State4 = send(State3, #sm_r{xmlns = AttrXmlns}), State5 = ejabberd_hooks:run_fold(c2s_session_resumed, LServer, State4, []), ?INFO_MSG("(~ts) Resumed session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)]), {ok, State5}; {error, El, Reason} -> log_resumption_error(User, LServer, Reason), {error, send(State, El)} end. -spec transition_to_pending(state(), _) -> state(). transition_to_pending(#{mgmt_state := active, mod := Mod, mgmt_timeout := 0} = State, _Reason) -> Mod:stop_async(self()), State; transition_to_pending(#{mgmt_state := active, jid := JID, socket := Socket, lserver := LServer, mgmt_timeout := Timeout} = State, Reason) -> State1 = cancel_ack_timer(State), ?INFO_MSG("(~ts) Closing c2s connection for ~ts: ~ts; " "waiting ~B seconds for stream resumption", [xmpp_socket:pp(Socket), jid:encode(JID), format_reason(State, Reason), Timeout div 1000]), TRef = erlang:start_timer(Timeout, self(), pending_timeout), State2 = State1#{mgmt_state => pending, mgmt_pending_timer => TRef}, ejabberd_hooks:run_fold(c2s_session_pending, LServer, State2, []); transition_to_pending(State, _Reason) -> State. -spec check_h_attribute(state(), non_neg_integer()) -> state(). check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID, lang := Lang} = State, H) when H > NumStanzasOut -> ?WARNING_MSG("~ts acknowledged ~B stanzas, but only ~B were sent", [jid:encode(JID), H, NumStanzasOut]), State1 = State#{mgmt_resend => false}, Err = xmpp:serr_undefined_condition( ?T("Client acknowledged more stanzas than sent by server"), Lang), send(State1, Err); check_h_attribute(#{mgmt_stanzas_out := NumStanzasOut, jid := JID} = State, H) -> ?DEBUG("~ts acknowledged ~B of ~B stanzas", [jid:encode(JID), H, NumStanzasOut]), mgmt_queue_drop(State, H). -spec update_num_stanzas_in(state(), xmpp_element() | xmlel()) -> state(). update_num_stanzas_in(#{mgmt_state := MgmtState, mgmt_stanzas_in := NumStanzasIn} = State, El) when MgmtState == active; MgmtState == pending -> NewNum = case {xmpp:is_stanza(El), NumStanzasIn} of {true, 4294967295} -> 0; {true, Num} -> Num + 1; {false, Num} -> Num end, State#{mgmt_stanzas_in => NewNum}; update_num_stanzas_in(State, _El) -> State. -spec send_rack(state()) -> state(). send_rack(#{mgmt_ack_timer := _} = State) -> State; send_rack(#{mgmt_xmlns := Xmlns, mgmt_stanzas_out := NumStanzasOut} = State) -> State1 = State#{mgmt_stanzas_req => NumStanzasOut}, State2 = start_ack_timer(State1), send(State2, #sm_r{xmlns = Xmlns}). -spec resend_rack(state()) -> state(). resend_rack(#{mgmt_ack_timer := _, mgmt_queue := Queue, mgmt_stanzas_out := NumStanzasOut, mgmt_stanzas_req := NumStanzasReq} = State) -> State1 = cancel_ack_timer(State), case NumStanzasReq < NumStanzasOut andalso not p1_queue:is_empty(Queue) of true -> send_rack(State1); false -> State1 end; resend_rack(State) -> State. -spec mgmt_queue_add(state(), xmlel() | xmpp_element()) -> state(). mgmt_queue_add(#{mgmt_stanzas_out := NumStanzasOut, mgmt_queue := Queue} = State, Pkt) -> NewNum = case NumStanzasOut of 4294967295 -> 0; Num -> Num + 1 end, Queue1 = p1_queue:in({NewNum, erlang:timestamp(), Pkt}, Queue), State1 = State#{mgmt_queue => Queue1, mgmt_stanzas_out => NewNum}, check_queue_length(State1). -spec mgmt_queue_drop(state(), non_neg_integer()) -> state(). mgmt_queue_drop(#{mgmt_queue := Queue} = State, NumHandled) -> NewQueue = p1_queue:dropwhile( fun({N, _T, _E}) -> N =< NumHandled end, Queue), State#{mgmt_queue => NewQueue}. -spec check_queue_length(state()) -> state(). check_queue_length(#{mgmt_max_queue := Limit} = State) when Limit == infinity; Limit == exceeded -> State; check_queue_length(#{mgmt_queue := Queue, mgmt_max_queue := Limit} = State) -> case p1_queue:len(Queue) > Limit of true -> State#{mgmt_max_queue => exceeded}; false -> State end. -spec route_late_queue_after_resume(state()) -> ok. route_late_queue_after_resume(#{mgmt_queue := Queue, jid := JID}) when ?qlen(Queue) > 0 -> ?DEBUG("Re-routing ~B late queued packets to ~ts", [p1_queue:len(Queue), jid:encode(JID)]), p1_queue:foreach( fun({_, _Time, Pkt}) -> ejabberd_router:route(Pkt) end, Queue); route_late_queue_after_resume(_State) -> ok. -spec resend_unacked_stanzas(state()) -> state(). resend_unacked_stanzas(#{mgmt_state := MgmtState, mgmt_queue := Queue, jid := JID} = State) when (MgmtState == active orelse MgmtState == pending orelse MgmtState == timeout) andalso ?qlen(Queue) > 0 -> ?DEBUG("Resending ~B unacknowledged stanza(s) to ~ts", [p1_queue:len(Queue), jid:encode(JID)]), p1_queue:foldl( fun({_, Time, Pkt}, AccState) -> Pkt1 = add_resent_delay_info(AccState, Pkt, Time), Pkt2 = if ?is_stanza(Pkt1) -> xmpp:put_meta(Pkt1, mgmt_is_resent, true); true -> Pkt1 end, send(AccState, Pkt2) end, State, Queue); resend_unacked_stanzas(State) -> State. -spec route_unacked_stanzas(state()) -> ok. route_unacked_stanzas(#{mgmt_state := MgmtState, mgmt_resend := MgmtResend, lang := Lang, user := User, jid := JID, lserver := LServer, mgmt_queue := Queue, resource := Resource} = State) when (MgmtState == active orelse MgmtState == pending orelse MgmtState == timeout) andalso ?qlen(Queue) > 0 -> ResendOnTimeout = case MgmtResend of Resend when is_boolean(Resend) -> Resend; if_offline -> case ejabberd_sm:get_user_resources(User, LServer) of [Resource] -> %% Same resource opened new session true; [] -> true; _ -> false end end, ?DEBUG("Re-routing ~B unacknowledged stanza(s) to ~ts", [p1_queue:len(Queue), jid:encode(JID)]), ModOfflineEnabled = gen_mod:is_loaded(LServer, mod_offline), p1_queue:foreach( fun({_, _Time, #presence{from = From}}) -> ?DEBUG("Dropping presence stanza from ~ts", [jid:encode(From)]); ({_, _Time, #iq{} = El}) -> Txt = ?T("User session terminated"), ejabberd_router:route_error( El, xmpp:err_service_unavailable(Txt, Lang)); ({_, _Time, #message{from = From, meta = #{carbon_copy := true}}}) -> %% XEP-0280 says: "When a receiving server attempts to deliver a %% forked message, and that message bounces with an error for %% any reason, the receiving server MUST NOT forward that error %% back to the original sender." Resending such a stanza could %% easily lead to unexpected results as well. ?DEBUG("Dropping forwarded message stanza from ~ts", [jid:encode(From)]); ({_, Time, #message{} = Msg}) -> case {ModOfflineEnabled, ResendOnTimeout, xmpp:get_meta(Msg, mam_archived, false)} of Val when Val == {true, true, false}; Val == {true, true, true}; Val == {false, true, false} -> NewEl = add_resent_delay_info(State, Msg, Time), ejabberd_router:route(NewEl); {_, _, true} -> ?DEBUG("Dropping archived message stanza from ~s", [jid:encode(xmpp:get_from(Msg))]); _ -> Txt = ?T("User session terminated"), ejabberd_router:route_error( Msg, xmpp:err_service_unavailable(Txt, Lang)) end; ({_, _Time, El}) -> %% Raw element of type 'error' resulting from a validation error %% We cannot pass it to the router, it will generate an error ?DEBUG("Do not route raw element from ack queue: ~p", [El]) end, Queue); route_unacked_stanzas(_State) -> ok. -spec inherit_session_state(state(), binary()) -> {ok, state()} | {error, error_reason()} | {error, error_reason(), non_neg_integer()}. inherit_session_state(#{user := U, server := S, mgmt_queue_type := QueueType} = State, PrevID) -> case decode_id(PrevID) of {ok, {R, MgmtID}} -> case ejabberd_sm:get_session_sid(U, S, R) of none -> case pop_stanzas_in({U, S, R}, MgmtID) of error -> {error, session_not_found}; {ok, H} -> {error, session_timed_out, H} end; {_, OldPID} = OldSID -> try resume_session(OldPID, MgmtID, State) of {resume, #{mgmt_xmlns := Xmlns, mgmt_queue := Queue, mgmt_timeout := Timeout, mgmt_stanzas_in := NumStanzasIn, mgmt_stanzas_out := NumStanzasOut} = OldState} -> State1 = ejabberd_c2s:copy_state(State, OldState), Queue1 = case QueueType of ram -> Queue; _ -> p1_queue:ram_to_file(Queue) end, State2 = State1#{sid => ejabberd_sm:make_sid(), mgmt_id => MgmtID, mgmt_xmlns => Xmlns, mgmt_queue => Queue1, mgmt_timeout => Timeout, mgmt_stanzas_in => NumStanzasIn, mgmt_stanzas_out => NumStanzasOut, mgmt_state => active}, State3 = ejabberd_c2s:open_session(State2), ejabberd_c2s:stop_async(OldPID), {ok, State3}; {error, Msg} -> {error, Msg} catch exit:{noproc, _} -> {error, session_is_dead}; exit:{normal, _} -> {error, session_has_exited}; exit:{shutdown, _} -> {error, session_has_exited}; exit:{killed, _} -> {error, session_was_killed}; exit:{timeout, _} -> ejabberd_sm:close_session(OldSID, U, S, R), ejabberd_c2s:stop_async(OldPID), {error, session_copy_timed_out} end end; error -> {error, invalid_previd} end. -spec resume_session(pid(), id(), state()) -> {resume, state()} | {error, error_reason()}. resume_session(PID, MgmtID, _State) -> ejabberd_c2s:call(PID, {resume_session, MgmtID}, timer:seconds(15)). -spec add_resent_delay_info(state(), stanza(), erlang:timestamp()) -> stanza(); (state(), xmlel(), erlang:timestamp()) -> xmlel(). add_resent_delay_info(#{lserver := LServer}, El, Time) when is_record(El, message); is_record(El, presence) -> misc:add_delay_info(El, jid:make(LServer), Time, <<"Resent">>); add_resent_delay_info(_State, El, _Time) -> %% TODO El. -spec send(state(), xmpp_element()) -> state(). send(#{mod := Mod} = State, Pkt) -> Mod:send(State, Pkt). -spec restart_pending_timer(state(), non_neg_integer()) -> state(). restart_pending_timer(#{mgmt_pending_timer := TRef} = State, NewTimeout) -> misc:cancel_timer(TRef), NewTRef = erlang:start_timer(NewTimeout, self(), pending_timeout), State#{mgmt_pending_timer => NewTRef}; restart_pending_timer(State, _NewTimeout) -> State. -spec start_ack_timer(state()) -> state(). start_ack_timer(#{mgmt_ack_timeout := infinity} = State) -> State; start_ack_timer(#{mgmt_ack_timeout := AckTimeout} = State) -> TRef = erlang:start_timer(AckTimeout, self(), ack_timeout), State#{mgmt_ack_timer => TRef}. -spec cancel_ack_timer(state()) -> state(). cancel_ack_timer(#{mgmt_ack_timer := TRef} = State) -> misc:cancel_timer(TRef), maps:remove(mgmt_ack_timer, State); cancel_ack_timer(State) -> State. -spec need_to_enqueue(state(), xmlel() | stanza()) -> {boolean(), state()}. need_to_enqueue(State, Pkt) when ?is_stanza(Pkt) -> {not xmpp:get_meta(Pkt, mgmt_is_resent, false), State}; need_to_enqueue(#{mgmt_force_enqueue := true} = State, #xmlel{}) -> State1 = maps:remove(mgmt_force_enqueue, State), State2 = maps:remove(mgmt_is_resent, State1), {true, State2}; need_to_enqueue(State, _) -> {false, State}. -spec make_id() -> id(). make_id() -> p1_rand:bytes(8). -spec encode_id(state()) -> binary(). encode_id(#{mgmt_id := MgmtID, resource := Resource}) -> misc:term_to_base64({Resource, MgmtID}). -spec decode_id(binary()) -> {ok, {binary(), id()}} | error. decode_id(Encoded) -> case misc:base64_to_term(Encoded) of {term, {Resource, MgmtID}} when is_binary(Resource), is_binary(MgmtID) -> {ok, {Resource, MgmtID}}; _ -> error end. %%%=================================================================== %%% Formatters and Logging %%%=================================================================== -spec format_error(error_reason()) -> binary(). format_error(session_not_found) -> ?T("Previous session not found"); format_error(session_timed_out) -> ?T("Previous session timed out"); format_error(session_is_dead) -> ?T("Previous session PID is dead"); format_error(session_has_exited) -> ?T("Previous session PID has exited"); format_error(session_was_killed) -> ?T("Previous session PID has been killed"); format_error(session_copy_timed_out) -> ?T("Session state copying timed out"); format_error(invalid_previd) -> ?T("Invalid 'previd' value"). -spec format_reason(state(), term()) -> binary(). format_reason(_, ack_timeout) -> <<"Timed out waiting for stream acknowledgement">>; format_reason(#{stop_reason := {socket, ack_timeout}} = State, _) -> format_reason(State, ack_timeout); format_reason(State, Reason) -> ejabberd_c2s:format_reason(State, Reason). -spec log_resumption_error(binary(), binary(), error_reason()) -> ok. log_resumption_error(User, Server, Reason) when Reason == invalid_previd -> ?WARNING_MSG("Cannot resume session for ~ts@~ts: ~ts", [User, Server, format_error(Reason)]); log_resumption_error(User, Server, Reason) -> ?INFO_MSG("Cannot resume session for ~ts@~ts: ~ts", [User, Server, format_error(Reason)]). %%%=================================================================== %%% Cache-like storage for last handled stanzas %%%=================================================================== init_cache(Opts) -> ets_cache:new(?STREAM_MGMT_CACHE, cache_opts(Opts)). cache_opts(Opts) -> [{max_size, mod_stream_mgmt_opt:cache_size(Opts)}, {life_time, mod_stream_mgmt_opt:cache_life_time(Opts)}, {type, ordered_set}]. -spec store_stanzas_in(ljid(), id(), non_neg_integer()) -> boolean(). store_stanzas_in(LJID, MgmtID, Num) -> ets_cache:insert(?STREAM_MGMT_CACHE, {LJID, MgmtID}, Num, ejabberd_cluster:get_nodes()). -spec pop_stanzas_in(ljid(), id()) -> {ok, non_neg_integer()} | error. pop_stanzas_in(LJID, MgmtID) -> case ets_cache:lookup(?STREAM_MGMT_CACHE, {LJID, MgmtID}) of {ok, Val} -> ets_cache:match_delete(?STREAM_MGMT_CACHE, {LJID, '_'}, ejabberd_cluster:get_nodes()), {ok, Val}; error -> error end. %%%=================================================================== %%% Configuration processing %%%=================================================================== get_max_ack_queue(Host) -> mod_stream_mgmt_opt:max_ack_queue(Host). get_configured_resume_timeout(Host) -> mod_stream_mgmt_opt:resume_timeout(Host). get_max_resume_timeout(Host, ResumeTimeout) -> case mod_stream_mgmt_opt:max_resume_timeout(Host) of undefined -> ResumeTimeout; Max when Max >= ResumeTimeout -> Max; _ -> ResumeTimeout end. get_ack_timeout(Host) -> mod_stream_mgmt_opt:ack_timeout(Host). get_resend_on_timeout(Host) -> mod_stream_mgmt_opt:resend_on_timeout(Host). get_queue_type(Host) -> mod_stream_mgmt_opt:queue_type(Host). mod_opt_type(max_ack_queue) -> econf:pos_int(infinity); mod_opt_type(resume_timeout) -> econf:either( econf:int(0, 0), econf:timeout(second)); mod_opt_type(max_resume_timeout) -> econf:either( econf:int(0, 0), econf:timeout(second)); mod_opt_type(ack_timeout) -> econf:timeout(second, infinity); mod_opt_type(resend_on_timeout) -> econf:either( if_offline, econf:bool()); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity); mod_opt_type(queue_type) -> econf:queue_type(). mod_options(Host) -> [{max_ack_queue, 5000}, {resume_timeout, timer:seconds(300)}, {max_resume_timeout, undefined}, {ack_timeout, timer:seconds(60)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_life_time, timer:hours(48)}, {resend_on_timeout, false}, {queue_type, ejabberd_option:queue_type(Host)}]. mod_doc() -> #{desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0198.html" "[XEP-0198: Stream Management]. This protocol allows " "active management of an XML stream between two XMPP " "entities, including features for stanza acknowledgements " "and stream resumption."), opts => [{max_ack_queue, #{value => ?T("Size"), desc => ?T("This option specifies the maximum number of " "unacknowledged stanzas queued for possible " "retransmission. When the limit is exceeded, " "the client session is terminated. The allowed " "values are positive integers and 'infinity'. " "You should be careful when setting this value " "as it should not be set too low, otherwise, " "you could kill sessions in a loop, before they " "get the chance to finish proper session initiation. " "It should definitely be set higher that the size " "of the offline queue (for example at least 3 times " "the value of the max offline queue and never lower " "than '1000'). The default value is '5000'.")}}, {resume_timeout, #{value => "timeout()", desc => ?T("This option configures the (default) period of time " "until a session times out if the connection is lost. " "During this period of time, a client may resume its " "session. Note that the client may request a different " "timeout value, see the 'max_resume_timeout' option. " "Setting it to '0' effectively disables session resumption. " "The default value is '5' minutes.")}}, {max_resume_timeout, #{value => "timeout()", desc => ?T("A client may specify the period of time until a session " "times out if the connection is lost. During this period " "of time, the client may resume its session. This option " "limits the period of time a client is permitted to request. " "It must be set to a timeout equal to or larger than the " "default 'resume_timeout'. By default, it is set to the " "same value as the 'resume_timeout' option.")}}, {ack_timeout, #{value => "timeout()", desc => ?T("A time to wait for stanza acknowledgements. " "Setting it to 'infinity' effectively disables the timeout. " "The default value is '1' minute.")}}, {resend_on_timeout, #{value => "true | false | if_offline", desc => ?T("If this option is set to 'true', any message stanzas " "that weren't acknowledged by the client will be resent " "on session timeout. This behavior might often be desired, " "but could have unexpected results under certain circumstances. " "For example, a message that was sent to two resources might " "get resent to one of them if the other one timed out. " "Therefore, the default value for this option is 'false', " "which tells ejabberd to generate an error message instead. " "As an alternative, the option may be set to 'if_offline'. " "In this case, unacknowledged messages are resent only if " "no other resource is online when the session times out. " "Otherwise, error messages are generated.")}}, {queue_type, #{value => "ram | file", desc => ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, " "but applied to this module only. " "The default value is '48 hours'.")}}]}. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_push_keepalive.erl�����������������������������������������������������������0000644�0002322�0002322�00000025322�14154362354�020774� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : mod_push_keepalive.erl %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Purpose : Keep pending XEP-0198 sessions alive with XEP-0357 %%% Created : 15 Jul 2017 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_push_keepalive). -author('holger@zedat.fu-berlin.de'). -behaviour(gen_mod). %% gen_mod callbacks. -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. -export([c2s_session_pending/1, c2s_session_resumed/1, c2s_copy_session/2, c2s_handle_cast/2, c2s_handle_info/2, c2s_stanza/3]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(PUSH_BEFORE_TIMEOUT_PERIOD, 120000). % 2 minutes. -type c2s_state() :: ejabberd_c2s:state(). %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> ok. start(Host, Opts) -> case mod_push_keepalive_opt:wake_on_start(Opts) of true -> wake_all(Host); false -> ok end, register_hooks(Host). -spec stop(binary()) -> ok. stop(Host) -> unregister_hooks(Host). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> case {mod_push_keepalive_opt:wake_on_start(NewOpts), mod_push_keepalive_opt:wake_on_start(OldOpts)} of {true, false} -> wake_all(Host); _ -> ok end. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> [{mod_push, hard}, {mod_client_state, soft}, {mod_stream_mgmt, soft}]. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(resume_timeout) -> econf:either( econf:int(0, 0), econf:timeout(second)); mod_opt_type(wake_on_start) -> econf:bool(); mod_opt_type(wake_on_timeout) -> econf:bool(). mod_options(_Host) -> [{resume_timeout, timer:hours(72)}, {wake_on_start, false}, {wake_on_timeout, true}]. mod_doc() -> #{desc => [?T("This module tries to keep the stream management " "session (see _`mod_stream_mgmt`_) of a disconnected " "mobile client alive if the client enabled push " "notifications for that session. However, the normal " "session resumption timeout is restored once a push " "notification is issued, so the session will be closed " "if the client doesn't respond to push notifications."), "", ?T("The module depends on _`mod_push`_.")], opts => [{resume_timeout, #{value => "timeout()", desc => ?T("This option specifies the period of time until " "the session of a disconnected push client times out. " "This timeout is only in effect as long as no push " "notification is issued. Once that happened, the " "resumption timeout configured for _`mod_stream_mgmt`_ " "is restored. " "The default value is '72' hours.")}}, {wake_on_start, #{value => "true | false", desc => ?T("If this option is set to 'true', notifications " "are generated for **all** registered push clients " "during server startup. This option should not be " "enabled on servers with many push clients as it " "can generate significant load on the involved push " "services and the server itself. " "The default value is 'false'.")}}, {wake_on_timeout, #{value => "true | false", desc => ?T("If this option is set to 'true', a notification " "is generated shortly before the session would time " "out as per the 'resume_timeout' option. " "The default value is 'true'.")}}]}. %%-------------------------------------------------------------------- %% Register/unregister hooks. %%-------------------------------------------------------------------- -spec register_hooks(binary()) -> ok. register_hooks(Host) -> ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, c2s_session_pending, 50), ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 40), ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, c2s_stanza, 50). -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, c2s_session_pending, 50), ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 40), ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, c2s_stanza, 50). %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- -spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, Pkt, _SendResult) -> case mod_push:is_incoming_chat_msg(Pkt) of true -> maybe_restore_resume_timeout(State); false -> State end; c2s_stanza(State, _Pkt, _SendResult) -> State. -spec c2s_session_pending(c2s_state()) -> c2s_state(). c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> case mod_stream_mgmt:queue_find(fun mod_push:is_incoming_chat_msg/1, Queue) of none -> State1 = maybe_adjust_resume_timeout(State), maybe_start_wakeup_timer(State1); _Msg -> State end; c2s_session_pending(State) -> State. -spec c2s_session_resumed(c2s_state()) -> c2s_state(). c2s_session_resumed(#{push_enabled := true} = State) -> maybe_restore_resume_timeout(State); c2s_session_resumed(State) -> State. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{push_enabled := true, push_resume_timeout := ResumeTimeout, push_wake_on_timeout := WakeOnTimeout} = OldState) -> State1 = case maps:find(push_resume_timeout_orig, OldState) of {ok, Val} -> State#{push_resume_timeout_orig => Val}; error -> State end, State1#{push_resume_timeout => ResumeTimeout, push_wake_on_timeout => WakeOnTimeout}; c2s_copy_session(State, _) -> State. -spec c2s_handle_cast(c2s_state(), any()) -> c2s_state(). c2s_handle_cast(#{lserver := LServer} = State, push_enable) -> ResumeTimeout = mod_push_keepalive_opt:resume_timeout(LServer), WakeOnTimeout = mod_push_keepalive_opt:wake_on_timeout(LServer), State#{push_resume_timeout => ResumeTimeout, push_wake_on_timeout => WakeOnTimeout}; c2s_handle_cast(State, push_disable) -> State1 = maps:remove(push_resume_timeout, State), maps:remove(push_wake_on_timeout, State1); c2s_handle_cast(State, _Msg) -> State. -spec c2s_handle_info(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. c2s_handle_info(#{push_enabled := true, mgmt_state := pending, jid := JID} = State, {timeout, _, push_keepalive}) -> ?INFO_MSG("Waking ~ts before session times out", [jid:encode(JID)]), mod_push:notify(State, none, undefined), {stop, State}; c2s_handle_info(State, _) -> State. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec maybe_adjust_resume_timeout(c2s_state()) -> c2s_state(). maybe_adjust_resume_timeout(#{push_resume_timeout := undefined} = State) -> State; maybe_adjust_resume_timeout(#{push_resume_timeout := Timeout} = State) -> OrigTimeout = mod_stream_mgmt:get_resume_timeout(State), ?DEBUG("Adjusting resume timeout to ~B seconds", [Timeout div 1000]), State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), State1#{push_resume_timeout_orig => OrigTimeout}. -spec maybe_restore_resume_timeout(c2s_state()) -> c2s_state(). maybe_restore_resume_timeout(#{push_resume_timeout_orig := Timeout} = State) -> ?DEBUG("Restoring resume timeout to ~B seconds", [Timeout div 1000]), State1 = mod_stream_mgmt:set_resume_timeout(State, Timeout), maps:remove(push_resume_timeout_orig, State1); maybe_restore_resume_timeout(State) -> State. -spec maybe_start_wakeup_timer(c2s_state()) -> c2s_state(). maybe_start_wakeup_timer(#{push_wake_on_timeout := true, push_resume_timeout := ResumeTimeout} = State) when is_integer(ResumeTimeout), ResumeTimeout > ?PUSH_BEFORE_TIMEOUT_PERIOD -> WakeTimeout = ResumeTimeout - ?PUSH_BEFORE_TIMEOUT_PERIOD, ?DEBUG("Scheduling wake-up timer to fire in ~B seconds", [WakeTimeout div 1000]), erlang:start_timer(WakeTimeout, self(), push_keepalive), State; maybe_start_wakeup_timer(State) -> State. -spec wake_all(binary()) -> ok. wake_all(LServer) -> ?INFO_MSG("Waking all push clients on ~ts", [LServer]), Mod = gen_mod:db_mod(LServer, mod_push), case Mod:lookup_sessions(LServer) of {ok, Sessions} -> IgnoreResponse = fun(_) -> ok end, lists:foreach(fun({_, PushLJID, Node, XData}) -> mod_push:notify(LServer, PushLJID, Node, XData, none, undefined, IgnoreResponse) end, Sessions); error -> ok end. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_sm.erl������������������������������������������������������������������0000644�0002322�0002322�00000103312�14154362354�017362� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_sm.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Session manager %%% Created : 24 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sm). -author('alexey@process-one.net'). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). %% API -export([start_link/0, stop/0, route/1, route/2, open_session/5, open_session/6, close_session/4, check_in_subscription/2, bounce_offline_message/1, bounce_sm_packet/1, disconnect_removed_user/2, get_user_resources/2, get_user_present_resources/2, set_presence/6, unset_presence/5, close_session_unset_presence/5, dirty_get_sessions_list/0, dirty_get_my_sessions_list/0, get_vh_session_list/1, get_vh_session_number/1, get_vh_by_backend/1, force_update_presence/1, connected_users/0, connected_users_number/0, user_resources/2, kick_user/2, kick_user/3, get_session_pid/3, get_session_sid/3, get_session_sids/2, get_session_sids/3, get_user_info/2, get_user_info/3, set_user_info/5, del_user_info/4, get_user_ip/3, get_max_user_sessions/2, get_all_pids/0, is_existing_resource/3, get_commands_spec/0, c2s_handle_info/2, host_up/1, host_down/1, make_sid/0, clean_cache/1, config_reloaded/0 ]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). -include("ejabberd_sm.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). -callback init() -> ok | {error, any()}. -callback set_session(#session{}) -> ok | {error, any()}. -callback delete_session(#session{}) -> ok | {error, any()}. -callback get_sessions() -> [#session{}]. -callback get_sessions(binary()) -> [#session{}]. -callback get_sessions(binary(), binary()) -> {ok, [#session{}]} | {error, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). -record(state, {}). %% default value for the maximum number of user connections -define(MAX_USER_SESSIONS, infinity). %%==================================================================== %% API %%==================================================================== -export_type([sid/0, info/0]). start_link() -> ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). -spec stop() -> ok. stop() -> _ = supervisor:terminate_child(ejabberd_sup, ?MODULE), _ = supervisor:delete_child(ejabberd_sup, ?MODULE), _ = supervisor:terminate_child(ejabberd_sup, ejabberd_c2s_sup), _ = supervisor:delete_child(ejabberd_sup, ejabberd_c2s_sup), ok. -spec route(jid(), term()) -> ok. %% @doc route arbitrary term to c2s process(es) route(To, Term) -> try do_route(To, Term), ok catch ?EX_RULE(E, R, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route term to ~ts:~n" "** Term = ~p~n" "** ~ts", [jid:encode(To), Term, misc:format_exception(2, E, R, StackTrace)]) end. -spec route(stanza()) -> ok. route(Packet) -> #jid{lserver = LServer} = xmpp:get_to(Packet), case ejabberd_hooks:run_fold(sm_receive_packet, LServer, Packet, []) of drop -> ?DEBUG("Hook dropped stanza:~n~ts", [xmpp:pp(Packet)]); Packet1 -> do_route(Packet1), ok end. -spec open_session(sid(), binary(), binary(), binary(), prio(), info()) -> ok. open_session(SID, User, Server, Resource, Priority, Info) -> set_session(SID, User, Server, Resource, Priority, Info), check_for_sessions_to_replace(User, Server, Resource), JID = jid:make(User, Server, Resource), ejabberd_hooks:run(sm_register_connection_hook, JID#jid.lserver, [SID, JID, Info]). -spec open_session(sid(), binary(), binary(), binary(), info()) -> ok. open_session(SID, User, Server, Resource, Info) -> open_session(SID, User, Server, Resource, undefined, Info). -spec close_session(sid(), binary(), binary(), binary()) -> ok. close_session(SID, User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), Sessions = get_sessions(Mod, LUser, LServer, LResource), Info = case lists:keyfind(SID, #session.sid, Sessions) of #session{info = I} = Session -> delete_session(Mod, Session), I; _ -> [] end, JID = jid:make(User, Server, Resource), ejabberd_hooks:run(sm_remove_connection_hook, JID#jid.lserver, [SID, JID, Info]). -spec check_in_subscription(boolean(), presence()) -> boolean() | {stop, false}. check_in_subscription(Acc, #presence{to = To}) -> #jid{user = User, server = Server} = To, case ejabberd_auth:user_exists(User, Server) of true -> Acc; false -> {stop, false} end. -spec bounce_offline_message({bounce, message()} | any()) -> any(). bounce_offline_message({bounce, #message{type = T}} = Acc) when T == chat; T == groupchat; T == normal -> bounce_sm_packet(Acc); bounce_offline_message(Acc) -> Acc. -spec bounce_sm_packet({bounce | term(), stanza()}) -> any(). bounce_sm_packet({bounce, Packet} = Acc) -> Lang = xmpp:get_lang(Packet), Txt = ?T("User session not found"), Err = xmpp:err_service_unavailable(Txt, Lang), ejabberd_router:route_error(Packet, Err), {stop, Acc}; bounce_sm_packet({_, Packet} = Acc) -> ?DEBUG("Dropping packet to unavailable resource:~n~ts", [xmpp:pp(Packet)]), Acc. -spec disconnect_removed_user(binary(), binary()) -> ok. disconnect_removed_user(User, Server) -> route(jid:make(User, Server), {exit, ?T("User removed")}). get_user_resources(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer), [element(3, S#session.usr) || S <- clean_session_list(Ss)]. -spec get_user_present_resources(binary(), binary()) -> [tuple()]. get_user_present_resources(LUser, LServer) -> Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer), [{S#session.priority, element(3, S#session.usr)} || S <- clean_session_list(Ss), is_integer(S#session.priority)]. -spec get_user_ip(binary(), binary(), binary()) -> ip(). get_user_ip(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> undefined; Ss -> Session = lists:max(Ss), proplists:get_value(ip, Session#session.info) end. -spec get_user_info(binary(), binary()) -> [{binary(), info()}]. get_user_info(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer), [{LResource, [{node, node(Pid)}, {ts, Ts}, {pid, Pid}, {priority, Priority} | Info]} || #session{usr = {_, _, LResource}, priority = Priority, info = Info, sid = {Ts, Pid}} <- clean_session_list(Ss)]. -spec get_user_info(binary(), binary(), binary()) -> info() | offline. get_user_info(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> offline; Ss -> Session = lists:max(Ss), {Ts, Pid} = Session#session.sid, Node = node(Pid), Priority = Session#session.priority, [{node, Node}, {ts, Ts}, {pid, Pid}, {priority, Priority} |Session#session.info] end. -spec set_user_info(binary(), binary(), binary(), atom(), term()) -> ok | {error, any()}. set_user_info(User, Server, Resource, Key, Val) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> {error, notfound}; Ss -> lists:foldl( fun(#session{sid = {_, Pid}, info = Info} = Session, _) when Pid == self() -> Info1 = lists:keystore(Key, 1, Info, {Key, Val}), set_session(Session#session{info = Info1}); (_, Acc) -> Acc end, {error, not_owner}, Ss) end. -spec del_user_info(binary(), binary(), binary(), atom()) -> ok | {error, any()}. del_user_info(User, Server, Resource, Key) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> {error, notfound}; Ss -> lists:foldl( fun(#session{sid = {_, Pid}, info = Info} = Session, _) when Pid == self() -> Info1 = lists:keydelete(Key, 1, Info), set_session(Session#session{info = Info1}); (_, Acc) -> Acc end, {error, not_owner}, Ss) end. -spec set_presence(sid(), binary(), binary(), binary(), prio(), presence()) -> ok | {error, notfound}. set_presence(SID, User, Server, Resource, Priority, Presence) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> {error, notfound}; Ss -> case lists:keyfind(SID, #session.sid, Ss) of #session{info = Info} -> set_session(SID, User, Server, Resource, Priority, Info), ejabberd_hooks:run(set_presence_hook, LServer, [User, Server, Resource, Presence]); false -> {error, notfound} end end. -spec unset_presence(sid(), binary(), binary(), binary(), binary()) -> ok | {error, notfound}. unset_presence(SID, User, Server, Resource, Status) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> {error, notfound}; Ss -> case lists:keyfind(SID, #session.sid, Ss) of #session{info = Info} -> set_session(SID, User, Server, Resource, undefined, Info), ejabberd_hooks:run(unset_presence_hook, LServer, [User, Server, Resource, Status]); false -> {error, notfound} end end. -spec close_session_unset_presence(sid(), binary(), binary(), binary(), binary()) -> ok. close_session_unset_presence(SID, User, Server, Resource, Status) -> close_session(SID, User, Server, Resource), ejabberd_hooks:run(unset_presence_hook, jid:nameprep(Server), [User, Server, Resource, Status]). -spec get_session_pid(binary(), binary(), binary()) -> none | pid(). get_session_pid(User, Server, Resource) -> case get_session_sid(User, Server, Resource) of {_, PID} -> PID; none -> none end. -spec get_session_sid(binary(), binary(), binary()) -> none | sid(). get_session_sid(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> none; Ss -> #session{sid = SID} = lists:max(Ss), SID end. -spec get_session_sids(binary(), binary()) -> [sid()]. get_session_sids(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = get_sm_backend(LServer), Sessions = get_sessions(Mod, LUser, LServer), [SID || #session{sid = SID} <- Sessions]. -spec get_session_sids(binary(), binary(), binary()) -> [sid()]. get_session_sids(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), Sessions = get_sessions(Mod, LUser, LServer, LResource), [SID || #session{sid = SID} <- Sessions]. -spec dirty_get_sessions_list() -> [ljid()]. dirty_get_sessions_list() -> lists:flatmap( fun(Mod) -> [S#session.usr || S <- get_sessions(Mod)] end, get_sm_backends()). -spec dirty_get_my_sessions_list() -> [#session{}]. dirty_get_my_sessions_list() -> lists:flatmap( fun(Mod) -> [S || S <- get_sessions(Mod), node(element(2, S#session.sid)) == node()] end, get_sm_backends()). -spec get_vh_session_list(binary()) -> [ljid()]. get_vh_session_list(Server) -> LServer = jid:nameprep(Server), Mod = get_sm_backend(LServer), [S#session.usr || S <- get_sessions(Mod, LServer)]. -spec get_all_pids() -> [pid()]. get_all_pids() -> lists:flatmap( fun(Mod) -> [element(2, S#session.sid) || S <- get_sessions(Mod)] end, get_sm_backends()). -spec get_vh_session_number(binary()) -> non_neg_integer(). get_vh_session_number(Server) -> LServer = jid:nameprep(Server), Mod = get_sm_backend(LServer), length(get_sessions(Mod, LServer)). c2s_handle_info(#{lang := Lang} = State, replaced) -> State1 = State#{replaced => true}, Err = xmpp:serr_conflict(?T("Replaced by new connection"), Lang), {stop, ejabberd_c2s:send(State1, Err)}; c2s_handle_info(#{lang := Lang} = State, kick) -> Err = xmpp:serr_policy_violation(?T("has been kicked"), Lang), {stop, ejabberd_c2s:send(State, Err)}; c2s_handle_info(#{lang := Lang} = State, {exit, Reason}) -> Err = xmpp:serr_conflict(Reason, Lang), {stop, ejabberd_c2s:send(State, Err)}; c2s_handle_info(State, _) -> State. -spec config_reloaded() -> ok. config_reloaded() -> init_cache(). %%==================================================================== %% gen_server callbacks %%==================================================================== init([]) -> process_flag(trap_exit, true), init_cache(), case lists:foldl( fun(Mod, ok) -> Mod:init(); (_, Err) -> Err end, ok, get_sm_backends()) of ok -> clean_cache(), gen_iq_handler:start(?MODULE), ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), ejabberd_hooks:add(host_down, ?MODULE, host_down, 60), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), lists:foreach(fun host_up/1, ejabberd_option:hosts()), ejabberd_commands:register_commands(get_commands_spec()), {ok, #state{}}; {error, Why} -> {stop, Why} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> lists:foreach(fun host_down/1, ejabberd_option:hosts()), ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 60), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50), ejabberd_commands:unregister_commands(get_commands_spec()), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_hooks:add(c2s_handle_info, Host, ejabberd_sm, c2s_handle_info, 50), ejabberd_hooks:add(roster_in_subscription, Host, ejabberd_sm, check_in_subscription, 20), ejabberd_hooks:add(offline_message_hook, Host, ejabberd_sm, bounce_offline_message, 100), ejabberd_hooks:add(bounce_sm_packet, Host, ejabberd_sm, bounce_sm_packet, 100), ejabberd_hooks:add(remove_user, Host, ejabberd_sm, disconnect_removed_user, 100), ejabberd_c2s:host_up(Host). -spec host_down(binary()) -> ok. host_down(Host) -> Mod = get_sm_backend(Host), Err = case ejabberd_cluster:get_nodes() of [Node] when Node == node() -> xmpp:serr_system_shutdown(); _ -> xmpp:serr_reset() end, lists:foreach( fun(#session{sid = {_, Pid}}) when node(Pid) == node() -> ejabberd_c2s:send(Pid, Err), ejabberd_c2s:stop_async(Pid); (_) -> ok end, get_sessions(Mod, Host)), ejabberd_hooks:delete(c2s_handle_info, Host, ejabberd_sm, c2s_handle_info, 50), ejabberd_hooks:delete(roster_in_subscription, Host, ejabberd_sm, check_in_subscription, 20), ejabberd_hooks:delete(offline_message_hook, Host, ejabberd_sm, bounce_offline_message, 100), ejabberd_hooks:delete(bounce_sm_packet, Host, ejabberd_sm, bounce_sm_packet, 100), ejabberd_hooks:delete(remove_user, Host, ejabberd_sm, disconnect_removed_user, 100), ejabberd_c2s:host_down(Host). -spec set_session(sid(), binary(), binary(), binary(), prio(), info()) -> ok | {error, any()}. set_session(SID, User, Server, Resource, Priority, Info) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), US = {LUser, LServer}, USR = {LUser, LServer, LResource}, set_session(#session{sid = SID, usr = USR, us = US, priority = Priority, info = Info}). -spec set_session(#session{}) -> ok | {error, any()}. set_session(#session{us = {LUser, LServer}} = Session) -> Mod = get_sm_backend(LServer), case Mod:set_session(Session) of ok -> case use_cache(Mod, LServer) of true -> ets_cache:delete(?SM_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end; {error, _} = Err -> Err end. -spec get_sessions(module()) -> [#session{}]. get_sessions(Mod) -> delete_dead(Mod, Mod:get_sessions()). -spec get_sessions(module(), binary()) -> [#session{}]. get_sessions(Mod, LServer) -> delete_dead(Mod, Mod:get_sessions(LServer)). -spec get_sessions(module(), binary(), binary()) -> [#session{}]. get_sessions(Mod, LUser, LServer) -> case use_cache(Mod, LServer) of true -> case ets_cache:lookup( ?SM_CACHE, {LUser, LServer}, fun() -> case Mod:get_sessions(LUser, LServer) of {ok, Ss} when Ss /= [] -> {ok, Ss}; _ -> error end end) of {ok, Sessions} -> delete_dead(Mod, Sessions); error -> [] end; false -> case Mod:get_sessions(LUser, LServer) of {ok, Ss} -> delete_dead(Mod, Ss); _ -> [] end end. -spec get_sessions(module(), binary(), binary(), binary()) -> [#session{}]. get_sessions(Mod, LUser, LServer, LResource) -> Sessions = get_sessions(Mod, LUser, LServer), [S || S <- Sessions, element(3, S#session.usr) == LResource]. -spec delete_session(module(), #session{}) -> ok. delete_session(Mod, #session{usr = {LUser, LServer, _}} = Session) -> Mod:delete_session(Session), case use_cache(Mod, LServer) of true -> ets_cache:delete(?SM_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end. -spec delete_dead(module(), [#session{}]) -> [#session{}]. delete_dead(Mod, Sessions) -> lists:filter( fun(#session{sid = {_, Pid}} = Session) when node(Pid) == node() -> case is_process_alive(Pid) of true -> true; false -> delete_session(Mod, Session), false end; (_) -> true end, Sessions). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec do_route(jid(), term()) -> any(). do_route(#jid{lresource = <<"">>} = To, Term) -> lists:foreach( fun(R) -> do_route(jid:replace_resource(To, R), Term) end, get_user_resources(To#jid.user, To#jid.server)); do_route(To, Term) -> ?DEBUG("Broadcasting ~p to ~ts", [Term, jid:encode(To)]), {U, S, R} = jid:tolower(To), Mod = get_sm_backend(S), case get_sessions(Mod, U, S, R) of [] -> ?DEBUG("Dropping broadcast to unavailable resourse: ~p", [Term]); Ss -> Session = lists:max(Ss), Pid = element(2, Session#session.sid), ?DEBUG("Sending to process ~p: ~p", [Pid, Term]), ejabberd_c2s:route(Pid, Term) end. -spec do_route(stanza()) -> any(). do_route(#presence{to = To, type = T} = Packet) when T == subscribe; T == subscribed; T == unsubscribe; T == unsubscribed -> ?DEBUG("Processing subscription:~n~ts", [xmpp:pp(Packet)]), #jid{luser = LUser, lserver = LServer} = To, case is_privacy_allow(Packet) andalso ejabberd_hooks:run_fold( roster_in_subscription, LServer, false, [Packet]) of true -> Mod = get_sm_backend(LServer), lists:foreach( fun(#session{sid = SID, usr = {_, _, R}, priority = Prio}) when is_integer(Prio) -> Pid = element(2, SID), Packet1 = Packet#presence{to = jid:replace_resource(To, R)}, ?DEBUG("Sending to process ~p:~n~ts", [Pid, xmpp:pp(Packet1)]), ejabberd_c2s:route(Pid, {route, Packet1}); (_) -> ok end, get_sessions(Mod, LUser, LServer)); false -> ok end; do_route(#presence{to = #jid{lresource = <<"">>} = To} = Packet) -> ?DEBUG("Processing presence to bare JID:~n~ts", [xmpp:pp(Packet)]), {LUser, LServer, _} = jid:tolower(To), lists:foreach( fun({_, R}) -> do_route(Packet#presence{to = jid:replace_resource(To, R)}) end, get_user_present_resources(LUser, LServer)); do_route(#message{to = #jid{lresource = <<"">>} = To, type = T} = Packet) -> ?DEBUG("Processing message to bare JID:~n~ts", [xmpp:pp(Packet)]), if T == chat; T == headline; T == normal -> route_message(Packet); true -> ejabberd_hooks:run_fold(bounce_sm_packet, To#jid.lserver, {bounce, Packet}, []) end; do_route(#iq{to = #jid{lresource = <<"">>} = To, type = T} = Packet) -> if T == set; T == get -> ?DEBUG("Processing IQ to bare JID:~n~ts", [xmpp:pp(Packet)]), gen_iq_handler:handle(?MODULE, Packet); true -> ejabberd_hooks:run_fold(bounce_sm_packet, To#jid.lserver, {pass, Packet}, []) end; do_route(Packet) -> ?DEBUG("Processing packet to full JID:~n~ts", [xmpp:pp(Packet)]), To = xmpp:get_to(Packet), {LUser, LServer, LResource} = jid:tolower(To), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> case Packet of #message{type = T} when T == chat; T == normal -> route_message(Packet); #message{type = T} when T == headline -> ejabberd_hooks:run_fold(bounce_sm_packet, LServer, {pass, Packet}, []); #presence{} -> ejabberd_hooks:run_fold(bounce_sm_packet, LServer, {pass, Packet}, []); _ -> ejabberd_hooks:run_fold(bounce_sm_packet, LServer, {bounce, Packet}, []) end; Ss -> Session = lists:max(Ss), Pid = element(2, Session#session.sid), ?DEBUG("Sending to process ~p:~n~ts", [Pid, xmpp:pp(Packet)]), ejabberd_c2s:route(Pid, {route, Packet}) end. %% The default list applies to the user as a whole, %% and is processed if there is no active list set %% for the target session/resource to which a stanza is addressed, %% or if there are no current sessions for the user. -spec is_privacy_allow(stanza()) -> boolean(). is_privacy_allow(Packet) -> To = xmpp:get_to(Packet), LServer = To#jid.server, allow == ejabberd_hooks:run_fold( privacy_check_packet, LServer, allow, [To, Packet, in]). -spec route_message(message()) -> any(). route_message(#message{to = To, type = Type} = Packet) -> LUser = To#jid.luser, LServer = To#jid.lserver, PrioRes = get_user_present_resources(LUser, LServer), case catch lists:max(PrioRes) of {MaxPrio, MaxRes} when is_integer(MaxPrio), MaxPrio >= 0 -> lists:foreach(fun ({P, R}) when P == MaxPrio; (P >= 0) and (Type == headline) -> LResource = jid:resourceprep(R), Mod = get_sm_backend(LServer), case get_sessions(Mod, LUser, LServer, LResource) of [] -> ok; % Race condition Ss -> Session = lists:max(Ss), Pid = element(2, Session#session.sid), ?DEBUG("Sending to process ~p~n", [Pid]), LMaxRes = jid:resourceprep(MaxRes), Packet1 = maybe_mark_as_copy(Packet, LResource, LMaxRes, P, MaxPrio), ejabberd_c2s:route(Pid, {route, Packet1}) end; %% Ignore other priority: ({_Prio, _Res}) -> ok end, PrioRes); _ -> case ejabberd_auth:user_exists(LUser, LServer) andalso is_privacy_allow(Packet) of true -> ejabberd_hooks:run_fold(offline_message_hook, LServer, {bounce, Packet}, []); false -> Err = xmpp:err_service_unavailable(), ejabberd_router:route_error(Packet, Err) end end. -spec maybe_mark_as_copy(message(), binary(), binary(), integer(), integer()) -> message(). maybe_mark_as_copy(Packet, R, R, P, P) -> Packet; maybe_mark_as_copy(Packet, _, _, P, P) -> xmpp:put_meta(Packet, sm_copy, true); maybe_mark_as_copy(Packet, _, _, _, _) -> Packet. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec clean_session_list([#session{}]) -> [#session{}]. clean_session_list(Ss) -> clean_session_list(lists:keysort(#session.usr, Ss), []). -spec clean_session_list([#session{}], [#session{}]) -> [#session{}]. clean_session_list([], Res) -> Res; clean_session_list([S], Res) -> [S | Res]; clean_session_list([S1, S2 | Rest], Res) -> if S1#session.usr == S2#session.usr -> if S1#session.sid > S2#session.sid -> clean_session_list([S1 | Rest], Res); true -> clean_session_list([S2 | Rest], Res) end; true -> clean_session_list([S2 | Rest], [S1 | Res]) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% On new session, check if some existing connections need to be replace -spec check_for_sessions_to_replace(binary(), binary(), binary()) -> ok | replaced. check_for_sessions_to_replace(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), check_existing_resources(LUser, LServer, LResource), check_max_sessions(LUser, LServer). -spec check_existing_resources(binary(), binary(), binary()) -> ok. check_existing_resources(LUser, LServer, LResource) -> Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer, LResource), if Ss == [] -> ok; true -> SIDs = [SID || #session{sid = SID} <- Ss], MaxSID = lists:max(SIDs), lists:foreach(fun ({_, Pid} = S) when S /= MaxSID -> ejabberd_c2s:route(Pid, replaced); (_) -> ok end, SIDs) end. -spec is_existing_resource(binary(), binary(), binary()) -> boolean(). is_existing_resource(LUser, LServer, LResource) -> [] /= get_resource_sessions(LUser, LServer, LResource). -spec get_resource_sessions(binary(), binary(), binary()) -> [sid()]. get_resource_sessions(User, Server, Resource) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LResource = jid:resourceprep(Resource), Mod = get_sm_backend(LServer), [S#session.sid || S <- get_sessions(Mod, LUser, LServer, LResource)]. -spec check_max_sessions(binary(), binary()) -> ok | replaced. check_max_sessions(LUser, LServer) -> Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer), MaxSessions = get_max_user_sessions(LUser, LServer), if length(Ss) =< MaxSessions -> ok; true -> #session{sid = {_, Pid}} = lists:min(Ss), ejabberd_c2s:route(Pid, replaced) end. %% Get the user_max_session setting %% This option defines the max number of time a given users are allowed to %% log in %% Defaults to infinity -spec get_max_user_sessions(binary(), binary()) -> infinity | non_neg_integer(). get_max_user_sessions(LUser, Host) -> case ejabberd_shaper:match(Host, max_user_sessions, jid:make(LUser, Host)) of Max when is_integer(Max) -> Max; infinity -> infinity; _ -> ?MAX_USER_SESSIONS end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec force_update_presence({binary(), binary()}) -> ok. force_update_presence({LUser, LServer}) -> Mod = get_sm_backend(LServer), Ss = get_sessions(Mod, LUser, LServer), lists:foreach(fun (#session{sid = {_, Pid}}) -> ejabberd_c2s:resend_presence(Pid) end, Ss). -spec get_sm_backend(binary()) -> module(). get_sm_backend(Host) -> DBType = ejabberd_option:sm_db_type(Host), list_to_existing_atom("ejabberd_sm_" ++ atom_to_list(DBType)). -spec get_sm_backends() -> [module()]. get_sm_backends() -> lists:usort([get_sm_backend(Host) || Host <- ejabberd_option:hosts()]). -spec get_vh_by_backend(module()) -> [binary()]. get_vh_by_backend(Mod) -> lists:filter( fun(Host) -> get_sm_backend(Host) == Mod end, ejabberd_option:hosts()). %%-------------------------------------------------------------------- %%% Cache stuff %%-------------------------------------------------------------------- -spec init_cache() -> ok. init_cache() -> case use_cache() of true -> ets_cache:new(?SM_CACHE, cache_opts()); false -> ets_cache:delete(?SM_CACHE) end. -spec cache_opts() -> [proplists:property()]. cache_opts() -> MaxSize = ejabberd_option:sm_cache_size(), CacheMissed = ejabberd_option:sm_cache_missed(), LifeTime = ejabberd_option:sm_cache_life_time(), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec clean_cache(node()) -> non_neg_integer(). clean_cache(Node) -> ets_cache:filter( ?SM_CACHE, fun(_, error) -> false; (_, {ok, Ss}) -> not lists:any( fun(#session{sid = {_, Pid}}) -> node(Pid) == Node end, Ss) end). -spec clean_cache() -> ok. clean_cache() -> ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]). -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, LServer) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(LServer); false -> ejabberd_option:sm_use_cache(LServer) end. -spec use_cache() -> boolean(). use_cache() -> lists:any( fun(Host) -> Mod = get_sm_backend(Host), use_cache(Mod, Host) end, ejabberd_option:hosts()). -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, LServer) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(LServer); false -> ejabberd_cluster:get_nodes() end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% ejabberd commands get_commands_spec() -> [#ejabberd_commands{name = connected_users, tags = [session], desc = "List all established sessions", policy = admin, module = ?MODULE, function = connected_users, args = [], result_desc = "List of users sessions", result_example = [<<"user1@example.com">>, <<"user2@example.com">>], result = {connected_users, {list, {sessions, string}}}}, #ejabberd_commands{name = connected_users_number, tags = [session, statistics], desc = "Get the number of established sessions", policy = admin, module = ?MODULE, function = connected_users_number, result_example = 2, args = [], result = {num_sessions, integer}}, #ejabberd_commands{name = user_resources, tags = [session], desc = "List user's connected resources", policy = admin, module = ?MODULE, function = user_resources, args = [{user, binary}, {host, binary}], args_desc = ["User name", "Server name"], args_example = [<<"user1">>, <<"example.com">>], result_example = [<<"tka1">>, <<"Gajim">>, <<"mobile-app">>], result = {resources, {list, {resource, string}}}}, #ejabberd_commands{name = kick_user, tags = [session], desc = "Disconnect user's active sessions", module = ?MODULE, function = kick_user, args = [{user, binary}, {host, binary}], args_desc = ["User name", "Server name"], args_example = [<<"user1">>, <<"example.com">>], result_desc = "Number of resources that were kicked", result_example = 3, result = {num_resources, integer}}]. -spec connected_users() -> [binary()]. connected_users() -> USRs = dirty_get_sessions_list(), SUSRs = lists:sort(USRs), lists:map(fun ({U, S, R}) -> <<U/binary, $@, S/binary, $/, R/binary>> end, SUSRs). connected_users_number() -> length(dirty_get_sessions_list()). user_resources(User, Server) -> Resources = get_user_resources(User, Server), lists:sort(Resources). -spec kick_user(binary(), binary()) -> non_neg_integer(). kick_user(User, Server) -> Resources = get_user_resources(User, Server), lists:foldl( fun(Resource, Acc) -> case kick_user(User, Server, Resource) of false -> Acc; true -> Acc + 1 end end, 0, Resources). -spec kick_user(binary(), binary(), binary()) -> boolean(). kick_user(User, Server, Resource) -> case get_session_pid(User, Server, Resource) of none -> false; Pid -> ejabberd_c2s:route(Pid, kick) end. make_sid() -> {misc:unique_timestamp(), self()}. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_disco_opt.erl����������������������������������������������������������������0000644�0002322�0002322�00000001533�14154362354�017751� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_disco_opt). -export([extra_domains/1]). -export([name/1]). -export([server_info/1]). -spec extra_domains(gen_mod:opts() | global | binary()) -> [binary()]. extra_domains(Opts) when is_map(Opts) -> gen_mod:get_opt(extra_domains, Opts); extra_domains(Host) -> gen_mod:get_module_opt(Host, mod_disco, extra_domains). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_disco, name). -spec server_info(gen_mod:opts() | global | binary()) -> [{'all' | [module()],binary(),[binary()]}]. server_info(Opts) when is_map(Opts) -> gen_mod:get_opt(server_info, Opts); server_info(Host) -> gen_mod:get_module_opt(Host, mod_disco, server_info). ���������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_service_log_opt.erl����������������������������������������������������������0000644�0002322�0002322�00000000523�14154362354�021147� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_service_log_opt). -export([loggers/1]). -spec loggers(gen_mod:opts() | global | binary()) -> [binary()]. loggers(Opts) when is_map(Opts) -> gen_mod:get_opt(loggers, Opts); loggers(Host) -> gen_mod:get_module_opt(Host, mod_service_log, loggers). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_http_ws.erl�������������������������������������������������������������0000644�0002322�0002322�00000035033�14154362354�020437� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_websocket.erl %%% Author : Eric Cestari <ecestari@process-one.net> %%% Purpose : XMPP Websocket support %%% Created : 09-10-2010 by Eric Cestari <ecestari@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_http_ws). -author('ecestari@process-one.net'). -behaviour(xmpp_socket). -behaviour(p1_fsm). -export([start/1, start_link/1, init/1, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3, send_xml/2, setopts/2, sockname/1, peername/1, controlling_process/2, get_owner/1, reset_stream/1, close/1, change_shaper/2, socket_handoff/3, get_transport/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -record(state, {socket :: ws_socket(), ping_interval :: non_neg_integer(), ping_timer = make_ref() :: reference(), pong_expected = false :: boolean(), timeout :: non_neg_integer(), timer = make_ref() :: reference(), input = [] :: list(), active = false :: boolean(), c2s_pid :: pid(), ws :: {#ws{}, pid()}, rfc_compliant = undefined :: boolean() | undefined}). %-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. -type ws_socket() :: {http_ws, pid(), {inet:ip_address(), inet:port_number()}}. -export_type([ws_socket/0]). start(WS) -> p1_fsm:start(?MODULE, [WS], ?FSMOPTS). start_link(WS) -> p1_fsm:start_link(?MODULE, [WS], ?FSMOPTS). send_xml({http_ws, FsmRef, _IP}, Packet) -> case catch p1_fsm:sync_send_all_state_event(FsmRef, {send_xml, Packet}, 15000) of {'EXIT', {timeout, _}} -> {error, timeout}; {'EXIT', _} -> {error, einval}; Res -> Res end. setopts({http_ws, FsmRef, _IP}, Opts) -> case lists:member({active, once}, Opts) of true -> p1_fsm:send_all_state_event(FsmRef, {activate, self()}); _ -> ok end. sockname(_Socket) -> {ok, {{0, 0, 0, 0}, 0}}. peername({http_ws, _FsmRef, IP}) -> {ok, IP}. controlling_process(_Socket, _Pid) -> ok. close({http_ws, FsmRef, _IP}) -> catch p1_fsm:sync_send_all_state_event(FsmRef, close). reset_stream({http_ws, _FsmRef, _IP} = Socket) -> Socket. change_shaper({http_ws, FsmRef, _IP}, Shaper) -> p1_fsm:send_all_state_event(FsmRef, {new_shaper, Shaper}). get_transport(_Socket) -> websocket. get_owner({http_ws, FsmRef, _IP}) -> FsmRef. socket_handoff(LocalPath, Request, Opts) -> ejabberd_websocket:socket_handoff(LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0). %%% Internal init([{#ws{ip = IP, http_opts = HOpts}, _} = WS]) -> 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; ({access, _}) -> true; (_) -> false end, HOpts), Opts = ejabberd_c2s_config:get_c2s_limits() ++ SOpts, PingInterval = ejabberd_option:websocket_ping_interval(), WSTimeout = ejabberd_option:websocket_timeout(), Socket = {http_ws, self(), IP}, ?DEBUG("Client connected through websocket ~p", [Socket]), case ejabberd_c2s:start(?MODULE, Socket, [{receiver, self()}|Opts]) of {ok, C2SPid} -> ejabberd_c2s:accept(C2SPid), Timer = erlang:start_timer(WSTimeout, self(), []), {ok, loop, #state{socket = Socket, timeout = WSTimeout, timer = Timer, ws = WS, c2s_pid = C2SPid, ping_interval = PingInterval}}; {error, Reason} -> {stop, Reason}; ignore -> ignore end. handle_event({activate, From}, StateName, State) -> State1 = case State#state.input of [] -> State#state{active = true}; Input -> lists:foreach( fun(I) when is_binary(I)-> From ! {tcp, State#state.socket, I}; (I2) -> From ! {tcp, State#state.socket, [I2]} end, Input), State#state{active = false, input = []} end, {next_state, StateName, State1#state{c2s_pid = From}}; handle_event({new_shaper, Shaper}, StateName, #state{ws = {_, WsPid}} = StateData) -> WsPid ! {new_shaper, Shaper}, {next_state, StateName, StateData}. handle_sync_event({send_xml, Packet}, _From, StateName, #state{ws = {_, WsPid}, rfc_compliant = R} = StateData) -> Packet2 = case {case R of undefined -> true; V -> V end, Packet} of {true, {xmlstreamstart, _, Attrs}} -> Attrs2 = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>} | lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], {xmlstreamelement, #xmlel{name = <<"open">>, attrs = Attrs2}}; {true, {xmlstreamend, _}} -> {xmlstreamelement, #xmlel{name = <<"close">>, attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}}; {true, {xmlstreamraw, <<"\r\n\r\n">>}} -> % cdata ping skip; {true, {xmlstreamelement, #xmlel{name=Name2} = El2}} -> El3 = case Name2 of <<"stream:", _/binary>> -> fxml:replace_tag_attr(<<"xmlns:stream">>, ?NS_STREAM, El2); _ -> case fxml:get_tag_attr_s(<<"xmlns">>, El2) of <<"">> -> fxml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, El2); _ -> El2 end end, {xmlstreamelement , El3}; _ -> Packet end, case Packet2 of {xmlstreamstart, Name, Attrs3} -> B = fxml:element_to_binary(#xmlel{name = Name, attrs = Attrs3}), route_text(WsPid, <<(binary:part(B, 0, byte_size(B)-2))/binary, ">">>); {xmlstreamend, Name} -> route_text(WsPid, <<"</", Name/binary, ">">>); {xmlstreamelement, El} -> route_text(WsPid, fxml:element_to_binary(El)); {xmlstreamraw, Bin} -> route_text(WsPid, Bin); {xmlstreamcdata, Bin2} -> route_text(WsPid, Bin2); skip -> ok end, SN2 = case Packet2 of {xmlstreamelement, #xmlel{name = <<"close">>}} -> stream_end_sent; _ -> StateName end, {reply, ok, SN2, StateData}; handle_sync_event(close, _From, StateName, #state{ws = {_, WsPid}, rfc_compliant = true} = StateData) when StateName /= stream_end_sent -> Close = #xmlel{name = <<"close">>, attrs = [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-framing">>}]}, route_text(WsPid, fxml:element_to_binary(Close)), {stop, normal, StateData}; handle_sync_event(close, _From, _StateName, StateData) -> {stop, normal, StateData}. handle_info(closed, _StateName, StateData) -> {stop, normal, StateData}; handle_info({received, Packet}, StateName, StateDataI) -> {StateData, Parsed} = parse(StateDataI, Packet), SD = case StateData#state.active of false -> Input = StateData#state.input ++ if is_binary(Parsed) -> [Parsed]; true -> Parsed end, StateData#state{input = Input}; true -> StateData#state.c2s_pid ! {tcp, StateData#state.socket, Parsed}, setup_timers(StateData#state{active = false}) end, {next_state, StateName, SD}; handle_info(PingPong, StateName, StateData) when PingPong == ping orelse PingPong == pong -> StateData2 = setup_timers(StateData), {next_state, StateName, StateData2#state{pong_expected = false}}; handle_info({timeout, Timer, _}, _StateName, #state{timer = Timer} = StateData) -> ?DEBUG("Closing websocket connection from hitting inactivity timeout", []), {stop, normal, StateData}; handle_info({timeout, Timer, _}, StateName, #state{ping_timer = Timer, ws = {_, WsPid}} = StateData) -> case StateData#state.pong_expected of false -> misc:cancel_timer(StateData#state.ping_timer), PingTimer = erlang:start_timer(StateData#state.ping_interval, self(), []), WsPid ! {ping, <<>>}, {next_state, StateName, StateData#state{ping_timer = PingTimer, pong_expected = true}}; true -> ?DEBUG("Closing websocket connection from missing pongs", []), {stop, normal, StateData} end; handle_info(_, StateName, StateData) -> {next_state, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. terminate(_Reason, _StateName, StateData) -> StateData#state.c2s_pid ! {tcp_closed, StateData#state.socket}. setup_timers(StateData) -> misc:cancel_timer(StateData#state.timer), Timer = erlang:start_timer(StateData#state.timeout, self(), []), misc:cancel_timer(StateData#state.ping_timer), PingTimer = case StateData#state.ping_interval of 0 -> StateData#state.ping_timer; V -> erlang:start_timer(V, self(), []) end, StateData#state{timer = Timer, ping_timer = PingTimer, pong_expected = false}. get_human_html_xmlel() -> Heading = <<"ejabberd ", (misc:atom_to_binary(?MODULE))/binary>>, #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], children = [#xmlel{name = <<"head">>, attrs = [], children = [#xmlel{name = <<"title">>, attrs = [], children = [{xmlcdata, Heading}]}]}, #xmlel{name = <<"body">>, attrs = [], children = [#xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, Heading}]}, #xmlel{name = <<"p">>, attrs = [], children = [{xmlcdata, <<"An implementation of ">>}, #xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"http://tools.ietf.org/html/rfc6455">>}], children = [{xmlcdata, <<"WebSocket protocol">>}]}]}, #xmlel{name = <<"p">>, attrs = [], children = [{xmlcdata, <<"This web page is only informative. To " "use WebSocket connection you need a Jabber/XMPP " "client that supports it.">>}]}]}]}. parse(#state{rfc_compliant = C} = State, Data) -> case C of undefined -> P = fxml_stream:new(self()), P2 = fxml_stream:parse(P, Data), fxml_stream:close(P2), case parsed_items([]) of error -> {State#state{rfc_compliant = true}, <<"parse error">>}; [] -> {State#state{rfc_compliant = true}, <<"parse error">>}; [{xmlstreamstart, <<"open">>, _} | _] -> parse(State#state{rfc_compliant = true}, Data); _ -> parse(State#state{rfc_compliant = false}, Data) end; true -> El = fxml_stream:parse_element(Data), case El of #xmlel{name = <<"open">>, attrs = Attrs} -> Attrs2 = [{<<"xmlns:stream">>, ?NS_STREAM}, {<<"xmlns">>, <<"jabber:client">>} | lists:keydelete(<<"xmlns">>, 1, lists:keydelete(<<"xmlns:stream">>, 1, Attrs))], {State, [{xmlstreamstart, <<"stream:stream">>, Attrs2}]}; #xmlel{name = <<"close">>} -> {State, [{xmlstreamend, <<"stream:stream">>}]}; {error, _} -> {State, <<"parse error">>}; _ -> {State, [El]} end; false -> {State, Data} end. parsed_items(List) -> receive {'$gen_event', El} when element(1, El) == xmlel; element(1, El) == xmlstreamstart; element(1, El) == xmlstreamelement; element(1, El) == xmlstreamcdata; element(1, El) == xmlstreamend -> parsed_items([El | List]); {'$gen_event', {xmlstreamerror, _}} -> error after 0 -> lists:reverse(List) end. -spec route_text(pid(), binary()) -> ok. route_text(Pid, Data) -> Pid ! {text_with_reply, Data, self()}, receive {text_reply, Pid} -> ok end. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_shared_roster_ldap.erl�������������������������������������������������������0000644�0002322�0002322�00000074603�14154362354�021642� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : mod_shared_roster_ldap.erl %%% Author : Realloc <realloc@realloc.spb.ru> %%% Marcin Owsiany <marcin@owsiany.pl> %%% Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% Description : LDAP shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_shared_roster_ldap). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([get_user_roster/2, get_jid_info/4, process_item/2, in_subscription/2, out_subscription/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). -include("eldap.hrl"). -include("translate.hrl"). -define(USER_CACHE, shared_roster_ldap_user_cache). -define(GROUP_CACHE, shared_roster_ldap_group_cache). -define(DISPLAYED_CACHE, shared_roster_ldap_displayed_cache). -define(LDAP_SEARCH_TIMEOUT, 5). %% Timeout for LDAP search queries in seconds -define(INVALID_SETTING_MSG, "~ts is not properly set! ~ts will not function."). -record(state, {host = <<"">> :: binary(), eldap_id = <<"">> :: binary(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), tls_options = [] :: list(), dn = <<"">> :: binary(), base = <<"">> :: binary(), password = <<"">> :: binary(), uid = <<"">> :: binary(), deref_aliases = never :: never | searching | finding | always, group_attr = <<"">> :: binary(), group_desc = <<"">> :: binary(), user_desc = <<"">> :: binary(), user_uid = <<"">> :: binary(), uid_format = <<"">> :: binary(), uid_format_re :: undefined | re:mp(), filter = <<"">> :: binary(), ufilter = <<"">> :: binary(), rfilter = <<"">> :: binary(), gfilter = <<"">> :: binary(), user_jid_attr = <<"">> :: binary(), auth_check = true :: boolean()}). -record(group_info, {desc, members}). %%==================================================================== %% API %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, _OldOpts) -> case init_cache(Host, NewOpts) of true -> ets_cache:setopts(?USER_CACHE, cache_opts(Host, NewOpts)), ets_cache:setopts(?GROUP_CACHE, cache_opts(Host, NewOpts)), ets_cache:setopts(?DISPLAYED_CACHE, cache_opts(Host, NewOpts)); false -> ok end, Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {set_state, parse_options(Host, NewOpts)}). depends(_Host, _Opts) -> [{mod_roster, hard}]. %%-------------------------------------------------------------------- %% Hooks %%-------------------------------------------------------------------- -spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. get_user_roster(Items, {U, S} = US) -> SRUsers = get_user_to_groups_map(US, true), {NewItems1, SRUsersRest} = lists:mapfoldl(fun (Item, SRUsers1) -> {_, _, {U1, S1, _}} = Item#roster.usj, US1 = {U1, S1}, case dict:find(US1, SRUsers1) of {ok, GroupNames} -> {Item#roster{subscription = both, groups = Item#roster.groups ++ GroupNames, ask = none}, dict:erase(US1, SRUsers1)}; error -> {Item, SRUsers1} end end, SRUsers, Items), SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, us = US, jid = {U1, S1, <<"">>}, name = get_user_name(U1, S1), subscription = both, ask = none, groups = GroupNames} || {{U1, S1}, GroupNames} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. %% This function in use to rewrite the roster entries when moving or renaming %% them in the user contact list. -spec process_item(#roster{}, binary()) -> #roster{}. process_item(RosterItem, _Host) -> USFrom = RosterItem#roster.us, {User, Server, _Resource} = RosterItem#roster.jid, USTo = {User, Server}, Map = get_user_to_groups_map(USFrom, false), case dict:find(USTo, Map) of error -> RosterItem; {ok, []} -> RosterItem; {ok, GroupNames} when RosterItem#roster.subscription == remove -> RosterItem#roster{subscription = both, ask = none, groups = GroupNames}; _ -> RosterItem#roster{subscription = both, ask = none} end. -spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> {subscription(), ask(), [binary()]}. get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, SRUsers = get_user_to_groups_map(US, false), case dict:find(US1, SRUsers) of {ok, GroupNames} -> NewGroups = if Groups == [] -> GroupNames; true -> Groups end, {both, none, NewGroups}; error -> {Subscription, Ask, Groups} end. -spec in_subscription(boolean(), presence()) -> boolean(). in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = JID, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, JID, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, DisplayedGroups = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> get_group_users(LServer, Group) end, DisplayedGroups)), case lists:member(US1, SRUsers) of true -> case Direction of in -> {stop, false}; out -> stop end; false -> Acc end. %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), State = parse_options(Host, Opts), init_cache(Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, ?MODULE, out_subscription, 30), ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:add(roster_process_item, Host, ?MODULE, process_item, 50), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), {ok, State}. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({set_state, NewState}, _State) -> {noreply, NewState}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:delete(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:delete(roster_out_subscription, Host, ?MODULE, out_subscription, 30), ejabberd_hooks:delete(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, ?MODULE, process_item, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- get_user_to_groups_map({_, Server} = US, SkipUS) -> DisplayedGroups = get_user_displayed_groups(US), lists:foldl(fun (Group, Dict1) -> GroupName = get_group_name(Server, Group), lists:foldl(fun (Contact, Dict) -> if SkipUS, Contact == US -> Dict; true -> dict:append(Contact, GroupName, Dict) end end, Dict1, get_group_users(Server, Group)) end, dict:new(), DisplayedGroups). eldap_search(State, FilterParseArgs, AttributesList) -> case apply(eldap_filter, parse, FilterParseArgs) of {ok, EldapFilter} -> case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, EldapFilter}, {timeout, ?LDAP_SEARCH_TIMEOUT}, {deref_aliases, State#state.deref_aliases}, {attributes, AttributesList}]) of #eldap_search_result{entries = Es} -> %% A result with entries. Return their list. Es; _ -> %% Something else. Pretend we got no results. [] end; _ -> %% Filter parsing failed. Pretend we got no results. [] end. get_user_displayed_groups({User, Host}) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), ets_cache:lookup(?DISPLAYED_CACHE, {User, Host}, fun () -> search_user_displayed_groups(State, User) end). search_user_displayed_groups(State, User) -> GroupAttr = State#state.group_attr, Entries = eldap_search(State, [eldap_filter:do_sub(State#state.rfilter, [{<<"%u">>, User}])], [GroupAttr]), Reply = lists:flatmap(fun (#eldap_entry{attributes = Attrs}) -> case Attrs of [{GroupAttr, ValuesList}] -> ValuesList; _ -> [] end end, Entries), lists:usort(Reply). get_group_users(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?GROUP_CACHE, {Group, Host}, fun () -> search_group_info(State, Group) end) of {ok, #group_info{members = Members}} when Members /= undefined -> Members; _ -> [] end. get_group_name(Host, Group) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?GROUP_CACHE, {Group, Host}, fun () -> search_group_info(State, Group) end) of {ok, #group_info{desc = GroupName}} when GroupName /= undefined -> GroupName; _ -> Group end. get_user_name(User, Host) -> {ok, State} = eldap_utils:get_state(Host, ?MODULE), case ets_cache:lookup(?USER_CACHE, {User, Host}, fun () -> search_user_name(State, User) end) of {ok, UserName} -> UserName; error -> User end. search_group_info(State, Group) -> Extractor = case State#state.uid_format_re of undefined -> fun (UID) -> catch eldap_utils:get_user_part(UID, State#state.uid_format) end; _ -> fun (UID) -> catch get_user_part_re(UID, State#state.uid_format_re) end end, AuthChecker = case State#state.auth_check of true -> fun ejabberd_auth:user_exists/2; _ -> fun (_U, _S) -> true end end, case eldap_search(State, [eldap_filter:do_sub(State#state.gfilter, [{<<"%g">>, Group}])], [State#state.group_attr, State#state.group_desc, State#state.uid]) of [] -> error; LDAPEntries -> {GroupDesc, MembersLists} = lists:foldl(fun(Entry, Acc) -> extract_members(State, Extractor, AuthChecker, Entry, Acc) end, {Group, []}, LDAPEntries), {ok, #group_info{desc = GroupDesc, members = lists:usort(lists:flatten(MembersLists))}} end. get_member_jid(#state{user_jid_attr = <<>>}, UID, Host) -> {jid:nodeprep(UID), Host}; get_member_jid(#state{user_jid_attr = UserJIDAttr, user_uid = UIDAttr} = State, UID, Host) -> Entries = eldap_search(State, [eldap_filter:do_sub(<<"(", UIDAttr/binary, "=%u)">>, [{<<"%u">>, UID}])], [UserJIDAttr]), case Entries of [] -> {error, error}; [#eldap_entry{attributes = [{UserJIDAttr, [MemberJID | _]}]} | _] -> try jid:decode(MemberJID) of #jid{luser = U, lserver = S} -> {U, S} catch error:{bad_jid, _} -> {error, Host} end end. extract_members(State, Extractor, AuthChecker, #eldap_entry{attributes = Attrs}, {DescAcc, JIDsAcc}) -> Host = State#state.host, case {eldap_utils:get_ldap_attr(State#state.group_attr, Attrs), eldap_utils:get_ldap_attr(State#state.group_desc, Attrs), lists:keysearch(State#state.uid, 1, Attrs)} of {ID, Desc, {value, {GroupMemberAttr, Members}}} when ID /= <<"">>, GroupMemberAttr == State#state.uid -> JIDs = lists:foldl( fun({ok, UID}, L) -> {MemberUID, MemberHost} = get_member_jid(State, UID, Host), case MemberUID of error -> L; _ -> case AuthChecker(MemberUID, MemberHost) of true -> [{MemberUID, MemberHost} | L]; _ -> L end end; (_, L) -> L end, [], lists:map(Extractor, Members)), {Desc, [JIDs | JIDsAcc]}; _ -> {DescAcc, JIDsAcc} end. search_user_name(State, User) -> case eldap_search(State, [eldap_filter:do_sub(State#state.ufilter, [{<<"%u">>, User}])], [State#state.user_desc, State#state.user_uid]) of [#eldap_entry{attributes = Attrs} | _] -> case {eldap_utils:get_ldap_attr(State#state.user_uid, Attrs), eldap_utils:get_ldap_attr(State#state.user_desc, Attrs)} of {UID, Desc} when UID /= <<"">> -> {ok, Desc}; _ -> error end; [] -> error end. %% Getting User ID part by regex pattern get_user_part_re(String, Pattern) -> case catch re:run(String, Pattern) of {match, Captured} -> {First, Len} = lists:nth(2, Captured), Result = str:sub_string(String, First + 1, First + Len), {ok, Result}; _ -> {error, badmatch} end. parse_options(Host, Opts) -> Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Cfg = ?eldap_config(mod_shared_roster_ldap_opt, Opts), GroupAttr = mod_shared_roster_ldap_opt:ldap_groupattr(Opts), GroupDesc = case mod_shared_roster_ldap_opt:ldap_groupdesc(Opts) of undefined -> GroupAttr; GD -> GD end, UserDesc = mod_shared_roster_ldap_opt:ldap_userdesc(Opts), UserUID = mod_shared_roster_ldap_opt:ldap_useruid(Opts), UIDAttr = mod_shared_roster_ldap_opt:ldap_memberattr(Opts), UIDAttrFormat = mod_shared_roster_ldap_opt:ldap_memberattr_format(Opts), UIDAttrFormatRe = mod_shared_roster_ldap_opt:ldap_memberattr_format_re(Opts), JIDAttr = mod_shared_roster_ldap_opt:ldap_userjidattr(Opts), AuthCheck = mod_shared_roster_ldap_opt:ldap_auth_check(Opts), ConfigFilter = mod_shared_roster_ldap_opt:ldap_filter(Opts), ConfigUserFilter = mod_shared_roster_ldap_opt:ldap_ufilter(Opts), ConfigGroupFilter = mod_shared_roster_ldap_opt:ldap_gfilter(Opts), RosterFilter = mod_shared_roster_ldap_opt:ldap_rfilter(Opts), SubFilter = <<"(&(", UIDAttr/binary, "=", UIDAttrFormat/binary, ")(", GroupAttr/binary, "=%g))">>, UserSubFilter = case ConfigUserFilter of <<"">> -> eldap_filter:do_sub(SubFilter, [{<<"%g">>, <<"*">>}]); UString -> UString end, GroupSubFilter = case ConfigGroupFilter of <<"">> -> eldap_filter:do_sub(SubFilter, [{<<"%u">>, <<"*">>}]); GString -> GString end, Filter = case ConfigFilter of <<"">> -> SubFilter; _ -> <<"(&", SubFilter/binary, ConfigFilter/binary, ")">> end, UserFilter = case ConfigFilter of <<"">> -> UserSubFilter; _ -> <<"(&", UserSubFilter/binary, ConfigFilter/binary, ")">> end, GroupFilter = case ConfigFilter of <<"">> -> GroupSubFilter; _ -> <<"(&", GroupSubFilter/binary, ConfigFilter/binary, ")">> end, #state{host = Host, eldap_id = Eldap_ID, servers = Cfg#eldap_config.servers, backups = Cfg#eldap_config.backups, port = Cfg#eldap_config.port, tls_options = Cfg#eldap_config.tls_options, dn = Cfg#eldap_config.dn, password = Cfg#eldap_config.password, base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, uid = UIDAttr, user_jid_attr = JIDAttr, group_attr = GroupAttr, group_desc = GroupDesc, user_desc = UserDesc, user_uid = UserUID, uid_format = UIDAttrFormat, uid_format_re = UIDAttrFormatRe, filter = Filter, ufilter = UserFilter, rfilter = RosterFilter, gfilter = GroupFilter, auth_check = AuthCheck}. init_cache(Host, Opts) -> UseCache = use_cache(Host, Opts), case UseCache of true -> CacheOpts = cache_opts(Host, Opts), ets_cache:new(?USER_CACHE, CacheOpts), ets_cache:new(?GROUP_CACHE, CacheOpts), ets_cache:new(?DISPLAYED_CACHE, CacheOpts); false -> ets_cache:delete(?USER_CACHE), ets_cache:delete(?GROUP_CACHE), ets_cache:delete(?DISPLAYED_CACHE) end, UseCache. use_cache(_Host, Opts) -> mod_shared_roster_ldap_opt:use_cache(Opts). cache_opts(_Host, Opts) -> MaxSize = mod_shared_roster_ldap_opt:cache_size(Opts), CacheMissed = mod_shared_roster_ldap_opt:cache_missed(Opts), LifeTime = mod_shared_roster_ldap_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. mod_opt_type(ldap_auth_check) -> econf:bool(); mod_opt_type(ldap_gfilter) -> econf:ldap_filter(); mod_opt_type(ldap_groupattr) -> econf:binary(); mod_opt_type(ldap_groupdesc) -> econf:binary(); mod_opt_type(ldap_memberattr) -> econf:binary(); mod_opt_type(ldap_memberattr_format) -> econf:binary(); mod_opt_type(ldap_memberattr_format_re) -> econf:re(); mod_opt_type(ldap_rfilter) -> econf:ldap_filter(); mod_opt_type(ldap_ufilter) -> econf:ldap_filter(); mod_opt_type(ldap_userdesc) -> econf:binary(); mod_opt_type(ldap_useruid) -> econf:binary(); mod_opt_type(ldap_userjidattr) -> econf:binary(); mod_opt_type(ldap_backups) -> econf:list(econf:domain(), [unique]); mod_opt_type(ldap_base) -> econf:binary(); mod_opt_type(ldap_deref_aliases) -> econf:enum([never, searching, finding, always]); mod_opt_type(ldap_encrypt) -> econf:enum([tls, starttls, none]); mod_opt_type(ldap_filter) -> econf:ldap_filter(); mod_opt_type(ldap_password) -> econf:binary(); mod_opt_type(ldap_port) -> econf:port(); mod_opt_type(ldap_rootdn) -> econf:binary(); mod_opt_type(ldap_servers) -> econf:list(econf:domain(), [unique]); mod_opt_type(ldap_tls_cacertfile) -> econf:pem(); mod_opt_type(ldap_tls_certfile) -> econf:pem(); mod_opt_type(ldap_tls_depth) -> econf:non_neg_int(); mod_opt_type(ldap_tls_verify) -> econf:enum([hard, soft, false]); mod_opt_type(ldap_uids) -> econf:either( econf:list( econf:and_then( econf:binary(), fun(U) -> {U, <<"%u">>} end)), econf:map(econf:binary(), econf:binary(), [unique])); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). -spec mod_options(binary()) -> [{ldap_uids, [{binary(), binary()}]} | {atom(), any()}]. mod_options(Host) -> [{ldap_auth_check, true}, {ldap_gfilter, <<"">>}, {ldap_groupattr, <<"cn">>}, {ldap_groupdesc, undefined}, {ldap_memberattr, <<"memberUid">>}, {ldap_memberattr_format, <<"%u">>}, {ldap_memberattr_format_re, undefined}, {ldap_rfilter, <<"">>}, {ldap_ufilter, <<"">>}, {ldap_userdesc, <<"cn">>}, {ldap_useruid, <<"cn">>}, {ldap_userjidattr, <<"">>}, {ldap_backups, ejabberd_option:ldap_backups(Host)}, {ldap_base, ejabberd_option:ldap_base(Host)}, {ldap_uids, ejabberd_option:ldap_uids(Host)}, {ldap_deref_aliases, ejabberd_option:ldap_deref_aliases(Host)}, {ldap_encrypt, ejabberd_option:ldap_encrypt(Host)}, {ldap_password, ejabberd_option:ldap_password(Host)}, {ldap_port, ejabberd_option:ldap_port(Host)}, {ldap_rootdn, ejabberd_option:ldap_rootdn(Host)}, {ldap_servers, ejabberd_option:ldap_servers(Host)}, {ldap_filter, ejabberd_option:ldap_filter(Host)}, {ldap_tls_certfile, ejabberd_option:ldap_tls_certfile(Host)}, {ldap_tls_cacertfile, ejabberd_option:ldap_tls_cacertfile(Host)}, {ldap_tls_depth, ejabberd_option:ldap_tls_depth(Host)}, {ldap_tls_verify, ejabberd_option:ldap_tls_verify(Host)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module lets the server administrator automatically " "populate users' rosters (contact lists) with entries based on " "users and groups defined in an LDAP-based directory."), "", ?T("NOTE: 'mod_shared_roster_ldap' depends on 'mod_roster' being " "enabled. Roster queries will return '503' errors if " "'mod_roster' is not enabled."), "", ?T("The module accepts many configuration options. Some of them, " "if unspecified, default to the values specified for the top " "level of configuration. This lets you avoid specifying, for " "example, the bind password in multiple places."), "", ?T("- Filters: 'ldap_rfilter', 'ldap_ufilter', 'ldap_gfilter', " "'ldap_filter'. These options specify LDAP filters used to " "query for shared roster information. All of them are run " "against the ldap_base."), ?T("- Attributes: 'ldap_groupattr', 'ldap_groupdesc', " "'ldap_memberattr', 'ldap_userdesc', 'ldap_useruid'. These " "options specify the names of the attributes which hold " "interesting data in the entries returned by running filters " "specified with the filter options."), ?T("- Control parameters: 'ldap_auth_check', " "'ldap_group_cache_validity', 'ldap_memberattr_format', " "'ldap_memberattr_format_re', 'ldap_user_cache_validity'. " "These parameters control the behaviour of the module."), ?T("- Connection parameters: The module also accepts the " "connection parameters, all of which default to the top-level " "parameter of the same name, if unspecified. " "See http://../database-ldap/#ldap-connection[LDAP Connection] " "section for more information about them."), "", ?T("Check also the http://../database-ldap/#configuration-examples" "[Configuration examples] section to get details about " "retrieving the roster, " "and configuration examples including Flat DIT and Deep DIT.")], opts => [ %% Filters: {ldap_rfilter, #{desc => ?T("So called \"Roster Filter\". Used to find names of " "all \"shared roster\" groups. See also the " "'ldap_groupattr' parameter. If unspecified, defaults to " "the top-level parameter of the same name. You must " "specify it in some place in the configuration, there is " "no default.")}}, {ldap_gfilter, #{desc => ?T("\"Group Filter\", used when retrieving human-readable " "name (a.k.a. \"Display Name\") and the members of a " "group. See also the parameters 'ldap_groupattr', " "'ldap_groupdesc' and 'ldap_memberattr'. If unspecified, " "defaults to the top-level parameter of the same name. " "If that one also is unspecified, then the filter is " "constructed exactly like \"User Filter\".")}}, {ldap_ufilter, #{desc => ?T("\"User Filter\", used for retrieving the human-readable " "name of roster entries (usually full names of people in " "the roster). See also the parameters 'ldap_userdesc' and " "'ldap_useruid'. For more information check the LDAP " "http://../database-ldap/#filters[Filters] section.")}}, {ldap_filter, #{desc => ?T("Additional filter which is AND-ed together " "with \"User Filter\" and \"Group Filter\". " "For more information check the LDAP " "http://../database-ldap/#filters[Filters] section.")}}, %% Attributes: {ldap_groupattr, #{desc => ?T("The name of the attribute that holds the group name, and " "that is used to differentiate between them. Retrieved " "from results of the \"Roster Filter\" " "and \"Group Filter\". Defaults to 'cn'.")}}, {ldap_groupdesc, #{desc => ?T("The name of the attribute which holds the human-readable " "group name in the objects you use to represent groups. " "Retrieved from results of the \"Group Filter\". " "Defaults to whatever 'ldap_groupattr' is set.")}}, {ldap_memberattr, #{desc => ?T("The name of the attribute which holds the IDs of the " "members of a group. Retrieved from results of the " "\"Group Filter\". Defaults to 'memberUid'. The name of " "the attribute differs depending on the objectClass you " "use for your group objects, for example: " "'posixGroup' -> 'memberUid'; 'groupOfNames' -> 'member'; " "'groupOfUniqueNames' -> 'uniqueMember'.")}}, {ldap_userdesc, #{desc => ?T("The name of the attribute which holds the human-readable " "user name. Retrieved from results of the " "\"User Filter\". Defaults to 'cn'.")}}, {ldap_useruid, #{desc => ?T("The name of the attribute which holds the ID of a roster " "item. Value of this attribute in the roster item objects " "needs to match the ID retrieved from the " "'ldap_memberattr' attribute of a group object. " "Retrieved from results of the \"User Filter\". " "Defaults to 'cn'.")}}, {ldap_userjidattr, #{desc => ?T("The name of the attribute which is used to map user id " "to XMPP jid. If not specified (and that is default value " "of this option), user jid will be created from user id and " " this module host.")}}, %% Control parameters: {ldap_memberattr_format, #{desc => ?T("A globbing format for extracting user ID from the value " "of the attribute named by 'ldap_memberattr'. Defaults " "to '%u', which means that the whole value is the member " "ID. If you change it to something different, you may " "also need to specify the User and Group Filters " "manually; see section Filters.")}}, {ldap_memberattr_format_re, #{desc => ?T("A regex for extracting user ID from the value of the " "attribute named by 'ldap_memberattr'. Check the LDAP " "http://../database-ldap/#control-parameters" "[Control Parameters] section.")}}, {ldap_auth_check, #{value => "true | false", desc => ?T("Whether the module should check (via the ejabberd " "authentication subsystem) for existence of each user in " "the shared LDAP roster. Set to 'false' if you want to " "disable the check. Default value is 'true'.")}}] ++ [{Opt, #{desc => {?T("Same as top-level _`~s`_ option, but " "applied to this module only."), [Opt]}}} || Opt <- [ldap_backups, ldap_base, ldap_uids, ldap_deref_aliases, ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, ldap_servers, ldap_tls_certfile, ldap_tls_cacertfile, ldap_tls_depth, ldap_tls_verify, use_cache, cache_size, cache_missed, cache_life_time]]}. �����������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_iq.erl������������������������������������������������������������������0000644�0002322�0002322�00000013653�14154362354�017364� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : ejabberd_iq.erl %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Purpose : %%% Created : 10 Nov 2017 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_iq). -behaviour(gen_server). %% API -export([start_link/0, route/4, dispatch/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {expire = infinity :: timeout()}). -type state() :: #state{}. %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec route(iq(), atom() | pid(), term(), non_neg_integer()) -> ok. route(#iq{type = T} = IQ, Proc, Ctx, Timeout) when T == set; T == get -> Expire = current_time() + Timeout, Rnd = p1_rand:get_string(), ID = encode_id(Expire, Rnd), ets:insert(?MODULE, {{Expire, Rnd}, Proc, Ctx}), gen_server:cast(?MODULE, {restart_timer, Expire}), ejabberd_router:route(IQ#iq{id = ID}). -spec dispatch(iq()) -> boolean(). dispatch(#iq{type = T, id = ID} = IQ) when T == error; T == result -> case decode_id(ID) of {ok, Expire, Rnd, Node} -> ejabberd_cluster:send({?MODULE, Node}, {route, IQ, {Expire, Rnd}}); error -> false end; dispatch(_) -> false. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> _ = ets:new(?MODULE, [named_table, ordered_set, public]), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), noreply(State). handle_cast({restart_timer, Expire}, State) -> State1 = State#state{expire = min(Expire, State#state.expire)}, noreply(State1); handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), noreply(State). handle_info({route, IQ, Key}, State) -> case ets:lookup(?MODULE, Key) of [{_, Proc, Ctx}] -> callback(Proc, IQ, Ctx), ets:delete(?MODULE, Key); [] -> ok end, noreply(State); handle_info(timeout, State) -> Expire = clean(ets:first(?MODULE)), noreply(State#state{expire = Expire}); handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), noreply(State). terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec current_time() -> non_neg_integer(). current_time() -> erlang:system_time(millisecond). -spec clean({non_neg_integer(), binary()} | '$end_of_table') -> non_neg_integer() | infinity. clean({Expire, _} = Key) -> case current_time() of Time when Time >= Expire -> case ets:lookup(?MODULE, Key) of [{_, Proc, Ctx}] -> callback(Proc, timeout, Ctx), ets:delete(?MODULE, Key); [] -> ok end, clean(ets:next(?MODULE, Key)); _ -> Expire end; clean('$end_of_table') -> infinity. -spec noreply(state()) -> {noreply, state()} | {noreply, state(), non_neg_integer()}. noreply(#state{expire = Expire} = State) -> case Expire of infinity -> {noreply, State}; _ -> Timeout = max(0, Expire - current_time()), {noreply, State, Timeout} end. -spec encode_id(non_neg_integer(), binary()) -> binary(). encode_id(Expire, Rnd) -> ExpireBin = integer_to_binary(Expire), Node = ejabberd_cluster:node_id(), CheckSum = calc_checksum(<<ExpireBin/binary, Rnd/binary, Node/binary>>), <<"rr-", ExpireBin/binary, $-, Rnd/binary, $-, CheckSum/binary, $-, Node/binary>>. -spec decode_id(binary()) -> {ok, non_neg_integer(), binary(), atom()} | error. decode_id(<<"rr-", ID/binary>>) -> try [ExpireBin, Tail] = binary:split(ID, <<"-">>), [Rnd, Rest] = binary:split(Tail, <<"-">>), [CheckSum, NodeBin] = binary:split(Rest, <<"-">>), CheckSum = calc_checksum(<<ExpireBin/binary, Rnd/binary, NodeBin/binary>>), Node = ejabberd_cluster:get_node_by_id(NodeBin), Expire = binary_to_integer(ExpireBin), {ok, Expire, Rnd, Node} catch _:{badmatch, _} -> error end; decode_id(_) -> error. -spec calc_checksum(binary()) -> binary(). calc_checksum(Data) -> Key = ejabberd_config:get_shared_key(), base64:encode(crypto:hash(sha, <<Data/binary, Key/binary>>)). -spec callback(atom() | pid(), #iq{} | timeout, term()) -> any(). callback(undefined, IQRes, Fun) -> try Fun(IQRes) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to process iq response:~n~ts~n** ~ts", [xmpp:pp(IQRes), misc:format_exception(2, Class, Reason, StackTrace)]) end; callback(Proc, IQRes, Ctx) -> try Proc ! {iq_reply, IQRes, Ctx} catch _:badarg -> ok end. �������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_sql.erl�����������������������������������������������������������������0000644�0002322�0002322�00000121115�14154362354�017543� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_sql.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Serve SQL connection %%% Created : 8 Dec 2004 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sql). -author('alexey@process-one.net'). -behaviour(p1_fsm). %% External exports -export([start_link/2, sql_query/2, sql_query_t/1, sql_transaction/2, sql_bloc/2, abort/1, restart/1, use_new_schema/0, sql_query_to_iolist/1, sql_query_to_iolist/2, escape/1, standard_escape/1, escape_like/1, escape_like_arg/1, escape_like_arg_circumflex/1, to_string_literal/2, to_string_literal_t/1, to_bool/1, sqlite_db/1, sqlite_file/1, encode_term/1, decode_term/1, odbcinst_config/0, init_mssql/1, keep_alive/2, to_list/2, to_array/2]). %% gen_fsm callbacks -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, print_state/1, code_change/4]). -export([connecting/2, connecting/3, session_established/2, session_established/3]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {db_ref :: undefined | pid(), db_type = odbc :: pgsql | mysql | sqlite | odbc | mssql, db_version :: undefined | non_neg_integer(), host :: binary(), pending_requests :: p1_queue:queue(), overload_reported :: undefined | integer()}). -define(STATE_KEY, ejabberd_sql_state). -define(NESTING_KEY, ejabberd_sql_nesting_level). -define(TOP_LEVEL_TXN, 0). -define(MAX_TRANSACTION_RESTARTS, 10). -define(KEEPALIVE_QUERY, [<<"SELECT 1;">>]). -define(PREPARE_KEY, ejabberd_sql_prepare). %%-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. -type state() :: #state{}. -type sql_query_simple() :: [sql_query() | binary()] | #sql_query{} | fun(() -> any()) | fun((atom(), _) -> any()). -type sql_query() :: sql_query_simple() | [{atom() | {atom(), any()}, sql_query_simple()}]. -type sql_query_result() :: {updated, non_neg_integer()} | {error, binary() | atom()} | {selected, [binary()], [[binary()]]} | {selected, [any()]} | ok. %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -spec start_link(binary(), pos_integer()) -> {ok, pid()} | {error, term()}. start_link(Host, I) -> Proc = binary_to_atom(get_worker_name(Host, I), utf8), p1_fsm:start_link({local, Proc}, ?MODULE, [Host], fsm_limit_opts() ++ ?FSMOPTS). -spec sql_query(binary(), sql_query()) -> sql_query_result(). sql_query(Host, Query) -> sql_call(Host, {sql_query, Query}). %% SQL transaction based on a list of queries %% This function automatically -spec sql_transaction(binary(), [sql_query()] | fun(() -> any())) -> {atomic, any()} | {aborted, any()}. sql_transaction(Host, Queries) when is_list(Queries) -> F = fun () -> lists:foreach(fun (Query) -> sql_query_t(Query) end, Queries) end, sql_transaction(Host, F); %% SQL transaction, based on a erlang anonymous function (F = fun) sql_transaction(Host, F) when is_function(F) -> case sql_call(Host, {sql_transaction, F}) of {atomic, _} = Ret -> Ret; {aborted, _} = Ret -> Ret; Err -> {aborted, Err} end. %% SQL bloc, based on a erlang anonymous function (F = fun) sql_bloc(Host, F) -> sql_call(Host, {sql_bloc, F}). sql_call(Host, Msg) -> Timeout = query_timeout(Host), case get(?STATE_KEY) of undefined -> sync_send_event(Host, {sql_cmd, Msg, current_time() + Timeout}, Timeout); _State -> nested_op(Msg) end. keep_alive(Host, Proc) -> Timeout = query_timeout(Host), case sync_send_event( Proc, {sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, current_time() + Timeout}, Timeout) of {selected,_,[[<<"1">>]]} -> ok; _Err -> ?ERROR_MSG("Keep alive query failed, closing connection: ~p", [_Err]), sync_send_event(Proc, force_timeout, Timeout) end. sync_send_event(Host, Msg, Timeout) when is_binary(Host) -> case ejabberd_sql_sup:start(Host) of ok -> Proc = get_worker(Host), sync_send_event(Proc, Msg, Timeout); {error, _} = Err -> Err end; sync_send_event(Proc, Msg, Timeout) -> try p1_fsm:sync_send_event(Proc, Msg, Timeout) catch _:{Reason, {p1_fsm, _, _}} -> {error, Reason} end. -spec sql_query_t(sql_query()) -> sql_query_result(). %% This function is intended to be used from inside an sql_transaction: sql_query_t(Query) -> QRes = sql_query_internal(Query), case QRes of {error, Reason} -> restart(Reason); Rs when is_list(Rs) -> case lists:keysearch(error, 1, Rs) of {value, {error, Reason}} -> restart(Reason); _ -> QRes end; _ -> QRes end. abort(Reason) -> exit(Reason). restart(Reason) -> throw({aborted, Reason}). -spec escape_char(char()) -> binary(). escape_char($\000) -> <<"\\0">>; escape_char($\n) -> <<"\\n">>; escape_char($\t) -> <<"\\t">>; escape_char($\b) -> <<"\\b">>; escape_char($\r) -> <<"\\r">>; escape_char($') -> <<"''">>; escape_char($") -> <<"\\\"">>; escape_char($\\) -> <<"\\\\">>; escape_char(C) -> <<C>>. -spec escape(binary()) -> binary(). escape(S) -> << <<(escape_char(Char))/binary>> || <<Char>> <= S >>. %% Escape character that will confuse an SQL engine %% Percent and underscore only need to be escaped for pattern matching like %% statement escape_like(S) when is_binary(S) -> << <<(escape_like(C))/binary>> || <<C>> <= S >>; escape_like($%) -> <<"\\%">>; escape_like($_) -> <<"\\_">>; escape_like($\\) -> <<"\\\\\\\\">>; escape_like(C) when is_integer(C), C >= 0, C =< 255 -> escape_char(C). escape_like_arg(S) when is_binary(S) -> << <<(escape_like_arg(C))/binary>> || <<C>> <= S >>; escape_like_arg($%) -> <<"\\%">>; escape_like_arg($_) -> <<"\\_">>; escape_like_arg($\\) -> <<"\\\\">>; escape_like_arg($[) -> <<"\\[">>; % For MSSQL escape_like_arg($]) -> <<"\\]">>; escape_like_arg(C) when is_integer(C), C >= 0, C =< 255 -> <<C>>. escape_like_arg_circumflex(S) when is_binary(S) -> << <<(escape_like_arg_circumflex(C))/binary>> || <<C>> <= S >>; escape_like_arg_circumflex($%) -> <<"^%">>; escape_like_arg_circumflex($_) -> <<"^_">>; escape_like_arg_circumflex($^) -> <<"^^">>; escape_like_arg_circumflex($[) -> <<"^[">>; % For MSSQL escape_like_arg_circumflex($]) -> <<"^]">>; escape_like_arg_circumflex(C) when is_integer(C), C >= 0, C =< 255 -> <<C>>. to_bool(<<"t">>) -> true; to_bool(<<"true">>) -> true; to_bool(<<"1">>) -> true; to_bool(true) -> true; to_bool(1) -> true; to_bool(_) -> false. to_list(EscapeFun, Val) -> Escaped = lists:join(<<",">>, lists:map(EscapeFun, Val)), [<<"(">>, Escaped, <<")">>]. to_array(EscapeFun, Val) -> Escaped = lists:join(<<",">>, lists:map(EscapeFun, Val)), lists:flatten([<<"{">>, Escaped, <<"}">>]). to_string_literal(odbc, S) -> <<"'", (escape(S))/binary, "'">>; to_string_literal(mysql, S) -> <<"'", (escape(S))/binary, "'">>; to_string_literal(mssql, S) -> <<"'", (standard_escape(S))/binary, "'">>; to_string_literal(sqlite, S) -> <<"'", (standard_escape(S))/binary, "'">>; to_string_literal(pgsql, S) -> <<"E'", (escape(S))/binary, "'">>. to_string_literal_t(S) -> State = get(?STATE_KEY), to_string_literal(State#state.db_type, S). encode_term(Term) -> escape(list_to_binary( erl_prettypr:format(erl_syntax:abstract(Term), [{paper, 65535}, {ribbon, 65535}]))). decode_term(Bin) -> Str = binary_to_list(<<Bin/binary, ".">>), try {ok, Tokens, _} = erl_scan:string(Str), {ok, Term} = erl_parse:parse_term(Tokens), Term catch _:{badmatch, {error, {Line, Mod, Reason}, _}} -> ?ERROR_MSG("Corrupted Erlang term in SQL database:~n" "** Scanner error: at line ~B: ~ts~n" "** Term: ~ts", [Line, Mod:format_error(Reason), Bin]), erlang:error(badarg); _:{badmatch, {error, {Line, Mod, Reason}}} -> ?ERROR_MSG("Corrupted Erlang term in SQL database:~n" "** Parser error: at line ~B: ~ts~n" "** Term: ~ts", [Line, Mod:format_error(Reason), Bin]), erlang:error(badarg) end. -spec sqlite_db(binary()) -> atom(). sqlite_db(Host) -> list_to_atom("ejabberd_sqlite_" ++ binary_to_list(Host)). -spec sqlite_file(binary()) -> string(). sqlite_file(Host) -> case ejabberd_option:sql_database(Host) of undefined -> Path = ["sqlite", atom_to_list(node()), binary_to_list(Host), "ejabberd.db"], case file:get_cwd() of {ok, Cwd} -> filename:join([Cwd|Path]); {error, Reason} -> ?ERROR_MSG("Failed to get current directory: ~ts", [file:format_error(Reason)]), filename:join(Path) end; File -> binary_to_list(File) end. use_new_schema() -> ejabberd_option:new_sql_schema(). -spec get_worker(binary()) -> atom(). get_worker(Host) -> PoolSize = ejabberd_option:sql_pool_size(Host), I = p1_rand:round_robin(PoolSize) + 1, binary_to_existing_atom(get_worker_name(Host, I), utf8). -spec get_worker_name(binary(), pos_integer()) -> binary(). get_worker_name(Host, I) -> <<"ejabberd_sql_", Host/binary, $_, (integer_to_binary(I))/binary>>. %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- init([Host]) -> process_flag(trap_exit, true), case ejabberd_option:sql_keepalive_interval(Host) of undefined -> ok; KeepaliveInterval -> timer:apply_interval(KeepaliveInterval, ?MODULE, keep_alive, [Host, self()]) end, [DBType | _] = db_opts(Host), p1_fsm:send_event(self(), connect), QueueType = ejabberd_option:sql_queue_type(Host), {ok, connecting, #state{db_type = DBType, host = Host, pending_requests = p1_queue:new(QueueType, max_fsm_queue())}}. connecting(connect, #state{host = Host} = State) -> ConnectRes = case db_opts(Host) of [mysql | Args] -> apply(fun mysql_connect/8, Args); [pgsql | Args] -> apply(fun pgsql_connect/8, Args); [sqlite | Args] -> apply(fun sqlite_connect/1, Args); [mssql | Args] -> apply(fun odbc_connect/2, Args); [odbc | Args] -> apply(fun odbc_connect/2, Args) end, case ConnectRes of {ok, Ref} -> try link(Ref) of _ -> lists:foreach( fun({{?PREPARE_KEY, _} = Key, _}) -> erase(Key); (_) -> ok end, get()), PendingRequests = p1_queue:dropwhile( fun(Req) -> p1_fsm:send_event(self(), Req), true end, State#state.pending_requests), State1 = State#state{db_ref = Ref, pending_requests = PendingRequests}, State2 = get_db_version(State1), {next_state, session_established, State2} catch _:Reason -> handle_reconnect(Reason, State) end; {error, Reason} -> handle_reconnect(Reason, State) end; connecting(Event, State) -> ?WARNING_MSG("Unexpected event in 'connecting': ~p", [Event]), {next_state, connecting, State}. connecting({sql_cmd, {sql_query, ?KEEPALIVE_QUERY}, Timestamp}, From, State) -> reply(From, {error, <<"SQL connection failed">>}, Timestamp), {next_state, connecting, State}; connecting({sql_cmd, Command, Timestamp} = Req, From, State) -> ?DEBUG("Queuing pending request while connecting:~n\t~p", [Req]), PendingRequests = try p1_queue:in({sql_cmd, Command, From, Timestamp}, State#state.pending_requests) catch error:full -> Err = <<"SQL request queue is overfilled">>, ?ERROR_MSG("~ts, bouncing all pending requests", [Err]), Q = p1_queue:dropwhile( fun({sql_cmd, _, To, TS}) -> reply(To, {error, Err}, TS), true end, State#state.pending_requests), p1_queue:in({sql_cmd, Command, From, Timestamp}, Q) end, {next_state, connecting, State#state{pending_requests = PendingRequests}}; connecting(Request, {Who, _Ref}, State) -> ?WARNING_MSG("Unexpected call ~p from ~p in 'connecting'", [Request, Who]), {next_state, connecting, State}. session_established({sql_cmd, Command, Timestamp}, From, State) -> run_sql_cmd(Command, From, State, Timestamp); session_established(Request, {Who, _Ref}, State) -> ?WARNING_MSG("Unexpected call ~p from ~p in 'session_established'", [Request, Who]), {next_state, session_established, State}. session_established({sql_cmd, Command, From, Timestamp}, State) -> run_sql_cmd(Command, From, State, Timestamp); session_established(force_timeout, State) -> {stop, timeout, State}; session_established(Event, State) -> ?WARNING_MSG("Unexpected event in 'session_established': ~p", [Event]), {next_state, session_established, State}. handle_event(_Event, StateName, State) -> {next_state, StateName, State}. handle_sync_event(_Event, _From, StateName, State) -> {reply, {error, badarg}, StateName, State}. code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. handle_info({'EXIT', _Pid, Reason}, _StateName, State) -> handle_reconnect(Reason, State); handle_info(Info, StateName, State) -> ?WARNING_MSG("Unexpected info in ~p: ~p", [StateName, Info]), {next_state, StateName, State}. terminate(_Reason, _StateName, State) -> case State#state.db_type of mysql -> catch p1_mysql_conn:stop(State#state.db_ref); sqlite -> catch sqlite3:close(sqlite_db(State#state.host)); _ -> ok end, ok. %%---------------------------------------------------------------------- %% Func: print_state/1 %% Purpose: Prepare the state to be printed on error log %% Returns: State to print %%---------------------------------------------------------------------- print_state(State) -> State. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- handle_reconnect(Reason, #state{host = Host} = State) -> StartInterval = ejabberd_option:sql_start_interval(Host), ?WARNING_MSG("~p connection failed:~n" "** Reason: ~p~n" "** Retry after: ~B seconds", [State#state.db_type, Reason, StartInterval div 1000]), p1_fsm:send_event_after(StartInterval, connect), {next_state, connecting, State}. run_sql_cmd(Command, From, State, Timestamp) -> case current_time() >= Timestamp of true -> State1 = report_overload(State), {next_state, session_established, State1}; false -> put(?NESTING_KEY, ?TOP_LEVEL_TXN), put(?STATE_KEY, State), abort_on_driver_error(outer_op(Command), From, Timestamp) end. %% Only called by handle_call, only handles top level operations. %% @spec outer_op(Op) -> {error, Reason} | {aborted, Reason} | {atomic, Result} outer_op({sql_query, Query}) -> sql_query_internal(Query); outer_op({sql_transaction, F}) -> outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); outer_op({sql_bloc, F}) -> execute_bloc(F). %% Called via sql_query/transaction/bloc from client code when inside a %% nested operation nested_op({sql_query, Query}) -> sql_query_internal(Query); nested_op({sql_transaction, F}) -> NestingLevel = get(?NESTING_KEY), if NestingLevel =:= (?TOP_LEVEL_TXN) -> outer_transaction(F, ?MAX_TRANSACTION_RESTARTS, <<"">>); true -> inner_transaction(F) end; nested_op({sql_bloc, F}) -> execute_bloc(F). %% Never retry nested transactions - only outer transactions inner_transaction(F) -> PreviousNestingLevel = get(?NESTING_KEY), case get(?NESTING_KEY) of ?TOP_LEVEL_TXN -> {backtrace, T} = process_info(self(), backtrace), ?ERROR_MSG("Inner transaction called at outer txn " "level. Trace: ~ts", [T]), erlang:exit(implementation_faulty); _N -> ok end, put(?NESTING_KEY, PreviousNestingLevel + 1), Result = (catch F()), put(?NESTING_KEY, PreviousNestingLevel), case Result of {aborted, Reason} -> {aborted, Reason}; {'EXIT', Reason} -> {'EXIT', Reason}; {atomic, Res} -> {atomic, Res}; Res -> {atomic, Res} end. outer_transaction(F, NRestarts, _Reason) -> PreviousNestingLevel = get(?NESTING_KEY), case get(?NESTING_KEY) of ?TOP_LEVEL_TXN -> ok; _N -> {backtrace, T} = process_info(self(), backtrace), ?ERROR_MSG("Outer transaction called at inner txn " "level. Trace: ~ts", [T]), erlang:exit(implementation_faulty) end, sql_begin(), put(?NESTING_KEY, PreviousNestingLevel + 1), try F() of Res -> sql_commit(), {atomic, Res} catch ?EX_RULE(throw, {aborted, Reason}, _) when NRestarts > 0 -> sql_rollback(), put(?NESTING_KEY, ?TOP_LEVEL_TXN), outer_transaction(F, NRestarts - 1, Reason); ?EX_RULE(throw, {aborted, Reason}, Stack) when NRestarts =:= 0 -> StackTrace = ?EX_STACK(Stack), ?ERROR_MSG("SQL transaction restarts exceeded~n** " "Restarts: ~p~n** Last abort reason: " "~p~n** Stacktrace: ~p~n** When State " "== ~p", [?MAX_TRANSACTION_RESTARTS, Reason, StackTrace, get(?STATE_KEY)]), sql_rollback(), {aborted, Reason}; ?EX_RULE(exit, Reason, _) -> sql_rollback(), {aborted, Reason} end. execute_bloc(F) -> case catch F() of {aborted, Reason} -> {aborted, Reason}; {'EXIT', Reason} -> {aborted, Reason}; Res -> {atomic, Res} end. execute_fun(F) when is_function(F, 0) -> F(); execute_fun(F) when is_function(F, 2) -> State = get(?STATE_KEY), F(State#state.db_type, State#state.db_version). sql_query_internal([{_, _} | _] = Queries) -> State = get(?STATE_KEY), case select_sql_query(Queries, State) of undefined -> {error, <<"no matching query for the current DBMS found">>}; Query -> sql_query_internal(Query) end; sql_query_internal(#sql_query{} = Query) -> State = get(?STATE_KEY), Res = try case State#state.db_type of odbc -> generic_sql_query(Query); mssql -> mssql_sql_query(Query); pgsql -> Key = {?PREPARE_KEY, Query#sql_query.hash}, case get(Key) of undefined -> Host = State#state.host, PreparedStatements = ejabberd_option:sql_prepared_statements(Host), case PreparedStatements of false -> put(Key, ignore); true -> case pgsql_prepare(Query, State) of {ok, _, _, _} -> put(Key, prepared); {error, Error} -> ?ERROR_MSG( "PREPARE failed for SQL query " "at ~p: ~p", [Query#sql_query.loc, Error]), put(Key, ignore) end end; _ -> ok end, case get(Key) of prepared -> pgsql_execute_sql_query(Query, State); _ -> pgsql_sql_query(Query) end; mysql -> generic_sql_query(Query); sqlite -> sqlite_sql_query(Query) end catch exit:{timeout, _} -> {error, <<"timed out">>}; exit:{killed, _} -> {error, <<"killed">>}; exit:{normal, _} -> {error, <<"terminated unexpectedly">>}; exit:{shutdown, _} -> {error, <<"shutdown">>}; ?EX_RULE(Class, Reason, Stack) -> StackTrace = ?EX_STACK(Stack), ?ERROR_MSG("Internal error while processing SQL query:~n** ~ts", [misc:format_exception(2, Class, Reason, StackTrace)]), {error, <<"internal error">>} end, check_error(Res, Query); sql_query_internal(F) when is_function(F) -> case catch execute_fun(F) of {aborted, Reason} -> {error, Reason}; {'EXIT', Reason} -> {error, Reason}; Res -> Res end; sql_query_internal(Query) -> State = get(?STATE_KEY), ?DEBUG("SQL: \"~ts\"", [Query]), QueryTimeout = query_timeout(State#state.host), Res = case State#state.db_type of odbc -> to_odbc(odbc:sql_query(State#state.db_ref, [Query], QueryTimeout - 1000)); mssql -> to_odbc(odbc:sql_query(State#state.db_ref, [Query], QueryTimeout - 1000)); pgsql -> pgsql_to_odbc(pgsql:squery(State#state.db_ref, Query, QueryTimeout - 1000)); mysql -> R = mysql_to_odbc(p1_mysql_conn:squery(State#state.db_ref, [Query], self(), [{timeout, QueryTimeout - 1000}, {result_type, binary}])), R; sqlite -> Host = State#state.host, sqlite_to_odbc(Host, sqlite3:sql_exec(sqlite_db(Host), Query)) end, check_error(Res, Query). select_sql_query(Queries, State) -> select_sql_query( Queries, State#state.db_type, State#state.db_version, undefined). select_sql_query([], _Type, _Version, undefined) -> undefined; select_sql_query([], _Type, _Version, Query) -> Query; select_sql_query([{any, Query} | _], _Type, _Version, _) -> Query; select_sql_query([{Type, Query} | _], Type, _Version, _) -> Query; select_sql_query([{{Type, _Version1}, Query1} | Rest], Type, undefined, _) -> select_sql_query(Rest, Type, undefined, Query1); select_sql_query([{{Type, Version1}, Query1} | Rest], Type, Version, Query) -> if Version >= Version1 -> Query1; true -> select_sql_query(Rest, Type, Version, Query) end; select_sql_query([{_, _} | Rest], Type, Version, Query) -> select_sql_query(Rest, Type, Version, Query). generic_sql_query(SQLQuery) -> sql_query_format_res( sql_query_internal(generic_sql_query_format(SQLQuery)), SQLQuery). generic_sql_query_format(SQLQuery) -> Args = (SQLQuery#sql_query.args)(generic_escape()), (SQLQuery#sql_query.format_query)(Args). generic_escape() -> #sql_escape{string = fun(X) -> <<"'", (escape(X))/binary, "'">> end, integer = fun(X) -> misc:i2l(X) end, boolean = fun(true) -> <<"1">>; (false) -> <<"0">> end, in_array_string = fun(X) -> <<"'", (escape(X))/binary, "'">> end, like_escape = fun() -> <<"">> end }. pgsql_sql_query(SQLQuery) -> sql_query_format_res( sql_query_internal(pgsql_sql_query_format(SQLQuery)), SQLQuery). pgsql_sql_query_format(SQLQuery) -> Args = (SQLQuery#sql_query.args)(pgsql_escape()), (SQLQuery#sql_query.format_query)(Args). pgsql_escape() -> #sql_escape{string = fun(X) -> <<"E'", (escape(X))/binary, "'">> end, integer = fun(X) -> misc:i2l(X) end, boolean = fun(true) -> <<"'t'">>; (false) -> <<"'f'">> end, in_array_string = fun(X) -> <<"E'", (escape(X))/binary, "'">> end, like_escape = fun() -> <<"ESCAPE E'\\\\'">> end }. sqlite_sql_query(SQLQuery) -> sql_query_format_res( sql_query_internal(sqlite_sql_query_format(SQLQuery)), SQLQuery). sqlite_sql_query_format(SQLQuery) -> Args = (SQLQuery#sql_query.args)(sqlite_escape()), (SQLQuery#sql_query.format_query)(Args). sqlite_escape() -> #sql_escape{string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end, integer = fun(X) -> misc:i2l(X) end, boolean = fun(true) -> <<"1">>; (false) -> <<"0">> end, in_array_string = fun(X) -> <<"'", (standard_escape(X))/binary, "'">> end, like_escape = fun() -> <<"ESCAPE '\\'">> end }. standard_escape(S) -> << <<(case Char of $' -> << "''" >>; _ -> << Char >> end)/binary>> || <<Char>> <= S >>. mssql_sql_query(SQLQuery) -> sqlite_sql_query(SQLQuery). pgsql_prepare(SQLQuery, State) -> Escape = #sql_escape{_ = fun(_) -> arg end, like_escape = fun() -> escape end}, {RArgs, _} = lists:foldl( fun(arg, {Acc, I}) -> {[<<$$, (integer_to_binary(I))/binary>> | Acc], I + 1}; (escape, {Acc, I}) -> {[<<"ESCAPE E'\\\\'">> | Acc], I}; (List, {Acc, I}) when is_list(List) -> {[<<$$, (integer_to_binary(I))/binary>> | Acc], I + 1} end, {[], 1}, (SQLQuery#sql_query.args)(Escape)), Args = lists:reverse(RArgs), %N = length((SQLQuery#sql_query.args)(Escape)), %Args = [<<$$, (integer_to_binary(I))/binary>> || I <- lists:seq(1, N)], Query = (SQLQuery#sql_query.format_query)(Args), pgsql:prepare(State#state.db_ref, SQLQuery#sql_query.hash, Query). pgsql_execute_escape() -> #sql_escape{string = fun(X) -> X end, integer = fun(X) -> [misc:i2l(X)] end, boolean = fun(true) -> "1"; (false) -> "0" end, in_array_string = fun(X) -> <<"\"", (escape(X))/binary, "\"">> end, like_escape = fun() -> ignore end }. pgsql_execute_sql_query(SQLQuery, State) -> Args = (SQLQuery#sql_query.args)(pgsql_execute_escape()), Args2 = lists:filter(fun(ignore) -> false; (_) -> true end, Args), ExecuteRes = pgsql:execute(State#state.db_ref, SQLQuery#sql_query.hash, Args2), % {T, ExecuteRes} = % timer:tc(pgsql, execute, [State#state.db_ref, SQLQuery#sql_query.hash, Args]), % io:format("T ~ts ~p~n", [SQLQuery#sql_query.hash, T]), Res = pgsql_execute_to_odbc(ExecuteRes), sql_query_format_res(Res, SQLQuery). sql_query_format_res({selected, _, Rows}, SQLQuery) -> Res = lists:flatmap( fun(Row) -> try [(SQLQuery#sql_query.format_res)(Row)] catch ?EX_RULE(Class, Reason, Stack) -> StackTrace = ?EX_STACK(Stack), ?ERROR_MSG("Error while processing SQL query result:~n" "** Row: ~p~n** ~ts", [Row, misc:format_exception(2, Class, Reason, StackTrace)]), [] end end, Rows), {selected, Res}; sql_query_format_res(Res, _SQLQuery) -> Res. sql_query_to_iolist(SQLQuery) -> generic_sql_query_format(SQLQuery). sql_query_to_iolist(sqlite, SQLQuery) -> sqlite_sql_query_format(SQLQuery); sql_query_to_iolist(_DbType, SQLQuery) -> generic_sql_query_format(SQLQuery). sql_begin() -> sql_query_internal( [{mssql, [<<"begin transaction;">>]}, {any, [<<"begin;">>]}]). sql_commit() -> sql_query_internal( [{mssql, [<<"commit transaction;">>]}, {any, [<<"commit;">>]}]). sql_rollback() -> sql_query_internal( [{mssql, [<<"rollback transaction;">>]}, {any, [<<"rollback;">>]}]). %% Generate the OTP callback return tuple depending on the driver result. abort_on_driver_error({error, <<"query timed out">>} = Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {stop, timeout, get(?STATE_KEY)}; abort_on_driver_error({error, <<"Failed sending data on socket", _/binary>>} = Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {stop, closed, get(?STATE_KEY)}; abort_on_driver_error({error, <<"SQL connection failed">>} = Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {stop, timeout, get(?STATE_KEY)}; abort_on_driver_error({error, <<"Communication link failure">>} = Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {stop, closed, get(?STATE_KEY)}; abort_on_driver_error(Reply, From, Timestamp) -> reply(From, Reply, Timestamp), {next_state, session_established, get(?STATE_KEY)}. -spec report_overload(state()) -> state(). report_overload(#state{overload_reported = PrevTime} = State) -> CurrTime = current_time(), case PrevTime == undefined orelse (CurrTime - PrevTime) > timer:seconds(30) of true -> ?ERROR_MSG("SQL connection pool is overloaded, " "discarding stale requests", []), State#state{overload_reported = current_time()}; false -> State end. -spec reply({pid(), term()}, term(), integer()) -> term(). reply(From, Reply, Timestamp) -> case current_time() >= Timestamp of true -> ok; false -> p1_fsm:reply(From, Reply) end. %% == pure ODBC code %% part of init/1 %% Open an ODBC database connection odbc_connect(SQLServer, Timeout) -> ejabberd:start_app(odbc), odbc:connect(binary_to_list(SQLServer), [{scrollable_cursors, off}, {extended_errors, on}, {tuple_row, off}, {timeout, Timeout}, {binary_strings, on}]). %% == Native SQLite code %% part of init/1 %% Open a database connection to SQLite sqlite_connect(Host) -> File = sqlite_file(Host), case filelib:ensure_dir(File) of ok -> case sqlite3:open(sqlite_db(Host), [{file, File}]) of {ok, Ref} -> sqlite3:sql_exec( sqlite_db(Host), "pragma foreign_keys = on"), {ok, Ref}; {error, {already_started, Ref}} -> {ok, Ref}; {error, Reason} -> {error, Reason} end; Err -> Err end. %% Convert SQLite query result to Erlang ODBC result formalism sqlite_to_odbc(Host, ok) -> {updated, sqlite3:changes(sqlite_db(Host))}; sqlite_to_odbc(Host, {rowid, _}) -> {updated, sqlite3:changes(sqlite_db(Host))}; sqlite_to_odbc(_Host, [{columns, Columns}, {rows, TRows}]) -> Rows = [lists:map( fun(I) when is_integer(I) -> integer_to_binary(I); (B) -> B end, tuple_to_list(Row)) || Row <- TRows], {selected, [list_to_binary(C) || C <- Columns], Rows}; sqlite_to_odbc(_Host, {error, _Code, Reason}) -> {error, Reason}; sqlite_to_odbc(_Host, _) -> {updated, undefined}. %% == Native PostgreSQL code %% part of init/1 %% Open a database connection to PostgreSQL pgsql_connect(Server, Port, DB, Username, Password, ConnectTimeout, Transport, SSLOpts) -> case pgsql:connect([{host, Server}, {database, DB}, {user, Username}, {password, Password}, {port, Port}, {transport, Transport}, {connect_timeout, ConnectTimeout}, {as_binary, true}|SSLOpts]) of {ok, Ref} -> pgsql:squery(Ref, [<<"alter database \"">>, DB, <<"\" set ">>, <<"standard_conforming_strings='off';">>]), pgsql:squery(Ref, [<<"set standard_conforming_strings to 'off';">>]), {ok, Ref}; Err -> Err end. %% Convert PostgreSQL query result to Erlang ODBC result formalism pgsql_to_odbc({ok, PGSQLResult}) -> case PGSQLResult of [Item] -> pgsql_item_to_odbc(Item); Items -> [pgsql_item_to_odbc(Item) || Item <- Items] end. pgsql_item_to_odbc({<<"SELECT", _/binary>>, Rows, Recs}) -> {selected, [element(1, Row) || Row <- Rows], Recs}; pgsql_item_to_odbc({<<"FETCH", _/binary>>, Rows, Recs}) -> {selected, [element(1, Row) || Row <- Rows], Recs}; pgsql_item_to_odbc(<<"INSERT ", OIDN/binary>>) -> [_OID, N] = str:tokens(OIDN, <<" ">>), {updated, binary_to_integer(N)}; pgsql_item_to_odbc(<<"DELETE ", N/binary>>) -> {updated, binary_to_integer(N)}; pgsql_item_to_odbc(<<"UPDATE ", N/binary>>) -> {updated, binary_to_integer(N)}; pgsql_item_to_odbc({error, Error}) -> {error, Error}; pgsql_item_to_odbc(_) -> {updated, undefined}. pgsql_execute_to_odbc({ok, {<<"SELECT", _/binary>>, Rows}}) -> {selected, [], [[Field || {_, Field} <- Row] || Row <- Rows]}; pgsql_execute_to_odbc({ok, {'INSERT', N}}) -> {updated, N}; pgsql_execute_to_odbc({ok, {'DELETE', N}}) -> {updated, N}; pgsql_execute_to_odbc({ok, {'UPDATE', N}}) -> {updated, N}; pgsql_execute_to_odbc({error, Error}) -> {error, Error}; pgsql_execute_to_odbc(_) -> {updated, undefined}. %% == Native MySQL code %% part of init/1 %% Open a database connection to MySQL mysql_connect(Server, Port, DB, Username, Password, ConnectTimeout, Transport, _) -> SSLOpts = case Transport of ssl -> [ssl_required]; _ -> [] end, case p1_mysql_conn:start(binary_to_list(Server), Port, binary_to_list(Username), binary_to_list(Password), binary_to_list(DB), ConnectTimeout, fun log/3, SSLOpts) of {ok, Ref} -> p1_mysql_conn:fetch( Ref, [<<"set names 'utf8mb4' collate 'utf8mb4_bin';">>], self()), {ok, Ref}; Err -> Err end. %% Convert MySQL query result to Erlang ODBC result formalism mysql_to_odbc({updated, MySQLRes}) -> {updated, p1_mysql:get_result_affected_rows(MySQLRes)}; mysql_to_odbc({data, MySQLRes}) -> mysql_item_to_odbc(p1_mysql:get_result_field_info(MySQLRes), p1_mysql:get_result_rows(MySQLRes)); mysql_to_odbc({error, MySQLRes}) when is_binary(MySQLRes) -> {error, MySQLRes}; mysql_to_odbc({error, MySQLRes}) when is_list(MySQLRes) -> {error, list_to_binary(MySQLRes)}; mysql_to_odbc({error, MySQLRes}) -> mysql_to_odbc({error, p1_mysql:get_result_reason(MySQLRes)}); mysql_to_odbc(ok) -> ok. %% When tabular data is returned, convert it to the ODBC formalism mysql_item_to_odbc(Columns, Recs) -> {selected, [element(2, Column) || Column <- Columns], Recs}. to_odbc({selected, Columns, Recs}) -> Rows = [lists:map( fun(I) when is_integer(I) -> integer_to_binary(I); (B) -> B end, Row) || Row <- Recs], {selected, [list_to_binary(C) || C <- Columns], Rows}; to_odbc({error, Reason}) when is_list(Reason) -> {error, list_to_binary(Reason)}; to_odbc(Res) -> Res. get_db_version(#state{db_type = pgsql} = State) -> case pgsql:squery(State#state.db_ref, <<"select current_setting('server_version_num')">>) of {ok, [{_, _, [[SVersion]]}]} -> case catch binary_to_integer(SVersion) of Version when is_integer(Version) -> State#state{db_version = Version}; Error -> ?WARNING_MSG("Error getting pgsql version: ~p", [Error]), State end; Res -> ?WARNING_MSG("Error getting pgsql version: ~p", [Res]), State end; get_db_version(State) -> State. log(Level, Format, Args) -> case Level of debug -> ?DEBUG(Format, Args); info -> ?INFO_MSG(Format, Args); normal -> ?INFO_MSG(Format, Args); error -> ?ERROR_MSG(Format, Args) end. db_opts(Host) -> Type = ejabberd_option:sql_type(Host), Server = ejabberd_option:sql_server(Host), Timeout = ejabberd_option:sql_connect_timeout(Host), Transport = case ejabberd_option:sql_ssl(Host) of false -> tcp; true -> ssl end, warn_if_ssl_unsupported(Transport, Type), case Type of odbc -> [odbc, Server, Timeout]; sqlite -> [sqlite, Host]; _ -> Port = ejabberd_option:sql_port(Host), DB = case ejabberd_option:sql_database(Host) of undefined -> <<"ejabberd">>; D -> D end, User = ejabberd_option:sql_username(Host), Pass = ejabberd_option:sql_password(Host), SSLOpts = get_ssl_opts(Transport, Host), case Type of mssql -> [mssql, <<"DRIVER=ODBC;SERVER=", Server/binary, ";UID=", User/binary, ";DATABASE=", DB/binary ,";PWD=", Pass/binary, ";PORT=", (integer_to_binary(Port))/binary ,";CLIENT_CHARSET=UTF-8;">>, Timeout]; _ -> [Type, Server, Port, DB, User, Pass, Timeout, Transport, SSLOpts] end end. warn_if_ssl_unsupported(tcp, _) -> ok; warn_if_ssl_unsupported(ssl, pgsql) -> ok; warn_if_ssl_unsupported(ssl, mysql) -> ok; warn_if_ssl_unsupported(ssl, Type) -> ?WARNING_MSG("SSL connection is not supported for ~ts", [Type]). get_ssl_opts(ssl, Host) -> Opts1 = case ejabberd_option:sql_ssl_certfile(Host) of undefined -> []; CertFile -> [{certfile, CertFile}] end, Opts2 = case ejabberd_option:sql_ssl_cafile(Host) of undefined -> Opts1; CAFile -> [{cacertfile, CAFile}|Opts1] end, case ejabberd_option:sql_ssl_verify(Host) of true -> case lists:keymember(cacertfile, 1, Opts2) of true -> [{verify, verify_peer}|Opts2]; false -> ?WARNING_MSG("SSL verification is enabled for " "SQL connection, but option " "'sql_ssl_cafile' is not set; " "verification will be disabled", []), Opts2 end; false -> Opts2 end; get_ssl_opts(tcp, _) -> []. init_mssql(Host) -> Driver = ejabberd_option:sql_odbc_driver(Host), ODBCINST = io_lib:fwrite("[ODBC]~n" "Driver = ~s~n", [Driver]), ?DEBUG("~ts:~n~ts", [odbcinst_config(), ODBCINST]), case filelib:ensure_dir(odbcinst_config()) of ok -> try ok = write_file_if_new(odbcinst_config(), ODBCINST), os:putenv("ODBCSYSINI", tmp_dir()), ok catch error:{badmatch, {error, Reason} = Err} -> ?ERROR_MSG("Failed to create temporary files in ~ts: ~ts", [tmp_dir(), file:format_error(Reason)]), Err end; {error, Reason} = Err -> ?ERROR_MSG("Failed to create temporary directory ~ts: ~ts", [tmp_dir(), file:format_error(Reason)]), Err end. write_file_if_new(File, Payload) -> case filelib:is_file(File) of true -> ok; false -> file:write_file(File, Payload) end. tmp_dir() -> case os:type() of {win32, _} -> filename:join([os:getenv("HOME"), "conf"]); _ -> filename:join(["/tmp", "ejabberd"]) end. odbcinst_config() -> filename:join(tmp_dir(), "odbcinst.ini"). max_fsm_queue() -> proplists:get_value(max_queue, fsm_limit_opts(), unlimited). fsm_limit_opts() -> ejabberd_config:fsm_limit_opts([]). query_timeout(LServer) -> ejabberd_option:sql_query_timeout(LServer). current_time() -> erlang:monotonic_time(millisecond). %% ***IMPORTANT*** This error format requires extended_errors turned on. extended_error({"08S01", _, Reason}) -> % TCP Provider: The specified network name is no longer available ?DEBUG("ODBC Link Failure: ~ts", [Reason]), <<"Communication link failure">>; extended_error({"08001", _, Reason}) -> % Login timeout expired ?DEBUG("ODBC Connect Timeout: ~ts", [Reason]), <<"SQL connection failed">>; extended_error({"IMC01", _, Reason}) -> % The connection is broken and recovery is not possible ?DEBUG("ODBC Link Failure: ~ts", [Reason]), <<"Communication link failure">>; extended_error({"IMC06", _, Reason}) -> % The connection is broken and recovery is not possible ?DEBUG("ODBC Link Failure: ~ts", [Reason]), <<"Communication link failure">>; extended_error({Code, _, Reason}) -> ?DEBUG("ODBC Error ~ts: ~ts", [Code, Reason]), iolist_to_binary(Reason); extended_error(Error) -> Error. check_error({error, Why} = Err, _Query) when Why == killed -> Err; check_error({error, Why}, #sql_query{} = Query) -> Err = extended_error(Why), ?ERROR_MSG("SQL query '~ts' at ~p failed: ~p", [Query#sql_query.hash, Query#sql_query.loc, Err]), {error, Err}; check_error({error, Why}, Query) -> Err = extended_error(Why), case catch iolist_to_binary(Query) of SQuery when is_binary(SQuery) -> ?ERROR_MSG("SQL query '~ts' failed: ~p", [SQuery, Err]); _ -> ?ERROR_MSG("SQL query ~p failed: ~p", [Query, Err]) end, {error, Err}; check_error(Result, _Query) -> Result. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_auth.erl����������������������������������������������������������������0000644�0002322�0002322�00000071633�14154362354�017716� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_auth.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Authentication %%% Created : 23 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth). -behaviour(gen_server). -author('alexey@process-one.net'). %% External exports -export([start_link/0, host_up/1, host_down/1, config_reloaded/0, set_password/3, check_password/4, check_password/6, check_password_with_authmodule/4, check_password_with_authmodule/6, try_register/3, get_users/0, get_users/1, password_to_scram/2, get_users/2, import_info/0, count_users/1, import/5, import_start/2, count_users/2, get_password/2, get_password_s/2, get_password_with_authmodule/2, user_exists/2, user_exists_in_other_modules/3, remove_user/2, remove_user/3, plain_password_required/1, store_type/1, entropy/1, backend_type/1, password_format/1, which_users_exists/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([auth_modules/1, convert_to_scram/1]). -include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). -define(SALT_LENGTH, 16). -record(state, {host_modules = #{} :: host_modules()}). -type host_modules() :: #{binary => [module()]}. -type password() :: binary() | #scram{}. -type digest_fun() :: fun((binary()) -> binary()). -export_type([password/0]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -type opts() :: [{prefix, binary()} | {from, integer()} | {to, integer()} | {limit, integer()} | {offset, integer()}]. -callback start(binary()) -> any(). -callback stop(binary()) -> any(). -callback reload(binary()) -> any(). -callback plain_password_required(binary()) -> boolean(). -callback store_type(binary()) -> plain | external | scram. -callback set_password(binary(), binary(), password()) -> {ets_cache:tag(), {ok, password()} | {error, db_failure | not_allowed}}. -callback remove_user(binary(), binary()) -> ok | {error, db_failure | not_allowed}. -callback user_exists(binary(), binary()) -> {ets_cache:tag(), boolean() | {error, db_failure}}. -callback check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. -callback try_register(binary(), binary(), password()) -> {ets_cache:tag(), {ok, password()} | {error, exists | db_failure | not_allowed}}. -callback get_users(binary(), opts()) -> [{binary(), binary()}]. -callback count_users(binary(), opts()) -> number(). -callback get_password(binary(), binary()) -> {ets_cache:tag(), {ok, password()} | error}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> boolean(). -optional_callbacks([reload/1, set_password/3, remove_user/2, user_exists/2, check_password/4, try_register/3, get_users/2, count_users/2, get_password/2, use_cache/1, cache_nodes/1]). -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> ejabberd_hooks:add(host_up, ?MODULE, host_up, 30), ejabberd_hooks:add(host_down, ?MODULE, host_down, 80), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 40), HostModules = lists:foldl( fun(Host, Acc) -> Modules = auth_modules(Host), maps:put(Host, Modules, Acc) end, #{}, ejabberd_option:hosts()), lists:foreach( fun({Host, Modules}) -> start(Host, Modules) end, maps:to_list(HostModules)), init_cache(HostModules), {ok, #state{host_modules = HostModules}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({host_up, Host}, #state{host_modules = HostModules} = State) -> Modules = auth_modules(Host), start(Host, Modules), NewHostModules = maps:put(Host, Modules, HostModules), init_cache(NewHostModules), {noreply, State#state{host_modules = NewHostModules}}; handle_cast({host_down, Host}, #state{host_modules = HostModules} = State) -> Modules = maps:get(Host, HostModules, []), stop(Host, Modules), NewHostModules = maps:remove(Host, HostModules), init_cache(NewHostModules), {noreply, State#state{host_modules = NewHostModules}}; handle_cast(config_reloaded, #state{host_modules = HostModules} = State) -> NewHostModules = lists:foldl( fun(Host, Acc) -> OldModules = maps:get(Host, HostModules, []), NewModules = auth_modules(Host), start(Host, NewModules -- OldModules), stop(Host, OldModules -- NewModules), reload(Host, misc:intersection(OldModules, NewModules)), maps:put(Host, NewModules, Acc) end, HostModules, ejabberd_option:hosts()), init_cache(NewHostModules), {noreply, State#state{host_modules = NewHostModules}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> ejabberd_hooks:delete(host_up, ?MODULE, host_up, 30), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 80), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 40), lists:foreach( fun({Host, Modules}) -> stop(Host, Modules) end, maps:to_list(State#state.host_modules)). code_change(_OldVsn, State, _Extra) -> {ok, State}. start(Host, Modules) -> lists:foreach(fun(M) -> M:start(Host) end, Modules). stop(Host, Modules) -> lists:foreach(fun(M) -> M:stop(Host) end, Modules). reload(Host, Modules) -> lists:foreach( fun(M) -> case erlang:function_exported(M, reload, 1) of true -> M:reload(Host); false -> ok end end, Modules). host_up(Host) -> gen_server:cast(?MODULE, {host_up, Host}). host_down(Host) -> gen_server:cast(?MODULE, {host_down, Host}). config_reloaded() -> gen_server:cast(?MODULE, config_reloaded). -spec plain_password_required(binary()) -> boolean(). plain_password_required(Server) -> lists:any(fun (M) -> M:plain_password_required(Server) end, auth_modules(Server)). -spec store_type(binary()) -> plain | scram | external. store_type(Server) -> case auth_modules(Server) of [ejabberd_auth_anonymous] -> external; Modules -> lists:foldl( fun(ejabberd_auth_anonymous, Type) -> Type; (_, external) -> external; (M, scram) -> case M:store_type(Server) of external -> external; _ -> scram end; (M, plain) -> M:store_type(Server) end, plain, Modules) end. -spec check_password(binary(), binary(), binary(), binary()) -> boolean(). check_password(User, AuthzId, Server, Password) -> check_password(User, AuthzId, Server, Password, <<"">>, undefined). -spec check_password(binary(), binary(), binary(), binary(), binary(), digest_fun() | undefined) -> boolean(). check_password(User, AuthzId, Server, Password, Digest, DigestGen) -> case check_password_with_authmodule( User, AuthzId, Server, Password, Digest, DigestGen) of {true, _AuthModule} -> true; false -> false end. -spec check_password_with_authmodule(binary(), binary(), binary(), binary()) -> false | {true, atom()}. check_password_with_authmodule(User, AuthzId, Server, Password) -> check_password_with_authmodule( User, AuthzId, Server, Password, <<"">>, undefined). -spec check_password_with_authmodule( binary(), binary(), binary(), binary(), binary(), digest_fun() | undefined) -> false | {true, atom()}. check_password_with_authmodule(User, AuthzId, Server, Password, Digest, DigestGen) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> case jid:nodeprep(AuthzId) of error -> false; LAuthzId -> untag_stop( lists:foldl( fun(Mod, false) -> case db_check_password( LUser, LAuthzId, LServer, Password, Digest, DigestGen, Mod) of true -> {true, Mod}; false -> false; {stop, true} -> {stop, {true, Mod}}; {stop, false} -> {stop, false} end; (_, Acc) -> Acc end, false, auth_modules(LServer))) end; _ -> false end. -spec set_password(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | invalid_jid | invalid_password}. set_password(User, Server, Password) -> case validate_credentials(User, Server, Password) of {ok, LUser, LServer} -> lists:foldl( fun(M, {error, _}) -> db_set_password(LUser, LServer, Password, M); (_, ok) -> ok end, {error, not_allowed}, auth_modules(LServer)); Err -> Err end. -spec try_register(binary(), binary(), password()) -> ok | {error, db_failure | not_allowed | exists | invalid_jid | invalid_password}. try_register(User, Server, Password) -> case validate_credentials(User, Server, Password) of {ok, LUser, LServer} -> case user_exists(LUser, LServer) of true -> {error, exists}; false -> case ejabberd_router:is_my_host(LServer) of true -> case lists:foldl( fun(_, ok) -> ok; (Mod, _) -> db_try_register( LUser, LServer, Password, Mod) end, {error, not_allowed}, auth_modules(LServer)) of ok -> ejabberd_hooks:run( register_user, LServer, [LUser, LServer]); {error, _} = Err -> Err end; false -> {error, not_allowed} end end; Err -> Err end. -spec get_users() -> [{binary(), binary()}]. get_users() -> lists:flatmap( fun({Host, Mod}) -> db_get_users(Host, [], Mod) end, auth_modules()). -spec get_users(binary()) -> [{binary(), binary()}]. get_users(Server) -> get_users(Server, []). -spec get_users(binary(), opts()) -> [{binary(), binary()}]. get_users(Server, Opts) -> case jid:nameprep(Server) of error -> []; LServer -> lists:flatmap( fun(M) -> db_get_users(LServer, Opts, M) end, auth_modules(LServer)) end. -spec count_users(binary()) -> non_neg_integer(). count_users(Server) -> count_users(Server, []). -spec count_users(binary(), opts()) -> non_neg_integer(). count_users(Server, Opts) -> case jid:nameprep(Server) of error -> 0; LServer -> lists:sum( lists:map( fun(M) -> db_count_users(LServer, Opts, M) end, auth_modules(LServer))) end. -spec get_password(binary(), binary()) -> false | password(). get_password(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> case lists:foldl( fun(M, error) -> db_get_password(LUser, LServer, M); (_M, Acc) -> Acc end, error, auth_modules(LServer)) of {ok, Password} -> Password; error -> false end; _ -> false end. -spec get_password_s(binary(), binary()) -> password(). get_password_s(User, Server) -> case get_password(User, Server) of false -> <<"">>; Password -> Password end. -spec get_password_with_authmodule(binary(), binary()) -> {false | password(), module()}. get_password_with_authmodule(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> case lists:foldl( fun(M, {error, _}) -> {db_get_password(LUser, LServer, M), M}; (_M, Acc) -> Acc end, {error, undefined}, auth_modules(LServer)) of {{ok, Password}, Module} -> {Password, Module}; {error, Module} -> {false, Module} end; _ -> {false, undefined} end. -spec user_exists(binary(), binary()) -> boolean(). user_exists(_User, <<"">>) -> false; user_exists(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> lists:any( fun(M) -> case db_user_exists(LUser, LServer, M) of {error, _} -> false; Else -> Else end end, auth_modules(LServer)); _ -> false end. -spec user_exists_in_other_modules(atom(), binary(), binary()) -> boolean() | maybe. user_exists_in_other_modules(Module, User, Server) -> user_exists_in_other_modules_loop( auth_modules(Server) -- [Module], User, Server). user_exists_in_other_modules_loop([], _User, _Server) -> false; user_exists_in_other_modules_loop([AuthModule | AuthModules], User, Server) -> case db_user_exists(User, Server, AuthModule) of true -> true; false -> user_exists_in_other_modules_loop(AuthModules, User, Server); {error, _} -> maybe end. -spec which_users_exists(list({binary(), binary()})) -> list({binary(), binary()}). which_users_exists(USPairs) -> ByServer = lists:foldl( fun({User, Server}, Dict) -> LServer = jid:nameprep(Server), LUser = jid:nodeprep(User), case gb_trees:lookup(LServer, Dict) of none -> gb_trees:insert(LServer, gb_sets:singleton(LUser), Dict); {value, Set} -> gb_trees:update(LServer, gb_sets:add(LUser, Set), Dict) end end, gb_trees:empty(), USPairs), Set = lists:foldl( fun({LServer, UsersSet}, Results) -> UsersList = gb_sets:to_list(UsersSet), lists:foldl( fun(M, Results2) -> try M:which_users_exists(LServer, UsersList) of {error, _} -> Results2; Res -> gb_sets:union( gb_sets:from_list([{U, LServer} || U <- Res]), Results2) catch _:undef -> lists:foldl( fun(U, R2) -> case user_exists(U, LServer) of true -> gb_sets:add({U, LServer}, R2); _ -> R2 end end, Results2, UsersList) end end, Results, auth_modules(LServer)) end, gb_sets:empty(), gb_trees:to_list(ByServer)), gb_sets:to_list(Set). -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> case validate_credentials(User, Server) of {ok, LUser, LServer} -> lists:foreach( fun(Mod) -> db_remove_user(LUser, LServer, Mod) end, auth_modules(LServer)), ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); _Err -> ok end. -spec remove_user(binary(), binary(), password()) -> ok | {error, atom()}. remove_user(User, Server, Password) -> case validate_credentials(User, Server, Password) of {ok, LUser, LServer} -> case lists:foldl( fun (_, ok) -> ok; (Mod, _) -> case db_check_password( LUser, <<"">>, LServer, Password, <<"">>, undefined, Mod) of true -> db_remove_user(LUser, LServer, Mod); {stop, true} -> db_remove_user(LUser, LServer, Mod); false -> {error, not_allowed}; {stop, false} -> {error, not_allowed} end end, {error, not_allowed}, auth_modules(Server)) of ok -> ejabberd_hooks:run( remove_user, LServer, [LUser, LServer]); Err -> Err end; Err -> Err end. %% @doc Calculate informational entropy. -spec entropy(iodata()) -> float(). entropy(B) -> case binary_to_list(B) of "" -> 0.0; S -> Set = lists:foldl(fun (C, [Digit, Printable, LowLetter, HiLetter, Other]) -> if C >= $a, C =< $z -> [Digit, Printable, 26, HiLetter, Other]; C >= $0, C =< $9 -> [9, Printable, LowLetter, HiLetter, Other]; C >= $A, C =< $Z -> [Digit, Printable, LowLetter, 26, Other]; C >= 33, C =< 126 -> [Digit, 33, LowLetter, HiLetter, Other]; true -> [Digit, Printable, LowLetter, HiLetter, 128] end end, [0, 0, 0, 0, 0], S), length(S) * math:log(lists:sum(Set)) / math:log(2) end. -spec backend_type(atom()) -> atom(). backend_type(Mod) -> case atom_to_list(Mod) of "ejabberd_auth_" ++ T -> list_to_atom(T); _ -> Mod end. -spec password_format(binary() | global) -> plain | scram. password_format(LServer) -> ejabberd_option:auth_password_format(LServer). %%%---------------------------------------------------------------------- %%% Backend calls %%%---------------------------------------------------------------------- -spec db_try_register(binary(), binary(), password(), module()) -> ok | {error, exists | db_failure | not_allowed}. db_try_register(User, Server, Password, Mod) -> case erlang:function_exported(Mod, try_register, 3) of true -> Password1 = case Mod:store_type(Server) of scram -> password_to_scram(Server, Password); _ -> Password end, Ret = case use_cache(Mod, Server) of true -> ets_cache:update( cache_tab(Mod), {User, Server}, {ok, Password}, fun() -> Mod:try_register(User, Server, Password1) end, cache_nodes(Mod, Server)); false -> ets_cache:untag(Mod:try_register(User, Server, Password1)) end, case Ret of {ok, _} -> ok; {error, _} = Err -> Err end; false -> {error, not_allowed} end. -spec db_set_password(binary(), binary(), password(), module()) -> ok | {error, db_failure | not_allowed}. db_set_password(User, Server, Password, Mod) -> case erlang:function_exported(Mod, set_password, 3) of true -> Password1 = case Mod:store_type(Server) of scram -> password_to_scram(Server, Password); _ -> Password end, Ret = case use_cache(Mod, Server) of true -> ets_cache:update( cache_tab(Mod), {User, Server}, {ok, Password}, fun() -> Mod:set_password(User, Server, Password1) end, cache_nodes(Mod, Server)); false -> ets_cache:untag(Mod:set_password(User, Server, Password1)) end, case Ret of {ok, _} -> ok; {error, _} = Err -> Err end; false -> {error, not_allowed} end. db_get_password(User, Server, Mod) -> UseCache = use_cache(Mod, Server), case erlang:function_exported(Mod, get_password, 2) of false when UseCache -> case ets_cache:lookup(cache_tab(Mod), {User, Server}) of {ok, exists} -> error; not_found -> error; Other -> Other end; false -> error; true when UseCache -> ets_cache:lookup( cache_tab(Mod), {User, Server}, fun() -> Mod:get_password(User, Server) end); true -> ets_cache:untag(Mod:get_password(User, Server)) end. db_user_exists(User, Server, Mod) -> case db_get_password(User, Server, Mod) of {ok, _} -> true; not_found -> false; error -> case {Mod:store_type(Server), use_cache(Mod, Server)} of {external, true} -> Val = case ets_cache:lookup(cache_tab(Mod), {User, Server}, error) of error -> ets_cache:update(cache_tab(Mod), {User, Server}, {ok, exists}, fun() -> case Mod:user_exists(User, Server) of {CacheTag, true} -> {CacheTag, {ok, exists}}; {CacheTag, false} -> {CacheTag, not_found}; {_, {error, _}} = Err -> Err end end); Other -> Other end, case Val of {ok, _} -> true; not_found -> false; error -> false; {error, _} = Err -> Err end; {external, false} -> ets_cache:untag(Mod:user_exists(User, Server)); _ -> false end end. db_check_password(User, AuthzId, Server, ProvidedPassword, Digest, DigestFun, Mod) -> case db_get_password(User, Server, Mod) of {ok, ValidPassword} -> match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun); error -> case {Mod:store_type(Server), use_cache(Mod, Server)} of {external, true} -> case ets_cache:update( cache_tab(Mod), {User, Server}, {ok, ProvidedPassword}, fun() -> case Mod:check_password( User, AuthzId, Server, ProvidedPassword) of {CacheTag, true} -> {CacheTag, {ok, ProvidedPassword}}; {CacheTag, {stop, true}} -> {CacheTag, {ok, ProvidedPassword}}; {CacheTag, false} -> {CacheTag, error}; {CacheTag, {stop, false}} -> {CacheTag, error} end end) of {ok, _} -> true; error -> false end; {external, false} -> ets_cache:untag( Mod:check_password(User, AuthzId, Server, ProvidedPassword)); _ -> false end end. db_remove_user(User, Server, Mod) -> case erlang:function_exported(Mod, remove_user, 2) of true -> case Mod:remove_user(User, Server) of ok -> case use_cache(Mod, Server) of true -> ets_cache:delete(cache_tab(Mod), {User, Server}, cache_nodes(Mod, Server)); false -> ok end; {error, _} = Err -> Err end; false -> {error, not_allowed} end. db_get_users(Server, Opts, Mod) -> case erlang:function_exported(Mod, get_users, 2) of true -> Mod:get_users(Server, Opts); false -> case use_cache(Mod, Server) of true -> ets_cache:fold( fun({User, S}, {ok, _}, Users) when S == Server -> [{User, Server}|Users]; (_, _, Users) -> Users end, [], cache_tab(Mod)); false -> [] end end. db_count_users(Server, Opts, Mod) -> case erlang:function_exported(Mod, count_users, 2) of true -> Mod:count_users(Server, Opts); false -> case use_cache(Mod, Server) of true -> ets_cache:fold( fun({_, S}, {ok, _}, Num) when S == Server -> Num + 1; (_, _, Num) -> Num end, 0, cache_tab(Mod)); false -> 0 end end. %%%---------------------------------------------------------------------- %%% SCRAM stuff %%%---------------------------------------------------------------------- is_password_scram_valid(Password, Scram) -> case jid:resourceprep(Password) of error -> false; _ -> IterationCount = Scram#scram.iterationcount, Hash = Scram#scram.hash, Salt = base64:decode(Scram#scram.salt), SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), base64:decode(Scram#scram.storedkey) == StoredKey end. password_to_scram(Host, Password) -> password_to_scram(Host, Password, ?SCRAM_DEFAULT_ITERATION_COUNT). password_to_scram(_Host, #scram{} = Password, _IterationCount) -> Password; password_to_scram(Host, Password, IterationCount) -> Hash = ejabberd_option:auth_scram_hash(Host), Salt = p1_rand:bytes(?SALT_LENGTH), SaltedPassword = scram:salted_password(Hash, Password, Salt, IterationCount), StoredKey = scram:stored_key(Hash, scram:client_key(Hash, SaltedPassword)), ServerKey = scram:server_key(Hash, SaltedPassword), #scram{storedkey = base64:encode(StoredKey), serverkey = base64:encode(ServerKey), salt = base64:encode(Salt), hash = Hash, iterationcount = IterationCount}. %%%---------------------------------------------------------------------- %%% Cache stuff %%%---------------------------------------------------------------------- -spec init_cache(host_modules()) -> ok. init_cache(HostModules) -> CacheOpts = cache_opts(), {True, False} = use_cache(HostModules), lists:foreach( fun(Module) -> ets_cache:new(cache_tab(Module), CacheOpts) end, True), lists:foreach( fun(Module) -> ets_cache:delete(cache_tab(Module)) end, False). -spec cache_opts() -> [proplists:property()]. cache_opts() -> MaxSize = ejabberd_option:auth_cache_size(), CacheMissed = ejabberd_option:auth_cache_missed(), LifeTime = ejabberd_option:auth_cache_life_time(), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(host_modules()) -> {True :: [module()], False :: [module()]}. use_cache(HostModules) -> {Enabled, Disabled} = maps:fold( fun(Host, Modules, Acc) -> lists:foldl( fun(Module, {True, False}) -> case use_cache(Module, Host) of true -> {sets:add_element(Module, True), False}; false -> {True, sets:add_element(Module, False)} end end, Acc, Modules) end, {sets:new(), sets:new()}, HostModules), {sets:to_list(Enabled), sets:to_list(sets:subtract(Disabled, Enabled))}. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, LServer) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(LServer); false -> ejabberd_option:auth_use_cache(LServer) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, LServer) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(LServer); false -> ejabberd_cluster:get_nodes() end. -spec cache_tab(module()) -> atom(). cache_tab(Mod) -> list_to_atom(atom_to_list(Mod) ++ "_cache"). %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- -spec auth_modules() -> [{binary(), module()}]. auth_modules() -> lists:flatmap( fun(Host) -> [{Host, Mod} || Mod <- auth_modules(Host)] end, ejabberd_option:hosts()). -spec auth_modules(binary()) -> [module()]. auth_modules(Server) -> LServer = jid:nameprep(Server), Methods = ejabberd_option:auth_method(LServer), [ejabberd:module_name([<<"ejabberd">>, <<"auth">>, misc:atom_to_binary(M)]) || M <- Methods]. -spec match_passwords(password(), password(), binary(), digest_fun() | undefined) -> boolean(). match_passwords(Password, #scram{} = Scram, <<"">>, undefined) -> is_password_scram_valid(Password, Scram); match_passwords(Password, #scram{} = Scram, Digest, DigestFun) -> StoredKey = base64:decode(Scram#scram.storedkey), DigRes = if Digest /= <<"">> -> Digest == DigestFun(StoredKey); true -> false end, if DigRes -> true; true -> StoredKey == Password andalso Password /= <<"">> end; match_passwords(ProvidedPassword, ValidPassword, <<"">>, undefined) -> ProvidedPassword == ValidPassword andalso ProvidedPassword /= <<"">>; match_passwords(ProvidedPassword, ValidPassword, Digest, DigestFun) -> DigRes = if Digest /= <<"">> -> Digest == DigestFun(ValidPassword); true -> false end, if DigRes -> true; true -> ValidPassword == ProvidedPassword andalso ProvidedPassword /= <<"">> end. -spec validate_credentials(binary(), binary()) -> {ok, binary(), binary()} | {error, invalid_jid}. validate_credentials(User, Server) -> validate_credentials(User, Server, #scram{}). -spec validate_credentials(binary(), binary(), password()) -> {ok, binary(), binary()} | {error, invalid_jid | invalid_password}. validate_credentials(_User, _Server, <<"">>) -> {error, invalid_password}; validate_credentials(User, Server, Password) -> case jid:nodeprep(User) of error -> {error, invalid_jid}; LUser -> case jid:nameprep(Server) of error -> {error, invalid_jid}; LServer -> if is_record(Password, scram) -> {ok, LUser, LServer}; true -> case jid:resourceprep(Password) of error -> {error, invalid_password}; _ -> {ok, LUser, LServer} end end end end. untag_stop({stop, Val}) -> Val; untag_stop(Val) -> Val. import_info() -> [{<<"users">>, 3}]. import_start(_LServer, mnesia) -> ejabberd_auth_mnesia:init_db(); import_start(_LServer, _) -> ok. import(Server, {sql, _}, mnesia, <<"users">>, Fields) -> ejabberd_auth_mnesia:import(Server, Fields); import(_LServer, {sql, _}, sql, <<"users">>, _) -> ok. -spec convert_to_scram(binary()) -> {error, any()} | ok. convert_to_scram(Server) -> LServer = jid:nameprep(Server), if LServer == error; LServer == <<>> -> {error, {incorrect_server_name, Server}}; true -> lists:foreach( fun({U, S}) -> case get_password(U, S) of Pass when is_binary(Pass) -> SPass = password_to_scram(Server, Pass), set_password(U, S, SPass); _ -> ok end end, get_users(LServer)), ok end. �����������������������������������������������������������������������������������������������������ejabberd-21.12/src/ejabberd_router.erl��������������������������������������������������������������0000644�0002322�0002322�00000036771�14154362354�020301� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : ejabberd_router.erl %%% Author : Alexey Shchepin <alexey@process-one.net> %%% Purpose : Main router %%% Created : 27 Nov 2002 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_router). -author('alexey@process-one.net'). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). %% API -export([route/1, route_error/2, route_iq/2, route_iq/3, route_iq/4, register_route/2, register_route/3, register_route/4, register_routes/1, host_of_route/1, process_iq/1, unregister_route/1, unregister_route/2, unregister_routes/1, get_all_routes/0, is_my_route/1, is_my_host/1, clean_cache/1, config_reloaded/0, get_backend/0]). -export([start_link/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Deprecated functions -export([route/3, route_error/4]). -deprecated([{route, 3}, {route_error, 4}]). %% This value is used in SIP and Megaco for a transaction lifetime. -define(IQ_TIMEOUT, 32000). -define(CALL_TIMEOUT, timer:minutes(10)). -include("logger.hrl"). -include("ejabberd_router.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_stacktrace.hrl"). -callback init() -> any(). -callback register_route(binary(), binary(), local_hint(), undefined | pos_integer(), pid()) -> ok | {error, term()}. -callback unregister_route(binary(), undefined | pos_integer(), pid()) -> ok | {error, term()}. -callback find_routes(binary()) -> {ok, [#route{}]} | {error, any()}. -callback get_all_routes() -> {ok, [binary()]} | {error, any()}. -record(state, {route_monitors = #{} :: #{{binary(), pid()} => reference()}}). %%==================================================================== %% API %%==================================================================== start_link() -> ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). -spec route(stanza()) -> ok. route(Packet) -> try do_route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end. -spec route(jid(), jid(), xmlel() | stanza()) -> ok. route(#jid{} = From, #jid{} = To, #xmlel{} = El) -> try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of Pkt -> route(From, To, Pkt) catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode xml element ~p when " "routing from ~ts to ~ts: ~ts", [El, jid:encode(From), jid:encode(To), xmpp:format_error(Why)]) end; route(#jid{} = From, #jid{} = To, Packet) -> route(xmpp:set_from_to(Packet, From, To)). -spec route_error(stanza(), stanza_error()) -> ok. route_error(Packet, Err) -> Type = xmpp:get_type(Packet), if Type == error; Type == result -> ok; true -> route(xmpp:make_error(Packet, Err)) end. %% Route the error packet only if the originating packet is not an error itself. %% RFC3920 9.3.1 -spec route_error(jid(), jid(), xmlel(), xmlel()) -> ok; (jid(), jid(), stanza(), stanza_error()) -> ok. route_error(From, To, #xmlel{} = ErrPacket, #xmlel{} = OrigPacket) -> #xmlel{attrs = Attrs} = OrigPacket, case <<"error">> == fxml:get_attr_s(<<"type">>, Attrs) of false -> route(From, To, ErrPacket); true -> ok end; route_error(From, To, Packet, #stanza_error{} = Err) -> Type = xmpp:get_type(Packet), if Type == error; Type == result -> ok; true -> route(From, To, xmpp:make_error(Packet, Err)) end. -spec route_iq(iq(), fun((iq() | timeout) -> any())) -> ok. route_iq(IQ, Fun) -> route_iq(IQ, Fun, undefined, ?IQ_TIMEOUT). -spec route_iq(iq(), term(), pid() | atom()) -> ok. route_iq(IQ, State, Proc) -> route_iq(IQ, State, Proc, ?IQ_TIMEOUT). -spec route_iq(iq(), term(), pid() | atom(), undefined | non_neg_integer()) -> ok. route_iq(IQ, State, Proc, undefined) -> route_iq(IQ, State, Proc, ?IQ_TIMEOUT); route_iq(IQ, State, Proc, Timeout) -> ejabberd_iq:route(IQ, Proc, State, Timeout). -spec register_route(binary(), binary()) -> ok. register_route(Domain, ServerHost) -> register_route(Domain, ServerHost, undefined). -spec register_route(binary(), binary(), local_hint() | undefined) -> ok. register_route(Domain, ServerHost, LocalHint) -> register_route(Domain, ServerHost, LocalHint, self()). -spec register_route(binary(), binary(), local_hint() | undefined, pid()) -> ok. register_route(Domain, ServerHost, LocalHint, Pid) -> case {jid:nameprep(Domain), jid:nameprep(ServerHost)} of {error, _} -> erlang:error({invalid_domain, Domain}); {_, error} -> erlang:error({invalid_domain, ServerHost}); {LDomain, LServerHost} -> Mod = get_backend(), case Mod:register_route(LDomain, LServerHost, LocalHint, get_component_number(LDomain), Pid) of ok -> ?DEBUG("Route registered: ~ts", [LDomain]), monitor_route(LDomain, Pid), ejabberd_hooks:run(route_registered, [LDomain]), delete_cache(Mod, LDomain); {error, Err} -> ?ERROR_MSG("Failed to register route ~ts: ~p", [LDomain, Err]) end end. -spec register_routes([{binary(), binary()}]) -> ok. register_routes(Domains) -> lists:foreach(fun ({Domain, ServerHost}) -> register_route(Domain, ServerHost) end, Domains). -spec unregister_route(binary()) -> ok. unregister_route(Domain) -> unregister_route(Domain, self()). -spec unregister_route(binary(), pid()) -> ok. unregister_route(Domain, Pid) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> Mod = get_backend(), case Mod:unregister_route( LDomain, get_component_number(LDomain), Pid) of ok -> ?DEBUG("Route unregistered: ~ts", [LDomain]), demonitor_route(LDomain, Pid), ejabberd_hooks:run(route_unregistered, [LDomain]), delete_cache(Mod, LDomain); {error, Err} -> ?ERROR_MSG("Failed to unregister route ~ts: ~p", [LDomain, Err]) end end. -spec unregister_routes([binary()]) -> ok. unregister_routes(Domains) -> lists:foreach(fun (Domain) -> unregister_route(Domain) end, Domains). -spec find_routes(binary()) -> [#route{}]. find_routes(Domain) -> Mod = get_backend(), case use_cache(Mod) of true -> case ets_cache:lookup( ?ROUTES_CACHE, {route, Domain}, fun() -> case Mod:find_routes(Domain) of {ok, Rs} when Rs /= [] -> {ok, Rs}; _ -> error end end) of {ok, Rs} -> Rs; error -> [] end; false -> case Mod:find_routes(Domain) of {ok, Rs} -> Rs; _ -> [] end end. -spec get_all_routes() -> [binary()]. get_all_routes() -> Mod = get_backend(), case use_cache(Mod) of true -> case ets_cache:lookup( ?ROUTES_CACHE, routes, fun() -> case Mod:get_all_routes() of {ok, Rs} when Rs /= [] -> {ok, Rs}; _ -> error end end) of {ok, Rs} -> Rs; error -> [] end; false -> case Mod:get_all_routes() of {ok, Rs} -> Rs; _ -> [] end end. -spec host_of_route(binary()) -> binary(). host_of_route(Domain) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> case find_routes(LDomain) of [#route{server_host = ServerHost}|_] -> ServerHost; _ -> erlang:error({unregistered_route, Domain}) end end. -spec is_my_route(binary()) -> boolean(). is_my_route(Domain) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> find_routes(LDomain) /= [] end. -spec is_my_host(binary()) -> boolean(). is_my_host(Domain) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> case find_routes(LDomain) of [#route{server_host = LDomain}|_] -> true; _ -> false end end. -spec process_iq(iq()) -> any(). process_iq(IQ) -> gen_iq_handler:handle(IQ). -spec config_reloaded() -> ok. config_reloaded() -> Mod = get_backend(), init_cache(Mod). %%==================================================================== %% gen_server callbacks %%==================================================================== init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), Mod = get_backend(), init_cache(Mod), Mod:init(), clean_cache(), {ok, #state{}}. handle_call({monitor, Domain, Pid}, _From, State) -> MRefs = State#state.route_monitors, MRefs1 = case maps:is_key({Domain, Pid}, MRefs) of true -> MRefs; false -> MRef = erlang:monitor(process, Pid), MRefs#{{Domain, Pid} => MRef} end, {reply, ok, State#state{route_monitors = MRefs1}}; handle_call({demonitor, Domain, Pid}, _From, State) -> MRefs = State#state.route_monitors, MRefs1 = case maps:find({Domain, Pid}, MRefs) of {ok, MRef} -> erlang:demonitor(MRef, [flush]), maps:remove({Domain, Pid}, MRefs); error -> MRefs end, {reply, ok, State#state{route_monitors = MRefs1}}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({route, Packet}, State) -> route(Packet), {noreply, State}; handle_info({'DOWN', MRef, _, Pid, Info}, State) -> MRefs = maps:filter( fun({Domain, P}, M) when P == Pid, M == MRef -> ?DEBUG("Process ~p with route registered to ~ts " "has terminated unexpectedly with reason: ~p", [P, Domain, Info]), try unregister_route(Domain, Pid) catch _:_ -> ok end, false; (_, _) -> true end, State#state.route_monitors), {noreply, State#state{route_monitors = MRefs}}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec do_route(stanza()) -> ok. do_route(OrigPacket) -> ?DEBUG("Route:~n~ts", [xmpp:pp(OrigPacket)]), case ejabberd_hooks:run_fold(filter_packet, OrigPacket, []) of drop -> ok; Packet -> case ejabberd_iq:dispatch(Packet) of true -> ok; false -> To = xmpp:get_to(Packet), LDstDomain = To#jid.lserver, case find_routes(LDstDomain) of [] -> ejabberd_s2s:route(Packet); [Route] -> do_route(Packet, Route); Routes -> From = xmpp:get_from(Packet), balancing_route(From, To, Packet, Routes) end, ok end end. -spec do_route(stanza(), #route{}) -> any(). do_route(Pkt, #route{local_hint = LocalHint, pid = Pid}) when is_pid(Pid) -> case LocalHint of {apply, Module, Function} when node(Pid) == node() -> Module:Function(Pkt); _ -> ejabberd_cluster:send(Pid, {route, Pkt}) end; do_route(_Pkt, _Route) -> ok. -spec balancing_route(jid(), jid(), stanza(), [#route{}]) -> any(). balancing_route(From, To, Packet, Rs) -> case get_domain_balancing(From, To, To#jid.lserver) of undefined -> Value = erlang:system_time(), case [R || R <- Rs, node(R#route.pid) == node()] of [] -> R = lists:nth(erlang:phash2(Value, length(Rs))+1, Rs), do_route(Packet, R); LRs -> R = lists:nth(erlang:phash2(Value, length(LRs))+1, LRs), do_route(Packet, R) end; Value -> SRs = lists:ukeysort(#route.local_hint, Rs), R = lists:nth(erlang:phash2(Value, length(SRs))+1, SRs), do_route(Packet, R) end. -spec get_component_number(binary()) -> pos_integer() | undefined. get_component_number(LDomain) -> M = ejabberd_option:domain_balancing(), case maps:get(LDomain, M, undefined) of undefined -> undefined; Opts -> maps:get(component_number, Opts) end. -spec get_domain_balancing(jid(), jid(), binary()) -> integer() | ljid() | undefined. get_domain_balancing(From, To, LDomain) -> M = ejabberd_option:domain_balancing(), case maps:get(LDomain, M, undefined) of undefined -> undefined; Opts -> case maps:get(type, Opts, random) of random -> erlang:system_time(); source -> jid:tolower(From); destination -> jid:tolower(To); bare_source -> jid:remove_resource(jid:tolower(From)); bare_destination -> jid:remove_resource(jid:tolower(To)) end end. -spec monitor_route(binary(), pid()) -> ok. monitor_route(Domain, Pid) -> ?GEN_SERVER:call(?MODULE, {monitor, Domain, Pid}, ?CALL_TIMEOUT). -spec demonitor_route(binary(), pid()) -> ok. demonitor_route(Domain, Pid) -> case whereis(?MODULE) == self() of true -> ok; false -> ?GEN_SERVER:call(?MODULE, {demonitor, Domain, Pid}, ?CALL_TIMEOUT) end. -spec get_backend() -> module(). get_backend() -> DBType = ejabberd_option:router_db_type(), list_to_existing_atom("ejabberd_router_" ++ atom_to_list(DBType)). -spec cache_nodes(module()) -> [node()]. cache_nodes(Mod) -> case erlang:function_exported(Mod, cache_nodes, 0) of true -> Mod:cache_nodes(); false -> ejabberd_cluster:get_nodes() end. -spec use_cache(module()) -> boolean(). use_cache(Mod) -> case erlang:function_exported(Mod, use_cache, 0) of true -> Mod:use_cache(); false -> ejabberd_option:router_use_cache() end. -spec delete_cache(module(), binary()) -> ok. delete_cache(Mod, Domain) -> case use_cache(Mod) of true -> ets_cache:delete(?ROUTES_CACHE, {route, Domain}, cache_nodes(Mod)), ets_cache:delete(?ROUTES_CACHE, routes, cache_nodes(Mod)); false -> ok end. -spec init_cache(module()) -> ok. init_cache(Mod) -> case use_cache(Mod) of true -> ets_cache:new(?ROUTES_CACHE, cache_opts()); false -> ets_cache:delete(?ROUTES_CACHE) end. -spec cache_opts() -> [proplists:property()]. cache_opts() -> MaxSize = ejabberd_option:router_cache_size(), CacheMissed = ejabberd_option:router_cache_missed(), LifeTime = ejabberd_option:router_cache_life_time(), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec clean_cache(node()) -> non_neg_integer(). clean_cache(Node) -> ets_cache:filter( ?ROUTES_CACHE, fun(_, error) -> false; (routes, _) -> false; ({route, _}, {ok, Rs}) -> not lists:any( fun(#route{pid = Pid}) -> node(Pid) == Node end, Rs) end). -spec clean_cache() -> ok. clean_cache() -> ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]). �������ejabberd-21.12/src/pubsub_migrate.erl���������������������������������������������������������������0000644�0002322�0002322�00000040052�14154362354�020136� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : pubsub_migrate.erl %%% Author : Christophe Romain <christophe.romain@process-one.net> %%% Purpose : Migration/Upgrade code put out of mod_pubsub %%% Created : 26 Jul 2014 by Christophe Romain <christophe.romain@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(pubsub_migrate). -dialyzer({no_return, report_and_stop/2}). -include("pubsub.hrl"). -include("logger.hrl"). -export([update_node_database/2, update_state_database/2]). -export([update_item_database/2, update_lastitem_database/2]). update_item_database(_Host, _ServerHost) -> convert_list_items(). update_node_database(Host, ServerHost) -> mnesia:del_table_index(pubsub_node, type), mnesia:del_table_index(pubsub_node, parentid), case catch mnesia:table_info(pubsub_node, attributes) of [host_node, host_parent, info] -> ?INFO_MSG("Upgrading pubsub nodes table...", []), F = fun () -> {Result, LastIdx} = lists:foldl(fun ({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, {RecList, NodeIdx}) -> ItemsList = lists:foldl(fun ({item, IID, Publisher, Payload}, Acc) -> C = {unknown, Publisher}, M = {erlang:timestamp(), Publisher}, mnesia:write(#pubsub_item{itemid = {IID, NodeIdx}, creation = C, modification = M, payload = Payload}), [{Publisher, IID} | Acc] end, [], Items), Owners = dict:fold(fun (JID, {entity, Aff, Sub}, Acc) -> UsrItems = lists:foldl(fun ({P, I}, IAcc) -> case P of JID -> [I | IAcc]; _ -> IAcc end end, [], ItemsList), mnesia:write({pubsub_state, {JID, NodeIdx}, UsrItems, Aff, Sub}), case Aff of owner -> [JID | Acc]; _ -> Acc end end, [], Entities), mnesia:delete({pubsub_node, NodeId}), {[#pubsub_node{nodeid = NodeId, id = NodeIdx, parents = [element(2, ParentId)], owners = Owners, options = Options} | RecList], NodeIdx + 1} end, {[], 1}, mnesia:match_object({pubsub_node, {Host, '_'}, '_', '_'})), mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}), Result end, {atomic, NewRecords} = mnesia:transaction(F), {atomic, ok} = mnesia:delete_table(pubsub_node), {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_node, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_node)}]), FNew = fun () -> lists:foreach(fun (Record) -> mnesia:write(Record) end, NewRecords) end, case mnesia:transaction(FNew) of {atomic, Result} -> ?INFO_MSG("Pubsub nodes table upgraded: ~p", [Result]); {aborted, Reason} -> ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", [Reason]) end; [nodeid, parentid, type, owners, options] -> F = fun ({pubsub_node, NodeId, {_, Parent}, Type, Owners, Options}) -> #pubsub_node{nodeid = NodeId, id = 0, parents = [Parent], type = Type, owners = Owners, options = Options} end, mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), FNew = fun () -> LastIdx = lists:foldl(fun (#pubsub_node{nodeid = NodeId} = PubsubNode, NodeIdx) -> mnesia:write(PubsubNode#pubsub_node{id = NodeIdx}), lists:foreach(fun (#pubsub_state{stateid = StateId} = State) -> {JID, _} = StateId, mnesia:delete({pubsub_state, StateId}), mnesia:write(State#pubsub_state{stateid = {JID, NodeIdx}}) end, mnesia:match_object(#pubsub_state{stateid = {'_', NodeId}, _ = '_'})), lists:foreach(fun (#pubsub_item{itemid = ItemId} = Item) -> {IID, _} = ItemId, {M1, M2} = Item#pubsub_item.modification, {C1, C2} = Item#pubsub_item.creation, mnesia:delete({pubsub_item, ItemId}), mnesia:write(Item#pubsub_item{itemid = {IID, NodeIdx}, modification = {M2, M1}, creation = {C2, C1}}) end, mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'})), NodeIdx + 1 end, 1, mnesia:match_object({pubsub_node, {Host, '_'}, '_', '_', '_', '_', '_'}) ++ mnesia:match_object({pubsub_node, {{'_', ServerHost, '_'}, '_'}, '_', '_', '_', '_', '_'})), mnesia:write(#pubsub_index{index = node, last = LastIdx, free = []}) end, case mnesia:transaction(FNew) of {atomic, Result} -> rename_default_nodeplugin(), ?INFO_MSG("Pubsub nodes table upgraded: ~p", [Result]); {aborted, Reason} -> ?ERROR_MSG("Problem upgrading Pubsub nodes table:~n~p", [Reason]) end; [nodeid, id, parent, type, owners, options] -> F = fun ({pubsub_node, NodeId, Id, Parent, Type, Owners, Options}) -> #pubsub_node{nodeid = NodeId, id = Id, parents = [Parent], type = Type, owners = Owners, options = Options} end, mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), rename_default_nodeplugin(); _ -> ok end, convert_list_nodes(). rename_default_nodeplugin() -> lists:foreach(fun (Node) -> mnesia:dirty_write(Node#pubsub_node{type = <<"hometree">>}) end, mnesia:dirty_match_object(#pubsub_node{type = <<"default">>, _ = '_'})). update_state_database(_Host, _ServerHost) -> % useless starting from ejabberd 17.04 % case catch mnesia:table_info(pubsub_state, attributes) of % [stateid, nodeidx, items, affiliation, subscriptions] -> % ?INFO_MSG("Upgrading pubsub states table...", []), % F = fun ({pubsub_state, {{U,S,R}, NodeID}, _NodeIdx, Items, Aff, Sub}, Acc) -> % JID = {U,S,R}, % Subs = case Sub of % none -> % []; % [] -> % []; % _ -> % SubID = pubsub_subscription:make_subid(), % [{Sub, SubID}] % end, % NewState = #pubsub_state{stateid = {JID, NodeID}, % items = Items, % affiliation = Aff, % subscriptions = Subs}, % [NewState | Acc] % end, % {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, % [F, [], pubsub_state]), % {atomic, ok} = mnesia:delete_table(pubsub_state), % {atomic, ok} = ejabberd_mnesia:create(?MODULE, pubsub_state, % [{disc_copies, [node()]}, % {attributes, record_info(fields, pubsub_state)}]), % FNew = fun () -> % lists:foreach(fun mnesia:write/1, NewRecs) % end, % case mnesia:transaction(FNew) of % {atomic, Result} -> % ?INFO_MSG("Pubsub states table upgraded: ~p", % [Result]); % {aborted, Reason} -> % ?ERROR_MSG("Problem upgrading Pubsub states table:~n~p", % [Reason]) % end; % _ -> % ok % end, convert_list_subscriptions(), convert_list_states(). update_lastitem_database(_Host, _ServerHost) -> convert_list_lasts(). %% binarization from old 2.1.x convert_list_items() -> convert_list_records( pubsub_item, record_info(fields, pubsub_item), fun(#pubsub_item{itemid = {I, _}}) -> I end, fun(#pubsub_item{itemid = {I, Nidx}, creation = {C, CKey}, modification = {M, MKey}, payload = Els} = R) -> R#pubsub_item{itemid = {bin(I), Nidx}, creation = {C, binusr(CKey)}, modification = {M, binusr(MKey)}, payload = [fxml:to_xmlel(El) || El<-Els]} end). convert_list_states() -> convert_list_records( pubsub_state, record_info(fields, pubsub_state), fun(#pubsub_state{stateid = {{U,_,_}, _}}) -> U end, fun(#pubsub_state{stateid = {U, Nidx}, items = Is, affiliation = A, subscriptions = Ss} = R) -> R#pubsub_state{stateid = {binusr(U), Nidx}, items = [bin(I) || I<-Is], affiliation = A, subscriptions = [{S,bin(Sid)} || {S,Sid}<-Ss]} end). convert_list_nodes() -> convert_list_records( pubsub_node, record_info(fields, pubsub_node), fun(#pubsub_node{nodeid = {{U,_,_}, _}}) -> U; (#pubsub_node{nodeid = {H, _}}) -> H end, fun(#pubsub_node{nodeid = {H, N}, id = I, parents = Ps, type = T, owners = Os, options = Opts} = R) -> R#pubsub_node{nodeid = {binhost(H), bin(N)}, id = I, parents = [bin(P) || P<-Ps], type = bin(T), owners = [binusr(O) || O<-Os], options = Opts} end). convert_list_subscriptions() -> [convert_list_records( pubsub_subscription, record_info(fields, pubsub_subscription), fun(#pubsub_subscription{subid = I}) -> I end, fun(#pubsub_subscription{subid = I, options = Opts} = R) -> R#pubsub_subscription{subid = bin(I), options = Opts} end) || lists:member(pubsub_subscription, mnesia:system_info(tables))]. convert_list_lasts() -> convert_list_records( pubsub_last_item, record_info(fields, pubsub_last_item), fun(#pubsub_last_item{itemid = I}) -> I end, fun(#pubsub_last_item{itemid = I, nodeid = Nidx, creation = {C, CKey}, payload = Payload} = R) -> R#pubsub_last_item{itemid = bin(I), nodeid = Nidx, creation = {C, binusr(CKey)}, payload = fxml:to_xmlel(Payload)} end). %% internal tools convert_list_records(Tab, Fields, DetectFun, ConvertFun) -> case mnesia:table_info(Tab, attributes) of Fields -> convert_table_to_binary( Tab, Fields, set, DetectFun, ConvertFun); _ -> ?INFO_MSG("Recreating ~p table", [Tab]), mnesia:transform_table(Tab, ignore, Fields), convert_list_records(Tab, Fields, DetectFun, ConvertFun) end. binhost({U,S,R}) -> binusr({U,S,R}); binhost(L) -> bin(L). binusr({U,S,R}) -> {bin(U), bin(S), bin(R)}. bin(L) -> iolist_to_binary(L). %% The code should be updated to support new ejabberd_mnesia %% transform functions (i.e. need_transform/1 and transform/1) convert_table_to_binary(Tab, Fields, Type, DetectFun, ConvertFun) -> case is_table_still_list(Tab, DetectFun) of true -> ?INFO_MSG("Converting '~ts' table from strings to binaries.", [Tab]), TmpTab = list_to_atom(atom_to_list(Tab) ++ "_tmp_table"), catch mnesia:delete_table(TmpTab), case ejabberd_mnesia:create(?MODULE, TmpTab, [{disc_only_copies, [node()]}, {type, Type}, {local_content, true}, {record_name, Tab}, {attributes, Fields}]) of {atomic, ok} -> mnesia:transform_table(Tab, ignore, Fields), case mnesia:transaction( fun() -> mnesia:write_lock_table(TmpTab), mnesia:foldl( fun(R, _) -> NewR = ConvertFun(R), mnesia:dirty_write(TmpTab, NewR) end, ok, Tab) end) of {atomic, ok} -> mnesia:clear_table(Tab), case mnesia:transaction( fun() -> mnesia:write_lock_table(Tab), mnesia:foldl( fun(R, _) -> mnesia:dirty_write(R) end, ok, TmpTab) end) of {atomic, ok} -> mnesia:delete_table(TmpTab); Err -> report_and_stop(Tab, Err) end; Err -> report_and_stop(Tab, Err) end; Err -> report_and_stop(Tab, Err) end; false -> ok end. is_table_still_list(Tab, DetectFun) -> is_table_still_list(Tab, DetectFun, mnesia:dirty_first(Tab)). is_table_still_list(_Tab, _DetectFun, '$end_of_table') -> false; is_table_still_list(Tab, DetectFun, Key) -> Rs = mnesia:dirty_read(Tab, Key), Res = lists:foldl(fun(_, true) -> true; (_, false) -> false; (R, _) -> case DetectFun(R) of '$next' -> '$next'; El -> is_list(El) end end, '$next', Rs), case Res of true -> true; false -> false; '$next' -> is_table_still_list(Tab, DetectFun, mnesia:dirty_next(Tab, Key)) end. report_and_stop(Tab, Err) -> ErrTxt = lists:flatten( io_lib:format( "Failed to convert '~ts' table to binary: ~p", [Tab, Err])), ?CRITICAL_MSG(ErrTxt, []), ejabberd:halt(). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_http_fileserver.erl����������������������������������������������������������0000644�0002322�0002322�00000053545�14154362354�021205� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% File : mod_http_fileserver.erl %%% Author : Massimiliano Mirra <mmirra [at] process-one [dot] net> %%% Purpose : Simple file server plugin for embedded ejabberd web server %%% Created : %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_http_fileserver). -author('mmirra@process-one.net'). -behaviour(gen_mod). -behaviour(gen_server). %% gen_mod callbacks -export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% request_handlers callbacks -export([process/2]). %% utility for other http modules -export([content_type/3]). -export([reopen_log/0, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include_lib("kernel/include/file.hrl"). -include("translate.hrl"). -record(state, {host, docroot, accesslog, accesslogfd, directory_indices, custom_headers, default_content_type, content_types = [], user_access = none}). %% Response is {DataSize, Code, [{HeaderKey, HeaderValue}], Data} -define(HTTP_ERR_FILE_NOT_FOUND, {-1, 404, [], <<"Not found">>}). -define(REQUEST_AUTH_HEADERS, [{<<"WWW-Authenticate">>, <<"Basic realm=\"ejabberd\"">>}]). -define(HTTP_ERR_FORBIDDEN, {-1, 403, [], <<"Forbidden">>}). -define(HTTP_ERR_REQUEST_AUTH, {-1, 401, ?REQUEST_AUTH_HEADERS, <<"Unauthorized">>}). -define(HTTP_ERR_HOST_UNKNOWN, {-1, 410, [], <<"Host unknown">>}). -define(DEFAULT_CONTENT_TYPES, [{<<".css">>, <<"text/css">>}, {<<".gif">>, <<"image/gif">>}, {<<".html">>, <<"text/html">>}, {<<".jar">>, <<"application/java-archive">>}, {<<".jpeg">>, <<"image/jpeg">>}, {<<".jpg">>, <<"image/jpeg">>}, {<<".js">>, <<"text/javascript">>}, {<<".png">>, <<"image/png">>}, {<<".svg">>, <<"image/svg+xml">>}, {<<".txt">>, <<"text/plain">>}, {<<".xml">>, <<"application/xml">>}, {<<".xpi">>, <<"application/x-xpinstall">>}, {<<".xul">>, <<"application/vnd.mozilla.xul+xml">>}]). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, OldOpts) -> Proc = get_proc_name(Host), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). depends(_Host, _Opts) -> []. %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- init([Host|_]) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), try initialize(Host, Opts) of State -> process_flag(trap_exit, true), {ok, State} catch throw:Reason -> {stop, Reason} end. initialize(Host, Opts) -> DocRoot = mod_http_fileserver_opt:docroot(Opts), AccessLog = mod_http_fileserver_opt:accesslog(Opts), AccessLogFD = try_open_log(AccessLog, Host), DirectoryIndices = mod_http_fileserver_opt:directory_indices(Opts), CustomHeaders = mod_http_fileserver_opt:custom_headers(Opts), DefaultContentType = mod_http_fileserver_opt:default_content_type(Opts), UserAccess0 = mod_http_fileserver_opt:must_authenticate_with(Opts), UserAccess = case UserAccess0 of [] -> none; _ -> maps:from_list(UserAccess0) end, ContentTypes = build_list_content_types( mod_http_fileserver_opt:content_types(Opts), ?DEFAULT_CONTENT_TYPES), ?DEBUG("Known content types: ~ts", [str:join([[$*, K, " -> ", V] || {K, V} <- ContentTypes], <<", ">>)]), #state{host = Host, accesslog = AccessLog, accesslogfd = AccessLogFD, docroot = DocRoot, directory_indices = DirectoryIndices, custom_headers = CustomHeaders, default_content_type = DefaultContentType, content_types = ContentTypes, user_access = UserAccess}. %% @spec (AdminCTs::[CT], Default::[CT]) -> [CT] %% where CT = {Extension::string(), Value} %% Value = string() | undefined %% @doc Return a unified list without duplicates. %% Elements of AdminCTs have more priority. %% If a CT is declared as 'undefined', then it is not included in the result. build_list_content_types(AdminCTsUnsorted, DefaultCTsUnsorted) -> AdminCTs = lists:ukeysort(1, AdminCTsUnsorted), DefaultCTs = lists:ukeysort(1, DefaultCTsUnsorted), CTsUnfiltered = lists:ukeymerge(1, AdminCTs, DefaultCTs), [{Extension, Value} || {Extension, Value} <- CTsUnfiltered, Value /= undefined]. try_open_log(undefined, _Host) -> undefined; try_open_log(FN, _Host) -> FD = try open_log(FN) of FD1 -> FD1 catch throw:{cannot_open_accesslog, FN, Reason} -> ?ERROR_MSG("Cannot open access log file: ~p~nReason: ~p", [FN, Reason]), undefined end, ejabberd_hooks:add(reopen_log_hook, ?MODULE, reopen_log, 50), FD. %%-------------------------------------------------------------------- %% Function: handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call({serve, LocalPath, Auth, RHeaders}, _From, State) -> IfModifiedSince = case find_header('If-Modified-Since', RHeaders, bad_date) of bad_date -> bad_date; Val -> httpd_util:convert_request_date(binary_to_list(Val)) end, Reply = serve(LocalPath, Auth, State#state.docroot, State#state.directory_indices, State#state.custom_headers, State#state.default_content_type, State#state.content_types, State#state.user_access, IfModifiedSince), {reply, Reply, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast({add_to_log, FileSize, Code, Request}, State) -> add_to_log(State#state.accesslogfd, FileSize, Code, Request), {noreply, State}; handle_cast(reopen_log, State) -> FD2 = reopen_log(State#state.accesslog, State#state.accesslogfd), {noreply, State#state{accesslogfd = FD2}}; handle_cast({reload, Host, NewOpts, _OldOpts}, OldState) -> try initialize(Host, NewOpts) of NewState -> FD = reopen_log(NewState#state.accesslog, OldState#state.accesslogfd), {noreply, NewState#state{accesslogfd = FD}} catch throw:_ -> {noreply, OldState} end; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any necessary %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, #state{host = Host} = State) -> close_log(State#state.accesslogfd), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_hooks:delete(reopen_log_hook, ?MODULE, reopen_log, 50); true -> ok end. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %% request_handlers callbacks %%==================================================================== %% @spec (LocalPath, Request) -> {HTTPCode::integer(), [Header], Page::string()} %% @doc Handle an HTTP request. %% LocalPath is the part of the requested URL path that is "local to the module". %% Returns the page to be sent back to the client and/or HTTP status code. process(LocalPath, #request{host = Host, auth = Auth, headers = RHeaders} = Request) -> ?DEBUG("Requested ~p", [LocalPath]), try VHost = ejabberd_router:host_of_route(Host), {FileSize, Code, Headers, Contents} = gen_server:call(get_proc_name(VHost), {serve, LocalPath, Auth, RHeaders}), add_to_log(FileSize, Code, Request#request{host = VHost}), {Code, Headers, Contents} catch _:{Why, _} when Why == noproc; Why == invalid_domain; Why == unregistered_route -> ?DEBUG("Received an HTTP request with Host: ~ts, " "but couldn't find the related " "ejabberd virtual host", [Host]), {FileSize1, Code1, Headers1, Contents1} = ?HTTP_ERR_HOST_UNKNOWN, add_to_log(FileSize1, Code1, Request#request{host = ejabberd_config:get_myname()}), {Code1, Headers1, Contents1} end. serve(LocalPath, Auth, DocRoot, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes, UserAccess, IfModifiedSince) -> CanProceed = case {UserAccess, Auth} of {none, _} -> true; {_, {User, Pass}} -> case maps:find(User, UserAccess) of {ok, Pass} -> true; _ -> false end; _ -> false end, case CanProceed of false -> ?HTTP_ERR_REQUEST_AUTH; true -> FileName = filename:join(filename:split(DocRoot) ++ LocalPath), case file:read_file_info(FileName) of {error, enoent} -> ?HTTP_ERR_FILE_NOT_FOUND; {error, enotdir} -> ?HTTP_ERR_FILE_NOT_FOUND; {error, eacces} -> ?HTTP_ERR_FORBIDDEN; {ok, #file_info{type = directory}} -> serve_index(FileName, DirectoryIndices, CustomHeaders, DefaultContentType, ContentTypes); {ok, #file_info{mtime = MTime} = FileInfo} -> case calendar:local_time_to_universal_time_dst(MTime) of [IfModifiedSince | _] -> serve_not_modified(FileInfo, FileName, CustomHeaders); _ -> serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) end end end. %% Troll through the directory indices attempting to find one which %% works, if none can be found, return a 404. serve_index(_FileName, [], _CH, _DefaultContentType, _ContentTypes) -> ?HTTP_ERR_FILE_NOT_FOUND; serve_index(FileName, [Index | T], CH, DefaultContentType, ContentTypes) -> IndexFileName = filename:join([FileName] ++ [Index]), case file:read_file_info(IndexFileName) of {error, _Error} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes); {ok, #file_info{type = directory}} -> serve_index(FileName, T, CH, DefaultContentType, ContentTypes); {ok, FileInfo} -> serve_file(FileInfo, IndexFileName, CH, DefaultContentType, ContentTypes) end. serve_not_modified(FileInfo, FileName, CustomHeaders) -> ?DEBUG("Delivering not modified: ~ts", [FileName]), {0, 304, ejabberd_http:apply_custom_headers( [{<<"Server">>, <<"ejabberd">>}, {<<"Last-Modified">>, last_modified(FileInfo)}], CustomHeaders), <<>>}. %% Assume the file exists if we got this far and attempt to read it in %% and serve it up. serve_file(FileInfo, FileName, CustomHeaders, DefaultContentType, ContentTypes) -> ?DEBUG("Delivering: ~ts", [FileName]), ContentType = content_type(FileName, DefaultContentType, ContentTypes), {FileInfo#file_info.size, 200, ejabberd_http:apply_custom_headers( [{<<"Server">>, <<"ejabberd">>}, {<<"Last-Modified">>, last_modified(FileInfo)}, {<<"Content-Type">>, ContentType}], CustomHeaders), {file, FileName}}. %%---------------------------------------------------------------------- %% Log file %%---------------------------------------------------------------------- open_log(FN) -> case file:open(FN, [append]) of {ok, FD} -> FD; {error, Reason} -> throw({cannot_open_accesslog, FN, Reason}) end. close_log(FD) -> file:close(FD). reopen_log(undefined, undefined) -> ok; reopen_log(FN, FD) -> close_log(FD), open_log(FN). reopen_log() -> lists:foreach( fun(Host) -> gen_server:cast(get_proc_name(Host), reopen_log) end, ejabberd_option:hosts()). add_to_log(FileSize, Code, Request) -> gen_server:cast(get_proc_name(Request#request.host), {add_to_log, FileSize, Code, Request}). add_to_log(undefined, _FileSize, _Code, _Request) -> ok; add_to_log(File, FileSize, Code, Request) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), IP = ip_to_string(element(1, Request#request.ip)), Path = join(Request#request.path, "/"), Query = case stringify_query(Request#request.q) of <<"">> -> ""; String -> [$? | String] end, UserAgent = find_header('User-Agent', Request#request.headers, "-"), Referer = find_header('Referer', Request#request.headers, "-"), %% Pseudo Combined Apache log format: %% 127.0.0.1 - - [28/Mar/2007:18:41:55 +0200] "GET / HTTP/1.1" 302 303 "-" "tsung" %% TODO some fields are hardcoded/missing: %% The date/time integers should have always 2 digits. For example day "7" should be "07" %% Month should be 3*letter, not integer 1..12 %% Missing time zone = (`+' | `-') 4*digit %% Missing protocol version: HTTP/1.1 %% For reference: http://httpd.apache.org/docs/2.2/logs.html io:format(File, "~ts - - [~p/~p/~p:~p:~p:~p] \"~ts /~ts~ts\" ~p ~p ~p ~p~n", [IP, Day, Month, Year, Hour, Minute, Second, Request#request.method, Path, Query, Code, FileSize, Referer, UserAgent]). stringify_query(Q) -> stringify_query(Q, []). stringify_query([], Res) -> join(lists:reverse(Res), "&"); stringify_query([{nokey, _B} | Q], Res) -> stringify_query(Q, Res); stringify_query([{A, B} | Q], Res) -> stringify_query(Q, [join([A,B], "=") | Res]). find_header(Header, Headers, Default) -> case lists:keysearch(Header, 1, Headers) of {value, {_, Value}} -> Value; false -> Default end. %%---------------------------------------------------------------------- %% Utilities %%---------------------------------------------------------------------- get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). join([], _) -> <<"">>; join([E], _) -> E; join([H | T], Separator) -> [H2 | T2] = case is_binary(H) of true -> [binary_to_list(I)||I<-[H|T]]; false -> [H | T] end, Res=lists:foldl(fun(E, Acc) -> lists:concat([Acc, Separator, E]) end, H2, T2), case is_binary(H) of true -> list_to_binary(Res); false -> Res end. content_type(Filename, DefaultContentType, ContentTypes) -> Extension = str:to_lower(filename:extension(Filename)), case lists:keysearch(Extension, 1, ContentTypes) of {value, {_, ContentType}} -> ContentType; false -> DefaultContentType end. last_modified(FileInfo) -> Then = FileInfo#file_info.mtime, httpd_util:rfc1123_date(Then). %% Convert IP address tuple to string representation. Accepts either %% IPv4 or IPv6 address tuples. ip_to_string(Address) when size(Address) == 4 -> join(tuple_to_list(Address), "."); ip_to_string(Address) when size(Address) == 8 -> Parts = lists:map(fun (Int) -> io_lib:format("~.16B", [Int]) end, tuple_to_list(Address)), string:to_lower(lists:flatten(join(Parts, ":"))). mod_opt_type(accesslog) -> econf:file(write); mod_opt_type(content_types) -> econf:map(econf:binary(), econf:binary()); mod_opt_type(custom_headers) -> econf:map(econf:binary(), econf:binary()); mod_opt_type(default_content_type) -> econf:binary(); mod_opt_type(directory_indices) -> econf:list(econf:binary()); mod_opt_type(docroot) -> econf:directory(write); mod_opt_type(must_authenticate_with) -> econf:list( econf:and_then( econf:and_then( econf:binary("^[^:]+:[^:]+$"), econf:binary_sep(":")), fun([K, V]) -> {K, V} end)). -spec mod_options(binary()) -> [{must_authenticate_with, [{binary(), binary()}]} | {atom(), any()}]. mod_options(_) -> [{accesslog, undefined}, {content_types, []}, {default_content_type, <<"application/octet-stream">>}, {custom_headers, []}, {directory_indices, []}, {must_authenticate_with, []}, %% Required option docroot]. mod_doc() -> #{desc => ?T("This simple module serves files from the local disk over HTTP."), opts => [{accesslog, #{value => ?T("Path"), desc => ?T("File to log accesses using an Apache-like format. " "No log will be recorded if this option is not specified.")}}, {docroot, #{value => ?T("Path"), desc => ?T("Directory to serve the files from. " "This is a mandatory option.")}}, {content_types, #{value => "{Extension: Type}", desc => ?T("Specify mappings of extension to content type. " "There are several content types already defined. " "With this option you can add new definitions " "or modify existing ones. The default values are:"), example => ["content_types:"| [" " ++ binary_to_list(E) ++ ": " ++ binary_to_list(T) || {E, T} <- ?DEFAULT_CONTENT_TYPES]]}}, {default_content_type, #{value => ?T("Type"), desc => ?T("Specify the content type to use for unknown extensions. " "The default value is 'application/octet-stream'.")}}, {custom_headers, #{value => "{Name: Value}", desc => ?T("Indicate custom HTTP headers to be included in all responses. " "There are no custom headers by default.")}}, {directory_indices, #{value => "[Index, ...]", desc => ?T("Indicate one or more directory index files, " "similarly to Apache's 'DirectoryIndex' variable. " "When an HTTP request hits a directory instead of a " "regular file, those directory indices are looked in order, " "and the first one found is returned. " "The default value is an empty list.")}}, {must_authenticate_with, #{value => ?T("[{Username, Hostname}, ...]"), desc => ?T("List of accounts that are allowed to use this service. " "Default value: '[]'.")}}], example => [{?T("This example configuration will serve the files from the " "local directory '/var/www' in the address " "'http://example.org:5280/pub/archive/'. In this example a new " "content type 'ogg' is defined, 'png' is redefined, and 'jpg' " "definition is deleted:"), ["listen:", " ...", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " ...", " /pub/archive: mod_http_fileserver", " ...", " ...", "", "modules:", " ...", " mod_http_fileserver:", " docroot: /var/www", " accesslog: /var/log/ejabberd/access.log", " directory_indices:", " - index.html", " - main.htm", " custom_headers:", " X-Powered-By: Erlang/OTP", " X-Fry: \"It's a widely-believed fact!\"", " content_types:", " .ogg: audio/ogg", " .png: image/png", " default_content_type: text/html", " ..."]}]}. �����������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/src/mod_fail2ban_opt.erl�������������������������������������������������������������0000644�0002322�0002322�00000001717�14154362354�020332� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_fail2ban_opt). -export([access/1]). -export([c2s_auth_ban_lifetime/1]). -export([c2s_max_auth_failures/1]). -spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, access). -spec c2s_auth_ban_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). c2s_auth_ban_lifetime(Opts) when is_map(Opts) -> gen_mod:get_opt(c2s_auth_ban_lifetime, Opts); c2s_auth_ban_lifetime(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, c2s_auth_ban_lifetime). -spec c2s_max_auth_failures(gen_mod:opts() | global | binary()) -> pos_integer(). c2s_max_auth_failures(Opts) when is_map(Opts) -> gen_mod:get_opt(c2s_max_auth_failures, Opts); c2s_max_auth_failures(Host) -> gen_mod:get_module_opt(Host, mod_fail2ban, c2s_max_auth_failures). �������������������������������������������������ejabberd-21.12/src/mod_muc_log.erl������������������������������������������������������������������0000644�0002322�0002322�00000115351�14154362354�017417� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%---------------------------------------------------------------------- %%% File : mod_muc_log.erl %%% Author : Badlop@process-one.net %%% Purpose : MUC room logging %%% Created : 12 Mar 2006 by Alexey Shchepin <alexey@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_log). -protocol({xep, 334, '0.2'}). -author('badlop@process-one.net'). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, get_url/1, check_access_log/2, add_to_log/5]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_muc_room.hrl"). -include("translate.hrl"). -record(room, {jid, title, subject, subject_author, config}). -define(PLAINTEXT_CO, <<"ZZCZZ">>). -define(PLAINTEXT_IN, <<"ZZIZZ">>). -define(PLAINTEXT_OUT, <<"ZZOZZ">>). -record(logstate, {host, out_dir, dir_type, dir_name, file_format, file_permissions, css_file, access, lang, timezone, spam_prevention, top_link}). %%==================================================================== %% API %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, _OldOpts) -> Proc = get_proc_name(Host), gen_server:cast(Proc, {reload, NewOpts}). add_to_log(Host, Type, Data, Room, Opts) -> gen_server:cast(get_proc_name(Host), {add_to_log, Type, Data, Room, Opts}). check_access_log(Host, From) -> case catch gen_server:call(get_proc_name(Host), {check_access_log, Host, From}) of {'EXIT', _Error} -> deny; Res -> Res end. -spec get_url(#state{}) -> {ok, binary()} | error. get_url(#state{room = Room, host = Host, server_host = ServerHost}) -> try mod_muc_log_opt:url(ServerHost) of undefined -> error; URL -> case mod_muc_log_opt:dirname(ServerHost) of room_jid -> {ok, <<URL/binary, $/, Room/binary, $@, Host/binary>>}; room_name -> {ok, <<URL/binary, $/, Room/binary>>} end catch error:{module_not_loaded, _, _} -> error end. depends(_Host, _Opts) -> [{mod_muc, hard}]. %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), {ok, init_state(Host, Opts)}. handle_call({check_access_log, ServerHost, FromJID}, _From, State) -> Reply = acl:match_rule(ServerHost, State#logstate.access, FromJID), {reply, Reply, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. handle_cast({reload, Opts}, #logstate{host = Host}) -> {noreply, init_state(Host, Opts)}; handle_cast({add_to_log, Type, Data, Room, Opts}, State) -> case catch add_to_log2(Type, Data, Room, Opts, State) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); _ -> ok end, {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- init_state(Host, Opts) -> OutDir = mod_muc_log_opt:outdir(Opts), DirType = mod_muc_log_opt:dirtype(Opts), DirName = mod_muc_log_opt:dirname(Opts), FileFormat = mod_muc_log_opt:file_format(Opts), FilePermissions = mod_muc_log_opt:file_permissions(Opts), CSSFile = mod_muc_log_opt:cssfile(Opts), AccessLog = mod_muc_log_opt:access_log(Opts), Timezone = mod_muc_log_opt:timezone(Opts), Top_link = mod_muc_log_opt:top_link(Opts), NoFollow = mod_muc_log_opt:spam_prevention(Opts), Lang = ejabberd_option:language(Host), #logstate{host = Host, out_dir = OutDir, dir_type = DirType, dir_name = DirName, file_format = FileFormat, css_file = CSSFile, file_permissions = FilePermissions, access = AccessLog, lang = Lang, timezone = Timezone, spam_prevention = NoFollow, top_link = Top_link}. add_to_log2(text, {Nick, Packet}, Room, Opts, State) -> case has_no_permanent_store_hint(Packet) of false -> case {Packet#message.subject, Packet#message.body} of {[], []} -> ok; {[], Body} -> Message = {body, xmpp:get_text(Body)}, add_message_to_log(Nick, Message, Room, Opts, State); {Subj, _} -> Message = {subject, xmpp:get_text(Subj)}, add_message_to_log(Nick, Message, Room, Opts, State) end; true -> ok end; add_to_log2(roomconfig_change, _Occupants, Room, Opts, State) -> add_message_to_log(<<"">>, roomconfig_change, Room, Opts, State); add_to_log2(roomconfig_change_enabledlogging, Occupants, Room, Opts, State) -> add_message_to_log(<<"">>, {roomconfig_change, Occupants}, Room, Opts, State); add_to_log2(room_existence, NewStatus, Room, Opts, State) -> add_message_to_log(<<"">>, {room_existence, NewStatus}, Room, Opts, State); add_to_log2(nickchange, {OldNick, NewNick}, Room, Opts, State) -> add_message_to_log(NewNick, {nickchange, OldNick}, Room, Opts, State); add_to_log2(join, Nick, Room, Opts, State) -> add_message_to_log(Nick, join, Room, Opts, State); add_to_log2(leave, {Nick, Reason}, Room, Opts, State) -> case Reason of <<"">> -> add_message_to_log(Nick, leave, Room, Opts, State); _ -> add_message_to_log(Nick, {leave, Reason}, Room, Opts, State) end; add_to_log2(kickban, {Nick, Reason, Code}, Room, Opts, State) -> add_message_to_log(Nick, {kickban, Code, Reason}, Room, Opts, State). %%---------------------------------------------------------------------- %% Core build_filename_string(TimeStamp, OutDir, RoomJID, DirType, DirName, FileFormat) -> {{Year, Month, Day}, _Time} = TimeStamp, {Dir, Filename, Rel} = case DirType of subdirs -> SYear = (str:format("~4..0w", [Year])), SMonth = (str:format("~2..0w", [Month])), SDay = (str:format("~2..0w", [Day])), {fjoin([SYear, SMonth]), SDay, <<"../..">>}; plain -> Date = (str:format("~4..0w-~2..0w-~2..0w", [Year, Month, Day])), {<<"">>, Date, <<".">>} end, RoomString = case DirName of room_jid -> RoomJID; room_name -> get_room_name(RoomJID) end, Extension = case FileFormat of html -> <<".html">>; plaintext -> <<".txt">> end, Fd = fjoin([OutDir, RoomString, Dir]), Fn = fjoin([Fd, <<Filename/binary, Extension/binary>>]), Fnrel = fjoin([Rel, Dir, <<Filename/binary, Extension/binary>>]), {Fd, Fn, Fnrel}. get_room_name(RoomJID) -> JID = jid:decode(RoomJID), JID#jid.user. %% calculate day before get_timestamp_daydiff(TimeStamp, Daydiff) -> {Date1, HMS} = TimeStamp, Date2 = calendar:gregorian_days_to_date(calendar:date_to_gregorian_days(Date1) + Daydiff), {Date2, HMS}. %% Try to close the previous day log, if it exists close_previous_log(Fn, Images_dir, FileFormat) -> case file:read_file_info(Fn) of {ok, _} -> {ok, F} = file:open(Fn, [append]), write_last_lines(F, Images_dir, FileFormat), file:close(F); _ -> ok end. write_last_lines(_, _, plaintext) -> ok; write_last_lines(F, Images_dir, _FileFormat) -> fw(F, <<"<div class=\"legend\">">>), fw(F, <<" <a href=\"http://www.ejabberd.im\"><img " "style=\"border:0\" src=\"~ts/powered-by-ejabbe" "rd.png\" alt=\"Powered by ejabberd - robust, scalable and extensible XMPP server\"/></a>">>, [Images_dir]), fw(F, <<" <a href=\"http://www.erlang.org/\"><img " "style=\"border:0\" src=\"~ts/powered-by-erlang" ".png\" alt=\"Powered by Erlang\"/></a>">>, [Images_dir]), fw(F, <<"<span class=\"w3c\">">>), fw(F, <<" <a href=\"http://validator.w3.org/check?uri" "=referer\"><img style=\"border:0;width:88px;h" "eight:31px\" src=\"~ts/valid-xhtml10.png\" " "alt=\"Valid XHTML 1.0 Transitional\" " "/></a>">>, [Images_dir]), fw(F, <<" <a href=\"http://jigsaw.w3.org/css-validato" "r/\"><img style=\"border:0;width:88px;height:" "31px\" src=\"~ts/vcss.png\" alt=\"Valid " "CSS!\"/></a>">>, [Images_dir]), fw(F, <<"</span></div></body></html>">>). set_filemode(Fn, {FileMode, FileGroup}) -> ok = file:change_mode(Fn, list_to_integer(integer_to_list(FileMode), 8)), ok = file:change_group(Fn, FileGroup). htmlize_nick(Nick1, html) -> htmlize(<<"<", Nick1/binary, ">">>, html); htmlize_nick(Nick1, plaintext) -> htmlize(<<?PLAINTEXT_IN/binary, Nick1/binary, ?PLAINTEXT_OUT/binary>>, plaintext). add_message_to_log(Nick1, Message, RoomJID, Opts, State) -> #logstate{out_dir = OutDir, dir_type = DirType, dir_name = DirName, file_format = FileFormat, file_permissions = FilePermissions, css_file = CSSFile, lang = Lang, timezone = Timezone, spam_prevention = NoFollow, top_link = TopLink} = State, Room = get_room_info(RoomJID, Opts), Nick = htmlize(Nick1, FileFormat), Nick2 = htmlize_nick(Nick1, FileFormat), Now = erlang:timestamp(), TimeStamp = case Timezone of local -> calendar:now_to_local_time(Now); universal -> calendar:now_to_universal_time(Now) end, {Fd, Fn, _Dir} = build_filename_string(TimeStamp, OutDir, Room#room.jid, DirType, DirName, FileFormat), {Date, Time} = TimeStamp, case file:read_file_info(Fn) of {ok, _} -> {ok, F} = file:open(Fn, [append]); {error, enoent} -> make_dir_rec(Fd), {ok, F} = file:open(Fn, [append]), catch set_filemode(Fn, FilePermissions), Datestring = get_dateweek(Date, Lang), TimeStampYesterday = get_timestamp_daydiff(TimeStamp, -1), {_FdYesterday, FnYesterday, DatePrev} = build_filename_string(TimeStampYesterday, OutDir, Room#room.jid, DirType, DirName, FileFormat), TimeStampTomorrow = get_timestamp_daydiff(TimeStamp, 1), {_FdTomorrow, _FnTomorrow, DateNext} = build_filename_string(TimeStampTomorrow, OutDir, Room#room.jid, DirType, DirName, FileFormat), HourOffset = calc_hour_offset(TimeStamp), put_header(F, Room, Datestring, CSSFile, Lang, HourOffset, DatePrev, DateNext, TopLink, FileFormat), Images_dir = fjoin([OutDir, <<"images">>]), file:make_dir(Images_dir), create_image_files(Images_dir), Images_url = case DirType of subdirs -> <<"../../../images">>; plain -> <<"../images">> end, close_previous_log(FnYesterday, Images_url, FileFormat) end, Text = case Message of roomconfig_change -> RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), io_lib:format("<font class=\"mrcm\">~ts</font><br/>", [tr(Lang, ?T("Chatroom configuration modified"))]); {roomconfig_change, Occupants} -> RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), io_lib:format("<font class=\"mrcm\">~ts</font><br/>", [tr(Lang, ?T("Chatroom configuration modified"))]); join -> io_lib:format("<font class=\"mj\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("joins the room"))]); leave -> io_lib:format("<font class=\"ml\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("leaves the room"))]); {leave, Reason} -> io_lib:format("<font class=\"ml\">~ts ~ts: ~ts</font><br/>", [Nick, tr(Lang, ?T("leaves the room")), htmlize(Reason, NoFollow, FileFormat)]); {kickban, 301, <<"">>} -> io_lib:format("<font class=\"mb\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("has been banned"))]); {kickban, 301, Reason} -> io_lib:format("<font class=\"mb\">~ts ~ts: ~ts</font><br/>", [Nick, tr(Lang, ?T("has been banned")), htmlize(Reason, FileFormat)]); {kickban, 307, <<"">>} -> io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("has been kicked"))]); {kickban, 307, Reason} -> io_lib:format("<font class=\"mk\">~ts ~ts: ~ts</font><br/>", [Nick, tr(Lang, ?T("has been kicked")), htmlize(Reason, FileFormat)]); {kickban, 321, <<"">>} -> io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("has been kicked because of an affiliation " "change"))]); {kickban, 322, <<"">>} -> io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("has been kicked because the room has " "been changed to members-only"))]); {kickban, 332, <<"">>} -> io_lib:format("<font class=\"mk\">~ts ~ts</font><br/>", [Nick, tr(Lang, ?T("has been kicked because of a system " "shutdown"))]); {nickchange, OldNick} -> io_lib:format("<font class=\"mnc\">~ts ~ts ~ts</font><br/>", [htmlize(OldNick, FileFormat), tr(Lang, ?T("is now known as")), Nick]); {subject, T} -> io_lib:format("<font class=\"msc\">~ts~ts~ts</font><br/>", [Nick, tr(Lang, ?T(" has set the subject to: ")), htmlize(T, NoFollow, FileFormat)]); {body, T} -> case {ejabberd_regexp:run(T, <<"^/me ">>), Nick} of {_, <<"">>} -> io_lib:format("<font class=\"msm\">~ts</font><br/>", [htmlize(T, NoFollow, FileFormat)]); {match, _} -> io_lib:format("<font class=\"mne\">~ts ~ts</font><br/>", [Nick, str:substr(htmlize(T, FileFormat), 5)]); {nomatch, _} -> io_lib:format("<font class=\"mn\">~ts</font> ~ts<br/>", [Nick2, htmlize(T, NoFollow, FileFormat)]) end; {room_existence, RoomNewExistence} -> io_lib:format("<font class=\"mrcm\">~ts</font><br/>", [get_room_existence_string(RoomNewExistence, Lang)]) end, {Hour, Minute, Second} = Time, STime = io_lib:format("~2..0w:~2..0w:~2..0w", [Hour, Minute, Second]), {_, _, Microsecs} = Now, STimeUnique = io_lib:format("~ts.~w", [STime, Microsecs]), fw(F, io_lib:format("<a id=\"~ts\" name=\"~ts\" href=\"#~ts\" " "class=\"ts\">[~ts]</a> ", [STimeUnique, STimeUnique, STimeUnique, STime]) ++ Text, FileFormat), file:close(F), ok. %%---------------------------------------------------------------------- %% Utilities get_room_existence_string(created, Lang) -> tr(Lang, ?T("Chatroom is created")); get_room_existence_string(destroyed, Lang) -> tr(Lang, ?T("Chatroom is destroyed")); get_room_existence_string(started, Lang) -> tr(Lang, ?T("Chatroom is started")); get_room_existence_string(stopped, Lang) -> tr(Lang, ?T("Chatroom is stopped")). get_dateweek(Date, Lang) -> Weekday = case calendar:day_of_the_week(Date) of 1 -> tr(Lang, ?T("Monday")); 2 -> tr(Lang, ?T("Tuesday")); 3 -> tr(Lang, ?T("Wednesday")); 4 -> tr(Lang, ?T("Thursday")); 5 -> tr(Lang, ?T("Friday")); 6 -> tr(Lang, ?T("Saturday")); 7 -> tr(Lang, ?T("Sunday")) end, {Y, M, D} = Date, Month = case M of 1 -> tr(Lang, ?T("January")); 2 -> tr(Lang, ?T("February")); 3 -> tr(Lang, ?T("March")); 4 -> tr(Lang, ?T("April")); 5 -> tr(Lang, ?T("May")); 6 -> tr(Lang, ?T("June")); 7 -> tr(Lang, ?T("July")); 8 -> tr(Lang, ?T("August")); 9 -> tr(Lang, ?T("September")); 10 -> tr(Lang, ?T("October")); 11 -> tr(Lang, ?T("November")); 12 -> tr(Lang, ?T("December")) end, unicode:characters_to_binary( case Lang of <<"en">> -> io_lib:format("~ts, ~ts ~w, ~w", [Weekday, Month, D, Y]); <<"es">> -> io_lib:format("~ts ~w de ~ts de ~w", [Weekday, D, Month, Y]); _ -> io_lib:format("~ts, ~w ~ts ~w", [Weekday, D, Month, Y]) end). make_dir_rec(Dir) -> filelib:ensure_dir(<<Dir/binary, $/>>). %% {ok, F1}=file:open("valid-xhtml10.png", [read]). %% {ok, F1b}=file:read(F1, 1000000). %% c("../../ejabberd/src/jlib.erl"). %% base64:encode(F1b). create_image_files(Images_dir) -> Filenames = [<<"powered-by-ejabberd.png">>, <<"powered-by-erlang.png">>, <<"valid-xhtml10.png">>, <<"vcss.png">>], lists:foreach( fun(Filename) -> Src = filename:join([misc:img_dir(), Filename]), Dst = fjoin([Images_dir, Filename]), case file:copy(Src, Dst) of {ok, _} -> ok; {error, Why} -> ?ERROR_MSG("Failed to copy ~ts to ~ts: ~ts", [Src, Dst, file:format_error(Why)]) end end, Filenames). fw(F, S) -> fw(F, S, [], html). fw(F, S, O) when is_list(O) -> fw(F, S, O, html); fw(F, S, FileFormat) when is_atom(FileFormat) -> fw(F, S, [], FileFormat). fw(F, S, O, FileFormat) -> S1 = <<(str:format(S, O))/binary, "\n">>, S2 = case FileFormat of html -> S1; plaintext -> S1a = ejabberd_regexp:greplace(S1, <<"<[^<^>]*>">>, <<"">>), S1x = ejabberd_regexp:greplace(S1a, ?PLAINTEXT_CO, <<"~~">>), S1y = ejabberd_regexp:greplace(S1x, ?PLAINTEXT_IN, <<"<">>), ejabberd_regexp:greplace(S1y, ?PLAINTEXT_OUT, <<">">>) end, file:write(F, S2). put_header(_, _, _, _, _, _, _, _, _, plaintext) -> ok; put_header(F, Room, Date, CSSFile, Lang, Hour_offset, Date_prev, Date_next, Top_link, FileFormat) -> fw(F, <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD " "XHTML 1.0 Transitional//EN\" \"http://www.w3." "org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>), fw(F, <<"<html xmlns=\"http://www.w3.org/1999/xhtml\" " "xml:lang=\"~ts\" lang=\"~ts\">">>, [Lang, Lang]), fw(F, <<"<head>">>), fw(F, <<"<meta http-equiv=\"Content-Type\" content=\"t" "ext/html; charset=utf-8\" />">>), fw(F, <<"<title>~ts - ~ts">>, [htmlize(Room#room.title), Date]), put_header_css(F, CSSFile), put_header_script(F), fw(F, <<"">>), fw(F, <<"">>), {Top_url, Top_text} = Top_link, fw(F, <<"

">>, [Top_url, Top_text]), fw(F, <<"
~ts
">>, [htmlize(Room#room.title)]), fw(F, <<"~ts" "">>, [Room#room.jid, Room#room.jid]), fw(F, <<"
~ts" "< " "^ >">>, [Date, Date_prev, Date_next]), case {htmlize(Room#room.subject_author), htmlize(Room#room.subject)} of {<<"">>, <<"">>} -> ok; {SuA, Su} -> fw(F, <<"
~ts~ts~ts
">>, [SuA, tr(Lang, ?T(" has set the subject to: ")), Su]) end, RoomConfig = roomconfig_to_string(Room#room.config, Lang, FileFormat), put_room_config(F, RoomConfig, Lang, FileFormat), Occupants = get_room_occupants(Room#room.jid), RoomOccupants = roomoccupants_to_string(Occupants, FileFormat), put_room_occupants(F, RoomOccupants, Lang, FileFormat), Time_offset_str = case Hour_offset < 0 of true -> io_lib:format("~p", [Hour_offset]); false -> io_lib:format("+~p", [Hour_offset]) end, fw(F, <<"
GMT~ts
">>, [Time_offset_str]). put_header_css(F, {file, Path}) -> fw(F, <<"">>); put_header_css(F, {url, URL}) -> fw(F, <<"">>, [URL]). put_header_script(F) -> fw(F, <<"">>). put_room_config(_F, _RoomConfig, _Lang, plaintext) -> ok; put_room_config(F, RoomConfig, Lang, _FileFormat) -> {_, Now2, _} = erlang:timestamp(), fw(F, <<"
">>), fw(F, <<"
~ts
">>, [Now2, tr(Lang, ?T("Room Configuration"))]), fw(F, <<"

~ts
">>, [Now2, RoomConfig]), fw(F, <<"
">>). put_room_occupants(_F, _RoomOccupants, _Lang, plaintext) -> ok; put_room_occupants(F, RoomOccupants, Lang, _FileFormat) -> {_, Now2, _} = erlang:timestamp(), %% htmlize %% The default behaviour is to ignore the nofollow spam prevention on links %% (NoFollow=false) fw(F, <<"
">>), fw(F, <<"
~ts
">>, [Now2, tr(Lang, ?T("Room Occupants"))]), fw(F, <<"

~ts
">>, [Now2, RoomOccupants]), fw(F, <<"
">>). htmlize(S1) -> htmlize(S1, html). htmlize(S1, plaintext) -> ejabberd_regexp:greplace(S1, <<"~">>, ?PLAINTEXT_CO); htmlize(S1, FileFormat) -> htmlize(S1, false, FileFormat). %% The NoFollow parameter tell if the spam prevention should be applied to the link found %% true means 'apply nofollow on links'. htmlize(S0, _NoFollow, plaintext) -> S1 = ejabberd_regexp:greplace(S0, <<"~">>, ?PLAINTEXT_CO), S1x = ejabberd_regexp:greplace(S1, <<"<">>, ?PLAINTEXT_IN), ejabberd_regexp:greplace(S1x, <<">">>, ?PLAINTEXT_OUT); htmlize(S1, NoFollow, _FileFormat) -> S2_list = str:tokens(S1, <<"\n">>), lists:foldl(fun (Si, Res) -> Si2 = htmlize2(Si, NoFollow), case Res of <<"">> -> Si2; _ -> <", Si2/binary>> end end, <<"">>, S2_list). htmlize2(S1, NoFollow) -> %% Regexp link %% Add the nofollow rel attribute when required S2 = ejabberd_regexp:greplace(S1, <<"\\&">>, <<"\\&">>), S3 = ejabberd_regexp:greplace(S2, <<"<">>, <<"\\<">>), S4 = ejabberd_regexp:greplace(S3, <<">">>, <<"\\>">>), S5 = ejabberd_regexp:greplace(S4, <<"((http|https|ftp)://|(mailto|xmpp):)[^] " ")'\"}]+">>, link_regexp(NoFollow)), S6 = ejabberd_regexp:greplace(S5, <<" ">>, <<"\\ \\ ">>), S7 = ejabberd_regexp:greplace(S6, <<"\\t">>, <<"\\ \\ \\ \\ ">>), S8 = ejabberd_regexp:greplace(S7, <<"~">>, <<"~~">>), ejabberd_regexp:greplace(S8, <<226, 128, 174>>, <<"[RLO]">>). link_regexp(false) -> <<"&">>; link_regexp(true) -> <<"&">>. get_room_info(RoomJID, Opts) -> Title = case lists:keysearch(title, 1, Opts) of {value, {_, T}} -> T; false -> <<"">> end, Subject = case lists:keysearch(subject, 1, Opts) of {value, {_, S}} -> xmpp:get_text(S); false -> <<"">> end, SubjectAuthor = case lists:keysearch(subject_author, 1, Opts) of {value, {_, SA}} -> SA; false -> <<"">> end, #room{jid = jid:encode(RoomJID), title = Title, subject = Subject, subject_author = SubjectAuthor, config = Opts}. roomconfig_to_string(Options, Lang, FileFormat) -> Title = case lists:keysearch(title, 1, Options) of {value, Tuple} -> [Tuple]; false -> [] end, Os1 = lists:keydelete(title, 1, Options), Os2 = lists:sort(Os1), Options2 = Title ++ Os2, lists:foldl(fun ({Opt, Val}, R) -> case get_roomconfig_text(Opt, Lang) of undefined -> R; OptText -> R2 = case Val of false -> <<"
", OptText/binary, "
">>; true -> <<"
", OptText/binary, "
">>; <<"">> -> <<"
", OptText/binary, "
">>; T -> case Opt of password -> <<"
", OptText/binary, "
">>; max_users -> <<"
", OptText/binary, ": \"", (htmlize(integer_to_binary(T), FileFormat))/binary, "\"
">>; title -> <<"
", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"
">>; description -> <<"
", OptText/binary, ": \"", (htmlize(T, FileFormat))/binary, "\"
">>; allow_private_messages_from_visitors -> <<"
", OptText/binary, ": \"", (htmlize(tr(Lang, misc:atom_to_binary(T)), FileFormat))/binary, "\"
">>; _ -> <<"\"", T/binary, "\"">> end end, <> end end, <<"">>, Options2). get_roomconfig_text(title, Lang) -> tr(Lang, ?T("Room title")); get_roomconfig_text(persistent, Lang) -> tr(Lang, ?T("Make room persistent")); get_roomconfig_text(public, Lang) -> tr(Lang, ?T("Make room public searchable")); get_roomconfig_text(public_list, Lang) -> tr(Lang, ?T("Make participants list public")); get_roomconfig_text(password_protected, Lang) -> tr(Lang, ?T("Make room password protected")); get_roomconfig_text(password, Lang) -> tr(Lang, ?T("Password")); get_roomconfig_text(anonymous, Lang) -> tr(Lang, ?T("This room is not anonymous")); get_roomconfig_text(members_only, Lang) -> tr(Lang, ?T("Make room members-only")); get_roomconfig_text(moderated, Lang) -> tr(Lang, ?T("Make room moderated")); get_roomconfig_text(members_by_default, Lang) -> tr(Lang, ?T("Default users as participants")); get_roomconfig_text(allow_change_subj, Lang) -> tr(Lang, ?T("Allow users to change the subject")); get_roomconfig_text(allow_private_messages, Lang) -> tr(Lang, ?T("Allow users to send private messages")); get_roomconfig_text(allow_private_messages_from_visitors, Lang) -> tr(Lang, ?T("Allow visitors to send private messages to")); get_roomconfig_text(allow_query_users, Lang) -> tr(Lang, ?T("Allow users to query other users")); get_roomconfig_text(allow_user_invites, Lang) -> tr(Lang, ?T("Allow users to send invites")); get_roomconfig_text(logging, Lang) -> tr(Lang, ?T("Enable logging")); get_roomconfig_text(allow_visitor_nickchange, Lang) -> tr(Lang, ?T("Allow visitors to change nickname")); get_roomconfig_text(allow_visitor_status, Lang) -> tr(Lang, ?T("Allow visitors to send status text in presence updates")); get_roomconfig_text(captcha_protected, Lang) -> tr(Lang, ?T("Make room CAPTCHA protected")); get_roomconfig_text(description, Lang) -> tr(Lang, ?T("Room description")); %% get_roomconfig_text(subject, Lang) -> "Subject"; %% get_roomconfig_text(subject_author, Lang) -> "Subject author"; get_roomconfig_text(max_users, Lang) -> tr(Lang, ?T("Maximum Number of Occupants")); get_roomconfig_text(_, _) -> undefined. %% Users = [{JID, Nick, Role}] roomoccupants_to_string(Users, _FileFormat) -> Res = [role_users_to_string(RoleS, Users1) || {RoleS, Users1} <- group_by_role(Users), Users1 /= []], iolist_to_binary([<<"
">>, Res, <<"
">>]). group_by_role(Users) -> {Ms, Ps, Vs, Ns} = lists:foldl(fun ({JID, Nick, moderator}, {Mod, Par, Vis, Non}) -> {[{JID, Nick}] ++ Mod, Par, Vis, Non}; ({JID, Nick, participant}, {Mod, Par, Vis, Non}) -> {Mod, [{JID, Nick}] ++ Par, Vis, Non}; ({JID, Nick, visitor}, {Mod, Par, Vis, Non}) -> {Mod, Par, [{JID, Nick}] ++ Vis, Non}; ({JID, Nick, none}, {Mod, Par, Vis, Non}) -> {Mod, Par, Vis, [{JID, Nick}] ++ Non} end, {[], [], [], []}, Users), case Ms of [] -> []; _ -> [{<<"Moderator">>, Ms}] end ++ case Ms of [] -> []; _ -> [{<<"Participant">>, Ps}] end ++ case Ms of [] -> []; _ -> [{<<"Visitor">>, Vs}] end ++ case Ms of [] -> []; _ -> [{<<"None">>, Ns}] end. role_users_to_string(RoleS, Users) -> SortedUsers = lists:keysort(2, Users), UsersString = << <">> || {_JID, Nick} <- SortedUsers >>, <>. get_room_occupants(RoomJIDString) -> RoomJID = jid:decode(RoomJIDString), RoomName = RoomJID#jid.luser, MucService = RoomJID#jid.lserver, case get_room_state(RoomName, MucService) of {ok, StateData} -> [{U#user.jid, U#user.nick, U#user.role} || U <- maps:values(StateData#state.users)]; error -> [] end. -spec get_room_state(binary(), binary()) -> {ok, mod_muc_room:state()} | error. get_room_state(RoomName, MucService) -> case mod_muc:find_online_room(RoomName, MucService) of {ok, RoomPid} -> get_room_state(RoomPid); error -> error end. -spec get_room_state(pid()) -> {ok, mod_muc_room:state()} | error. get_room_state(RoomPid) -> case mod_muc_room:get_state(RoomPid) of {ok, State} -> {ok, State}; {error, _} -> error end. get_proc_name(Host) -> gen_mod:get_module_proc(Host, ?MODULE). calc_hour_offset(TimeHere) -> TimeZero = calendar:universal_time(), TimeHereHour = calendar:datetime_to_gregorian_seconds(TimeHere) div 3600, TimeZeroHour = calendar:datetime_to_gregorian_seconds(TimeZero) div 3600, TimeHereHour - TimeZeroHour. fjoin(FileList) -> list_to_binary(filename:join([binary_to_list(File) || File <- FileList])). -spec tr(binary(), binary()) -> binary(). tr(Lang, Text) -> translate:translate(Lang, Text). has_no_permanent_store_hint(Packet) -> xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-storage'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-permanent-store'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-permanent-storage'}). mod_opt_type(access_log) -> econf:acl(); mod_opt_type(cssfile) -> econf:url_or_file(); mod_opt_type(dirname) -> econf:enum([room_jid, room_name]); mod_opt_type(dirtype) -> econf:enum([subdirs, plain]); mod_opt_type(file_format) -> econf:enum([html, plaintext]); mod_opt_type(file_permissions) -> econf:and_then( econf:options( #{mode => econf:non_neg_int(), group => econf:non_neg_int()}), fun(Opts) -> {proplists:get_value(mode, Opts, 644), proplists:get_value(group, Opts, 33)} end); mod_opt_type(outdir) -> econf:directory(write); mod_opt_type(spam_prevention) -> econf:bool(); mod_opt_type(timezone) -> econf:enum([local, universal]); mod_opt_type(url) -> econf:url(); mod_opt_type(top_link) -> econf:and_then( econf:non_empty( econf:map(econf:binary(), econf:binary())), fun hd/1). -spec mod_options(binary()) -> [{top_link, {binary(), binary()}} | {file_permissions, {non_neg_integer(), non_neg_integer()}} | {atom(), any()}]. mod_options(_) -> [{access_log, muc_admin}, {cssfile, {file, filename:join(misc:css_dir(), <<"muc.css">>)}}, {dirname, room_jid}, {dirtype, subdirs}, {file_format, html}, {file_permissions, {644, 33}}, {outdir, <<"www/muc">>}, {spam_prevention, true}, {timezone, local}, {url, undefined}, {top_link, {<<"/">>, <<"Home">>}}]. mod_doc() -> #{desc => [?T("This module enables optional logging " "of Multi-User Chat (MUC) public " "conversations to HTML. Once you enable " "this module, users can join a room using a " "MUC capable XMPP client, and if they have " "enough privileges, they can request the " "configuration form in which they can set " "the option to enable room logging."), "", ?T("Features:"), "", ?T("- Room details are added on top of each page: " "room title, JID, author, subject and configuration."), "", ?T("- The room JID in the generated HTML is a link " "to join the room (using XMPP URI)."), "", ?T("- Subject and room configuration changes are tracked " "and displayed."), "", ?T("- Joins, leaves, nick changes, kicks, bans and '/me' " "are tracked and displayed, including the reason if available."), "", ?T("- Generated HTML files are XHTML 1.0 Transitional and " "CSS compliant."), "", ?T("- Timestamps are self-referencing links."), "", ?T("- Links on top for quicker navigation: " "Previous day, Next day, Up."), "", ?T("- CSS is used for style definition, and a custom " "CSS file can be used."), "", ?T("- URLs on messages and subjects are converted to hyperlinks."), "", ?T("- Timezone used on timestamps is shown on the log files."), "", ?T("- A custom link can be added on top of each page."), "", ?T("The module depends on _`mod_muc`_.")], opts => [{access_log, #{value => ?T("AccessName"), desc => ?T("This option restricts which occupants are " "allowed to enable or disable room logging. " "The default value is 'muc_admin'. NOTE: " "for this default setting you need to have an " "access rule for 'muc_admin' in order to take effect.")}}, {cssfile, #{value => ?T("Path | URL"), desc => ?T("With this option you can set whether the HTML " "files should have a custom CSS file or if they " "need to use the embedded CSS. Allowed values " "are either 'Path' to local file or an 'URL' to " "a remote file. By default a predefined CSS will " "be embedded into the HTML page.")}}, {dirname, #{value => "room_jid | room_name", desc => ?T("Allows to configure the name of the room directory. " "If set to 'room_jid', the room directory name will " "be the full room JID. Otherwise, the room directory " "name will be only the room name, not including the " "MUC service name. The default value is 'room_jid'.")}}, {dirtype, #{value => "subdirs | plain", desc => ?T("The type of the created directories can be specified " "with this option. If set to 'subdirs', subdirectories " "are created for each year and month. Otherwise, the " "names of the log files contain the full date, and " "there are no subdirectories. The default value is 'subdirs'.")}}, {file_format, #{value => "html | plaintext", desc => ?T("Define the format of the log files: 'html' stores " "in HTML format, 'plaintext' stores in plain text. " "The default value is 'html'.")}}, {file_permissions, #{value => "{mode: Mode, group: Group}", desc => ?T("Define the permissions that must be used when " "creating the log files: the number of the mode, " "and the numeric id of the group that will own the " "files. The default value is shown in the example below:"), example => ["file_permissions:", " mode: 644", " group: 33"]}}, {outdir, #{value => ?T("Path"), desc => ?T("This option sets the full path to the directory " "in which the HTML files should be stored. " "Make sure the ejabberd daemon user has write " "access on that directory. The default value is 'www/muc'.")}}, {spam_prevention, #{value => "true | false", desc => ?T("If set to 'true', a special attribute is added to links " "that prevent their indexation by search engines. " "The default value is 'true', which mean that 'nofollow' " "attributes will be added to user submitted links.")}}, {timezone, #{value => "local | universal", desc => ?T("The time zone for the logs is configurable with " "this option. If set to 'local', the local time, as " "reported to Erlang emulator by the operating system, " "will be used. Otherwise, UTC time will be used. " "The default value is 'local'.")}}, {url, #{value => ?T("URL"), desc => ?T("A top level 'URL' where a client can access " "logs of a particular conference. The conference name " "is appended to the URL if 'dirname' option is set to " "'room_name' or a conference JID is appended to the 'URL' " "otherwise. There is no default value.")}}, {top_link, #{value => "{URL: Text}", desc => ?T("With this option you can customize the link on " "the top right corner of each log file. " "The default value is shown in the example below:"), example => ["top_link:", " /: Home"]}}]}. ejabberd-21.12/src/ejabberd_db_sup.erl0000644000232200023220000000334214154362354020221 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 13 June 2019 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_db_sup). -behaviour(supervisor). %% API -export([start_link/0]). %% Supervisor callbacks -export([init/1]). %%%=================================================================== %%% API functions %%%=================================================================== start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([]) -> {ok, {{one_for_one, 10, 1}, []}}. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/prosody2ejabberd.erl0000644000232200023220000004256314154362354020377 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : prosody2ejabberd.erl %%% Author : Evgeny Khramtsov %%% Created : 20 Jan 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(prosody2ejabberd). %% API -export([from_dir/1]). -include_lib("xmpp/include/scram.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("mod_roster.hrl"). -include("mod_offline.hrl"). -include("mod_privacy.hrl"). %%%=================================================================== %%% API %%%=================================================================== from_dir(ProsodyDir) -> case code:ensure_loaded(luerl) of {module, _} -> case file:list_dir(ProsodyDir) of {ok, HostDirs} -> lists:foreach( fun(HostDir) -> Host = list_to_binary(HostDir), lists:foreach( fun(SubDir) -> Path = filename:join( [ProsodyDir, HostDir, SubDir]), convert_dir(Path, Host, SubDir) end, ["vcard", "accounts", "roster", "private", "config", "offline", "privacy", "pep", "pubsub"]) end, HostDirs); {error, Why} = Err -> ?ERROR_MSG("Failed to list ~ts: ~ts", [ProsodyDir, file:format_error(Why)]), Err end; {error, _} = Err -> ?ERROR_MSG("The file 'luerl.beam' is not found: maybe " "ejabberd is not compiled with Lua support", []), Err end. %%%=================================================================== %%% Internal functions %%%=================================================================== convert_dir(Path, Host, Type) -> case file:list_dir(Path) of {ok, Files} -> lists:foreach( fun(File) -> FilePath = filename:join(Path, File), case Type of "pep" -> case filelib:is_dir(FilePath) of true -> JID = list_to_binary(File ++ "@" ++ Host), convert_dir(FilePath, JID, "pubsub"); false -> ok end; _ -> case eval_file(FilePath) of {ok, Data} -> Name = iolist_to_binary(filename:rootname(File)), convert_data(url_decode(Host), Type, url_decode(Name), Data); Err -> Err end end end, Files); {error, enoent} -> ok; {error, Why} = Err -> ?ERROR_MSG("Failed to list ~ts: ~ts", [Path, file:format_error(Why)]), Err end. eval_file(Path) -> case file:read_file(Path) of {ok, Data} -> State0 = luerl:init(), State1 = luerl:set_table([item], fun([X], State) -> {[X], State} end, State0), NewData = case filename:extension(Path) of ".list" -> <<"return {", Data/binary, "};">>; _ -> Data end, case luerl:eval(NewData, State1) of {ok, _} = Res -> Res; {error, Why} = Err -> ?ERROR_MSG("Failed to eval ~ts: ~p", [Path, Why]), Err end; {error, Why} = Err -> ?ERROR_MSG("Failed to read file ~ts: ~ts", [Path, file:format_error(Why)]), Err end. maybe_get_scram_auth(Data) -> case proplists:get_value(<<"iteration_count">>, Data, no_ic) of IC when is_float(IC) -> %% A float like 4096.0 is read #scram{ storedkey = misc:hex_to_base64(proplists:get_value(<<"stored_key">>, Data, <<"">>)), serverkey = misc:hex_to_base64(proplists:get_value(<<"server_key">>, Data, <<"">>)), salt = base64:encode(proplists:get_value(<<"salt">>, Data, <<"">>)), iterationcount = round(IC) }; _ -> <<"">> end. convert_data(Host, "accounts", User, [Data]) -> Password = case proplists:get_value(<<"password">>, Data, no_pass) of no_pass -> maybe_get_scram_auth(Data); Pass when is_binary(Pass) -> Pass end, case ejabberd_auth:try_register(User, Host, Password) of ok -> ok; Err -> ?ERROR_MSG("Failed to register user ~ts@~ts: ~p", [User, Host, Err]), Err end; convert_data(Host, "roster", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), Rosters = lists:flatmap( fun({<<"pending">>, L}) -> convert_pending_item(LUser, LServer, L); ({S, L}) when is_binary(S) -> convert_roster_item(LUser, LServer, S, L); (_) -> [] end, Data), lists:foreach(fun mod_roster:set_roster/1, Rosters); convert_data(Host, "private", User, [Data]) -> PrivData = lists:flatmap( fun({_TagXMLNS, Raw}) -> case deserialize(Raw) of [El] -> XMLNS = fxml:get_tag_attr_s(<<"xmlns">>, El), [{XMLNS, El}]; _ -> [] end end, Data), mod_private:set_data(jid:make(User, Host), PrivData); convert_data(Host, "vcard", User, [Data]) -> LServer = jid:nameprep(Host), case deserialize(Data) of [VCard] -> mod_vcard:set_vcard(User, LServer, VCard); _ -> ok end; convert_data(_Host, "config", _User, [Data]) -> RoomJID1 = case proplists:get_value(<<"jid">>, Data, not_found) of not_found -> proplists:get_value(<<"_jid">>, Data, room_jid_not_found); A when is_binary(A) -> A end, RoomJID = jid:decode(RoomJID1), Config = proplists:get_value(<<"_data">>, Data, []), RoomCfg = convert_room_config(Data), case proplists:get_bool(<<"persistent">>, Config) of true when RoomJID /= error -> mod_muc:store_room(find_serverhost(RoomJID#jid.lserver), RoomJID#jid.lserver, RoomJID#jid.luser, RoomCfg); _ -> ok end; convert_data(Host, "offline", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), lists:foreach( fun({_, RawXML}) -> case deserialize(RawXML) of [El] -> case el_to_offline_msg(LUser, LServer, El) of [Msg] -> ok = mod_offline:store_offline_msg(Msg); [] -> ok end; _ -> ok end end, Data); convert_data(Host, "privacy", User, [Data]) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), Lists = proplists:get_value(<<"lists">>, Data, []), Priv = #privacy{ us = {LUser, LServer}, default = proplists:get_value(<<"default">>, Data, none), lists = lists:flatmap( fun({Name, Vals}) -> Items = proplists:get_value(<<"items">>, Vals, []), case lists:map(fun convert_privacy_item/1, Items) of [] -> []; ListItems -> [{Name, ListItems}] end end, Lists)}, mod_privacy:set_list(Priv); convert_data(HostStr, "pubsub", Node, [Data]) -> case decode_pubsub_host(HostStr) of Host when is_binary(Host); is_tuple(Host) -> Type = node_type(Host), NodeData = convert_node_config(HostStr, Data), DefaultConfig = mod_pubsub:config(Host, default_node_config, []), Owner = proplists:get_value(owner, NodeData), Options = lists:foldl( fun({_Opt, undefined}, Acc) -> Acc; ({Opt, Val}, Acc) -> lists:keystore(Opt, 1, Acc, {Opt, Val}) end, DefaultConfig, proplists:get_value(options, NodeData)), case mod_pubsub:tree_action(Host, create_node, [Host, Node, Type, Owner, Options, []]) of {ok, Nidx} -> case mod_pubsub:node_action(Host, Type, create_node, [Nidx, Owner]) of {result, _} -> Access = open, % always allow subscriptions proplists:get_value(access_model, Options), Publish = open, % always allow publications proplists:get_value(publish_model, Options), MaxItems = proplists:get_value(max_items, Options), Affiliations = proplists:get_value(affiliations, NodeData), Subscriptions = proplists:get_value(subscriptions, NodeData), Items = proplists:get_value(items, NodeData), [mod_pubsub:node_action(Host, Type, set_affiliation, [Nidx, Entity, Aff]) || {Entity, Aff} <- Affiliations, Entity =/= Owner], [mod_pubsub:node_action(Host, Type, subscribe_node, [Nidx, jid:make(Entity), Entity, Access, never, [], [], []]) || Entity <- Subscriptions], [mod_pubsub:node_action(Host, Type, publish_item, [Nidx, Publisher, Publish, MaxItems, ItemId, Payload, []]) || {ItemId, Publisher, Payload} <- Items]; Error -> Error end; Error -> ?ERROR_MSG("Failed to import pubsub node ~ts on ~p:~n~p", [Node, Host, NodeData]), Error end; Error -> ?ERROR_MSG("Failed to import pubsub node: ~p", [Error]), Error end; convert_data(_Host, _Type, _User, _Data) -> ok. convert_pending_item(LUser, LServer, LuaList) -> lists:flatmap( fun({S, true}) -> try jid:decode(S) of J -> LJID = jid:tolower(J), [#roster{usj = {LUser, LServer, LJID}, us = {LUser, LServer}, jid = LJID, ask = in}] catch _:{bad_jid, _} -> [] end; (_) -> [] end, LuaList). convert_roster_item(LUser, LServer, JIDstring, LuaList) -> try jid:decode(JIDstring) of JID -> LJID = jid:tolower(JID), InitR = #roster{usj = {LUser, LServer, LJID}, us = {LUser, LServer}, jid = LJID}, lists:foldl( fun({<<"groups">>, Val}, [R]) -> Gs = lists:flatmap( fun({G, true}) -> [G]; (_) -> [] end, Val), [R#roster{groups = Gs}]; ({<<"subscription">>, Sub}, [R]) -> [R#roster{subscription = misc:binary_to_atom(Sub)}]; ({<<"ask">>, <<"subscribe">>}, [R]) -> [R#roster{ask = out}]; ({<<"name">>, Name}, [R]) -> [R#roster{name = Name}]; ({<<"persist">>, false}, _) -> []; (_, []) -> [] end, [InitR], LuaList) catch _:{bad_jid, _} -> [] end. convert_room_affiliations(Data) -> lists:flatmap( fun({J, Aff}) -> try jid:decode(J) of #jid{luser = U, lserver = S} -> [{{U, S, <<>>}, misc:binary_to_atom(Aff)}] catch _:{bad_jid, _} -> [] end end, proplists:get_value(<<"_affiliations">>, Data, [])). convert_room_config(Data) -> Config = proplists:get_value(<<"_data">>, Data, []), Pass = case proplists:get_value(<<"password">>, Config, <<"">>) of <<"">> -> []; Password -> [{password_protected, true}, {password, Password}] end, Subj = try jid:decode( proplists:get_value( <<"subject_from">>, Config, <<"">>)) of #jid{lresource = Nick} when Nick /= <<"">> -> [{subject, proplists:get_value(<<"subject">>, Config, <<"">>)}, {subject_author, Nick}] catch _:{bad_jid, _} -> [] end, Anonymous = case proplists:get_value(<<"whois">>, Config, <<"moderators">>) of <<"moderators">> -> true; _ -> false end, [{affiliations, convert_room_affiliations(Data)}, {allow_change_subj, proplists:get_bool(<<"changesubject">>, Config)}, {mam, proplists:get_bool(<<"archiving">>, Config)}, {description, proplists:get_value(<<"description">>, Config, <<"">>)}, {members_only, proplists:get_bool(<<"members_only">>, Config)}, {moderated, proplists:get_bool(<<"moderated">>, Config)}, {persistent, proplists:get_bool(<<"persistent">>, Config)}, {anonymous, Anonymous}] ++ Pass ++ Subj. convert_privacy_item({_, Item}) -> Action = proplists:get_value(<<"action">>, Item, <<"allow">>), Order = proplists:get_value(<<"order">>, Item, 0), T = misc:binary_to_atom(proplists:get_value(<<"type">>, Item, <<"none">>)), V = proplists:get_value(<<"value">>, Item, <<"">>), MatchIQ = proplists:get_bool(<<"iq">>, Item), MatchMsg = proplists:get_bool(<<"message">>, Item), MatchPresIn = proplists:get_bool(<<"presence-in">>, Item), MatchPresOut = proplists:get_bool(<<"presence-out">>, Item), MatchAll = if (MatchIQ == false) and (MatchMsg == false) and (MatchPresIn == false) and (MatchPresOut == false) -> true; true -> false end, {Type, Value} = try case T of none -> {T, none}; group -> {T, V}; jid -> {T, jid:tolower(jid:decode(V))}; subscription -> {T, misc:binary_to_atom(V)} end catch _:_ -> {none, none} end, #listitem{type = Type, value = Value, action = misc:binary_to_atom(Action), order = erlang:trunc(Order), match_all = MatchAll, match_iq = MatchIQ, match_message = MatchMsg, match_presence_in = MatchPresIn, match_presence_out = MatchPresOut}. url_decode(Encoded) -> url_decode(Encoded, <<>>). url_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> Hex = list_to_integer([Hi, Lo], 16), url_decode(Tail, <>); url_decode(<>, Acc) -> url_decode(Tail, <>); url_decode(<<>>, Acc) -> Acc. decode_pubsub_host(Host) -> try jid:decode(Host) of #jid{luser = <<>>, lserver = LServer} -> LServer; #jid{luser = LUser, lserver = LServer} -> {LUser, LServer, <<>>} catch _:{bad_jid, _} -> bad_jid end. node_type({_U, _S, _R}) -> <<"pep">>; node_type(Host) -> hd(mod_pubsub:plugins(Host)). max_items(Config, Default) -> case round(proplists:get_value(<<"max_items">>, Config, Default)) of I when I =< 0 -> Default; I -> I end. convert_node_affiliations(Data) -> lists:flatmap( fun({J, Aff}) -> try jid:decode(J) of JID -> [{JID, misc:binary_to_atom(Aff)}] catch _:{bad_jid, _} -> [] end end, proplists:get_value(<<"affiliations">>, Data, [])). convert_node_subscriptions(Data) -> lists:flatmap( fun({J, true}) -> try jid:decode(J) of JID -> [jid:tolower(JID)] catch _:{bad_jid, _} -> [] end; (_) -> [] end, proplists:get_value(<<"subscribers">>, Data, [])). convert_node_items(Host, Data) -> Authors = proplists:get_value(<<"data_author">>, Data, []), lists:flatmap( fun({ItemId, Item}) -> try jid:decode(proplists:get_value(ItemId, Authors, Host)) of JID -> [El] = deserialize(Item), [{ItemId, JID, El#xmlel.children}] catch _:{bad_jid, _} -> [] end end, proplists:get_value(<<"data">>, Data, [])). convert_node_config(Host, Data) -> Config = proplists:get_value(<<"config">>, Data, []), [{affiliations, convert_node_affiliations(Data)}, {subscriptions, convert_node_subscriptions(Data)}, {owner, jid:decode(proplists:get_value(<<"creator">>, Config, Host))}, {items, convert_node_items(Host, Data)}, {options, [ {deliver_notifications, proplists:get_value(<<"deliver_notifications">>, Config, true)}, {deliver_payloads, proplists:get_value(<<"deliver_payloads">>, Config, true)}, {persist_items, proplists:get_value(<<"persist_items">>, Config, true)}, {max_items, max_items(Config, 10)}, {access_model, misc:binary_to_atom(proplists:get_value(<<"access_model">>, Config, <<"open">>))}, {publish_model, misc:binary_to_atom(proplists:get_value(<<"publish_model">>, Config, <<"publishers">>))}, {title, proplists:get_value(<<"title">>, Config, <<"">>)} ]} ]. el_to_offline_msg(LUser, LServer, #xmlel{attrs = Attrs} = El) -> try TS = xmpp_util:decode_timestamp( fxml:get_attr_s(<<"stamp">>, Attrs)), Attrs1 = lists:filter( fun({<<"stamp">>, _}) -> false; ({<<"stamp_legacy">>, _}) -> false; (_) -> true end, Attrs), El1 = El#xmlel{attrs = Attrs1}, case xmpp:decode(El1, ?NS_CLIENT, [ignore_els]) of #message{from = #jid{} = From, to = #jid{} = To} = Packet -> [#offline_msg{ us = {LUser, LServer}, timestamp = TS, expire = never, from = From, to = To, packet = Packet}]; _ -> [] end catch _:{bad_timestamp, _} -> []; _:{bad_jid, _} -> []; _:{xmpp_codec, _} -> [] end. find_serverhost(Host) -> [ServerHost] = lists:filter( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> lists:member(Host, gen_mod:get_module_opt_hosts(ServerHost, mod_muc)); false -> false end end, ejabberd_option:hosts()), ServerHost. deserialize(L) -> deserialize(L, #xmlel{}, []). deserialize([{Other, _}|T], El, Acc) when (Other == <<"key">>) or (Other == <<"when">>) or (Other == <<"with">>) -> deserialize(T, El, Acc); deserialize([{<<"attr">>, Attrs}|T], El, Acc) -> deserialize(T, El#xmlel{attrs = Attrs ++ El#xmlel.attrs}, Acc); deserialize([{<<"name">>, Name}|T], El, Acc) -> deserialize(T, El#xmlel{name = Name}, Acc); deserialize([{_, S}|T], #xmlel{children = Els} = El, Acc) when is_binary(S) -> deserialize(T, El#xmlel{children = [{xmlcdata, S}|Els]}, Acc); deserialize([{_, L}|T], #xmlel{children = Els} = El, Acc) when is_list(L) -> deserialize(T, El#xmlel{children = deserialize(L) ++ Els}, Acc); deserialize([], #xmlel{children = Els} = El, Acc) -> [El#xmlel{children = lists:reverse(Els)}|Acc]. ejabberd-21.12/src/ejabberd_option.erl0000644000232200023220000011142714154362354020261 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(ejabberd_option). -export([access_rules/0, access_rules/1]). -export([acl/0, acl/1]). -export([acme/0]). -export([allow_contrib_modules/0]). -export([allow_multiple_connections/0, allow_multiple_connections/1]). -export([anonymous_protocol/0, anonymous_protocol/1]). -export([api_permissions/0]). -export([append_host_config/0]). -export([auth_cache_life_time/0]). -export([auth_cache_missed/0]). -export([auth_cache_size/0]). -export([auth_method/0, auth_method/1]). -export([auth_opts/0, auth_opts/1]). -export([auth_password_format/0, auth_password_format/1]). -export([auth_scram_hash/0, auth_scram_hash/1]). -export([auth_use_cache/0, auth_use_cache/1]). -export([c2s_cafile/0, c2s_cafile/1]). -export([c2s_ciphers/0, c2s_ciphers/1]). -export([c2s_dhfile/0, c2s_dhfile/1]). -export([c2s_protocol_options/0, c2s_protocol_options/1]). -export([c2s_tls_compression/0, c2s_tls_compression/1]). -export([ca_file/0]). -export([cache_life_time/0, cache_life_time/1]). -export([cache_missed/0, cache_missed/1]). -export([cache_size/0, cache_size/1]). -export([captcha_cmd/0]). -export([captcha_host/0]). -export([captcha_limit/0]). -export([captcha_url/0]). -export([certfiles/0]). -export([cluster_backend/0]). -export([cluster_nodes/0]). -export([default_db/0, default_db/1]). -export([default_ram_db/0, default_ram_db/1]). -export([define_macro/0, define_macro/1]). -export([disable_sasl_mechanisms/0, disable_sasl_mechanisms/1]). -export([domain_balancing/0]). -export([ext_api_headers/0, ext_api_headers/1]). -export([ext_api_http_pool_size/0, ext_api_http_pool_size/1]). -export([ext_api_path_oauth/0]). -export([ext_api_url/0, ext_api_url/1]). -export([extauth_pool_name/0, extauth_pool_name/1]). -export([extauth_pool_size/0, extauth_pool_size/1]). -export([extauth_program/0, extauth_program/1]). -export([fqdn/0]). -export([hide_sensitive_log_data/0, hide_sensitive_log_data/1]). -export([host_config/0]). -export([hosts/0]). -export([include_config_file/0, include_config_file/1]). -export([jwt_auth_only_rule/0, jwt_auth_only_rule/1]). -export([jwt_jid_field/0, jwt_jid_field/1]). -export([jwt_key/0, jwt_key/1]). -export([language/0, language/1]). -export([ldap_backups/0, ldap_backups/1]). -export([ldap_base/0, ldap_base/1]). -export([ldap_deref_aliases/0, ldap_deref_aliases/1]). -export([ldap_dn_filter/0, ldap_dn_filter/1]). -export([ldap_encrypt/0, ldap_encrypt/1]). -export([ldap_filter/0, ldap_filter/1]). -export([ldap_password/0, ldap_password/1]). -export([ldap_port/0, ldap_port/1]). -export([ldap_rootdn/0, ldap_rootdn/1]). -export([ldap_servers/0, ldap_servers/1]). -export([ldap_tls_cacertfile/0, ldap_tls_cacertfile/1]). -export([ldap_tls_certfile/0, ldap_tls_certfile/1]). -export([ldap_tls_depth/0, ldap_tls_depth/1]). -export([ldap_tls_verify/0, ldap_tls_verify/1]). -export([ldap_uids/0, ldap_uids/1]). -export([listen/0]). -export([log_rotate_count/0]). -export([log_rotate_size/0]). -export([loglevel/0]). -export([max_fsm_queue/0, max_fsm_queue/1]). -export([modules/0, modules/1]). -export([negotiation_timeout/0]). -export([net_ticktime/0]). -export([new_sql_schema/0]). -export([oauth_access/0, oauth_access/1]). -export([oauth_cache_life_time/0]). -export([oauth_cache_missed/0]). -export([oauth_cache_rest_failure_life_time/0]). -export([oauth_cache_size/0]). -export([oauth_client_id_check/0, oauth_client_id_check/1]). -export([oauth_db_type/0]). -export([oauth_expire/0]). -export([oauth_use_cache/0]). -export([oom_killer/0]). -export([oom_queue/0]). -export([oom_watermark/0]). -export([outgoing_s2s_families/0, outgoing_s2s_families/1]). -export([outgoing_s2s_ipv4_address/0, outgoing_s2s_ipv4_address/1]). -export([outgoing_s2s_ipv6_address/0, outgoing_s2s_ipv6_address/1]). -export([outgoing_s2s_port/0, outgoing_s2s_port/1]). -export([outgoing_s2s_timeout/0, outgoing_s2s_timeout/1]). -export([pam_service/0, pam_service/1]). -export([pam_userinfotype/0, pam_userinfotype/1]). -export([pgsql_users_number_estimate/0, pgsql_users_number_estimate/1]). -export([queue_dir/0]). -export([queue_type/0, queue_type/1]). -export([redis_connect_timeout/0]). -export([redis_db/0]). -export([redis_password/0]). -export([redis_pool_size/0]). -export([redis_port/0]). -export([redis_queue_type/0]). -export([redis_server/0]). -export([registration_timeout/0]). -export([resource_conflict/0, resource_conflict/1]). -export([router_cache_life_time/0]). -export([router_cache_missed/0]). -export([router_cache_size/0]). -export([router_db_type/0]). -export([router_use_cache/0]). -export([rpc_timeout/0]). -export([s2s_access/0, s2s_access/1]). -export([s2s_cafile/0, s2s_cafile/1]). -export([s2s_ciphers/0, s2s_ciphers/1]). -export([s2s_dhfile/0, s2s_dhfile/1]). -export([s2s_dns_retries/0, s2s_dns_retries/1]). -export([s2s_dns_timeout/0, s2s_dns_timeout/1]). -export([s2s_max_retry_delay/0]). -export([s2s_protocol_options/0, s2s_protocol_options/1]). -export([s2s_queue_type/0, s2s_queue_type/1]). -export([s2s_timeout/0, s2s_timeout/1]). -export([s2s_tls_compression/0, s2s_tls_compression/1]). -export([s2s_use_starttls/0, s2s_use_starttls/1]). -export([s2s_zlib/0, s2s_zlib/1]). -export([shaper/0]). -export([shaper_rules/0, shaper_rules/1]). -export([sm_cache_life_time/0]). -export([sm_cache_missed/0]). -export([sm_cache_size/0]). -export([sm_db_type/0, sm_db_type/1]). -export([sm_use_cache/0, sm_use_cache/1]). -export([sql_connect_timeout/0, sql_connect_timeout/1]). -export([sql_database/0, sql_database/1]). -export([sql_keepalive_interval/0, sql_keepalive_interval/1]). -export([sql_odbc_driver/0, sql_odbc_driver/1]). -export([sql_password/0, sql_password/1]). -export([sql_pool_size/0, sql_pool_size/1]). -export([sql_port/0, sql_port/1]). -export([sql_prepared_statements/0, sql_prepared_statements/1]). -export([sql_query_timeout/0, sql_query_timeout/1]). -export([sql_queue_type/0, sql_queue_type/1]). -export([sql_server/0, sql_server/1]). -export([sql_ssl/0, sql_ssl/1]). -export([sql_ssl_cafile/0, sql_ssl_cafile/1]). -export([sql_ssl_certfile/0, sql_ssl_certfile/1]). -export([sql_ssl_verify/0, sql_ssl_verify/1]). -export([sql_start_interval/0, sql_start_interval/1]). -export([sql_type/0, sql_type/1]). -export([sql_username/0, sql_username/1]). -export([trusted_proxies/0]). -export([use_cache/0, use_cache/1]). -export([validate_stream/0]). -export([version/0]). -export([websocket_origin/0]). -export([websocket_ping_interval/0]). -export([websocket_timeout/0]). -spec access_rules() -> [{atom(),acl:access()}]. access_rules() -> access_rules(global). -spec access_rules(global | binary()) -> [{atom(),acl:access()}]. access_rules(Host) -> ejabberd_config:get_option({access_rules, Host}). -spec acl() -> [{atom(),[acl:acl_rule()]}]. acl() -> acl(global). -spec acl(global | binary()) -> [{atom(),[acl:acl_rule()]}]. acl(Host) -> ejabberd_config:get_option({acl, Host}). -spec acme() -> #{'auto'=>boolean(), 'ca_url'=>binary(), 'cert_type'=>'ec' | 'rsa', 'contact'=>[binary()]}. acme() -> ejabberd_config:get_option({acme, global}). -spec allow_contrib_modules() -> boolean(). allow_contrib_modules() -> ejabberd_config:get_option({allow_contrib_modules, global}). -spec allow_multiple_connections() -> boolean(). allow_multiple_connections() -> allow_multiple_connections(global). -spec allow_multiple_connections(global | binary()) -> boolean(). allow_multiple_connections(Host) -> ejabberd_config:get_option({allow_multiple_connections, Host}). -spec anonymous_protocol() -> 'both' | 'login_anon' | 'sasl_anon'. anonymous_protocol() -> anonymous_protocol(global). -spec anonymous_protocol(global | binary()) -> 'both' | 'login_anon' | 'sasl_anon'. anonymous_protocol(Host) -> ejabberd_config:get_option({anonymous_protocol, Host}). -spec api_permissions() -> [ejabberd_access_permissions:permission()]. api_permissions() -> ejabberd_config:get_option({api_permissions, global}). -spec append_host_config() -> [{binary(),any()}]. append_host_config() -> ejabberd_config:get_option({append_host_config, global}). -spec auth_cache_life_time() -> 'infinity' | pos_integer(). auth_cache_life_time() -> ejabberd_config:get_option({auth_cache_life_time, global}). -spec auth_cache_missed() -> boolean(). auth_cache_missed() -> ejabberd_config:get_option({auth_cache_missed, global}). -spec auth_cache_size() -> 'infinity' | pos_integer(). auth_cache_size() -> ejabberd_config:get_option({auth_cache_size, global}). -spec auth_method() -> [atom()]. auth_method() -> auth_method(global). -spec auth_method(global | binary()) -> [atom()]. auth_method(Host) -> ejabberd_config:get_option({auth_method, Host}). -spec auth_opts() -> [{any(),any()}]. auth_opts() -> auth_opts(global). -spec auth_opts(global | binary()) -> [{any(),any()}]. auth_opts(Host) -> ejabberd_config:get_option({auth_opts, Host}). -spec auth_password_format() -> 'plain' | 'scram'. auth_password_format() -> auth_password_format(global). -spec auth_password_format(global | binary()) -> 'plain' | 'scram'. auth_password_format(Host) -> ejabberd_config:get_option({auth_password_format, Host}). -spec auth_scram_hash() -> 'sha' | 'sha256' | 'sha512'. auth_scram_hash() -> auth_scram_hash(global). -spec auth_scram_hash(global | binary()) -> 'sha' | 'sha256' | 'sha512'. auth_scram_hash(Host) -> ejabberd_config:get_option({auth_scram_hash, Host}). -spec auth_use_cache() -> boolean(). auth_use_cache() -> auth_use_cache(global). -spec auth_use_cache(global | binary()) -> boolean(). auth_use_cache(Host) -> ejabberd_config:get_option({auth_use_cache, Host}). -spec c2s_cafile() -> 'undefined' | binary(). c2s_cafile() -> c2s_cafile(global). -spec c2s_cafile(global | binary()) -> 'undefined' | binary(). c2s_cafile(Host) -> ejabberd_config:get_option({c2s_cafile, Host}). -spec c2s_ciphers() -> 'undefined' | binary(). c2s_ciphers() -> c2s_ciphers(global). -spec c2s_ciphers(global | binary()) -> 'undefined' | binary(). c2s_ciphers(Host) -> ejabberd_config:get_option({c2s_ciphers, Host}). -spec c2s_dhfile() -> 'undefined' | binary(). c2s_dhfile() -> c2s_dhfile(global). -spec c2s_dhfile(global | binary()) -> 'undefined' | binary(). c2s_dhfile(Host) -> ejabberd_config:get_option({c2s_dhfile, Host}). -spec c2s_protocol_options() -> 'undefined' | binary(). c2s_protocol_options() -> c2s_protocol_options(global). -spec c2s_protocol_options(global | binary()) -> 'undefined' | binary(). c2s_protocol_options(Host) -> ejabberd_config:get_option({c2s_protocol_options, Host}). -spec c2s_tls_compression() -> 'false' | 'true' | 'undefined'. c2s_tls_compression() -> c2s_tls_compression(global). -spec c2s_tls_compression(global | binary()) -> 'false' | 'true' | 'undefined'. c2s_tls_compression(Host) -> ejabberd_config:get_option({c2s_tls_compression, Host}). -spec ca_file() -> binary(). ca_file() -> ejabberd_config:get_option({ca_file, global}). -spec cache_life_time() -> 'infinity' | pos_integer(). cache_life_time() -> cache_life_time(global). -spec cache_life_time(global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Host) -> ejabberd_config:get_option({cache_life_time, Host}). -spec cache_missed() -> boolean(). cache_missed() -> cache_missed(global). -spec cache_missed(global | binary()) -> boolean(). cache_missed(Host) -> ejabberd_config:get_option({cache_missed, Host}). -spec cache_size() -> 'infinity' | pos_integer(). cache_size() -> cache_size(global). -spec cache_size(global | binary()) -> 'infinity' | pos_integer(). cache_size(Host) -> ejabberd_config:get_option({cache_size, Host}). -spec captcha_cmd() -> 'undefined' | binary(). captcha_cmd() -> ejabberd_config:get_option({captcha_cmd, global}). -spec captcha_host() -> binary(). captcha_host() -> ejabberd_config:get_option({captcha_host, global}). -spec captcha_limit() -> 'infinity' | pos_integer(). captcha_limit() -> ejabberd_config:get_option({captcha_limit, global}). -spec captcha_url() -> 'undefined' | binary(). captcha_url() -> ejabberd_config:get_option({captcha_url, global}). -spec certfiles() -> 'undefined' | [binary()]. certfiles() -> ejabberd_config:get_option({certfiles, global}). -spec cluster_backend() -> atom(). cluster_backend() -> ejabberd_config:get_option({cluster_backend, global}). -spec cluster_nodes() -> [atom()]. cluster_nodes() -> ejabberd_config:get_option({cluster_nodes, global}). -spec default_db() -> 'mnesia' | 'sql'. default_db() -> default_db(global). -spec default_db(global | binary()) -> 'mnesia' | 'sql'. default_db(Host) -> ejabberd_config:get_option({default_db, Host}). -spec default_ram_db() -> 'mnesia' | 'redis' | 'sql'. default_ram_db() -> default_ram_db(global). -spec default_ram_db(global | binary()) -> 'mnesia' | 'redis' | 'sql'. default_ram_db(Host) -> ejabberd_config:get_option({default_ram_db, Host}). -spec define_macro() -> any(). define_macro() -> define_macro(global). -spec define_macro(global | binary()) -> any(). define_macro(Host) -> ejabberd_config:get_option({define_macro, Host}). -spec disable_sasl_mechanisms() -> [binary()]. disable_sasl_mechanisms() -> disable_sasl_mechanisms(global). -spec disable_sasl_mechanisms(global | binary()) -> [binary()]. disable_sasl_mechanisms(Host) -> ejabberd_config:get_option({disable_sasl_mechanisms, Host}). -spec domain_balancing() -> #{binary()=>#{'component_number':=1..1114111, 'type'=>'bare_destination' | 'bare_source' | 'destination' | 'random' | 'source'}}. domain_balancing() -> ejabberd_config:get_option({domain_balancing, global}). -spec ext_api_headers() -> binary(). ext_api_headers() -> ext_api_headers(global). -spec ext_api_headers(global | binary()) -> binary(). ext_api_headers(Host) -> ejabberd_config:get_option({ext_api_headers, Host}). -spec ext_api_http_pool_size() -> pos_integer(). ext_api_http_pool_size() -> ext_api_http_pool_size(global). -spec ext_api_http_pool_size(global | binary()) -> pos_integer(). ext_api_http_pool_size(Host) -> ejabberd_config:get_option({ext_api_http_pool_size, Host}). -spec ext_api_path_oauth() -> binary(). ext_api_path_oauth() -> ejabberd_config:get_option({ext_api_path_oauth, global}). -spec ext_api_url() -> binary(). ext_api_url() -> ext_api_url(global). -spec ext_api_url(global | binary()) -> binary(). ext_api_url(Host) -> ejabberd_config:get_option({ext_api_url, Host}). -spec extauth_pool_name() -> 'undefined' | binary(). extauth_pool_name() -> extauth_pool_name(global). -spec extauth_pool_name(global | binary()) -> 'undefined' | binary(). extauth_pool_name(Host) -> ejabberd_config:get_option({extauth_pool_name, Host}). -spec extauth_pool_size() -> 'undefined' | pos_integer(). extauth_pool_size() -> extauth_pool_size(global). -spec extauth_pool_size(global | binary()) -> 'undefined' | pos_integer(). extauth_pool_size(Host) -> ejabberd_config:get_option({extauth_pool_size, Host}). -spec extauth_program() -> 'undefined' | string(). extauth_program() -> extauth_program(global). -spec extauth_program(global | binary()) -> 'undefined' | string(). extauth_program(Host) -> ejabberd_config:get_option({extauth_program, Host}). -spec fqdn() -> [binary()]. fqdn() -> ejabberd_config:get_option({fqdn, global}). -spec hide_sensitive_log_data() -> boolean(). hide_sensitive_log_data() -> hide_sensitive_log_data(global). -spec hide_sensitive_log_data(global | binary()) -> boolean(). hide_sensitive_log_data(Host) -> ejabberd_config:get_option({hide_sensitive_log_data, Host}). -spec host_config() -> [{binary(),any()}]. host_config() -> ejabberd_config:get_option({host_config, global}). -spec hosts() -> [binary(),...]. hosts() -> ejabberd_config:get_option({hosts, global}). -spec include_config_file() -> any(). include_config_file() -> include_config_file(global). -spec include_config_file(global | binary()) -> any(). include_config_file(Host) -> ejabberd_config:get_option({include_config_file, Host}). -spec jwt_auth_only_rule() -> atom(). jwt_auth_only_rule() -> jwt_auth_only_rule(global). -spec jwt_auth_only_rule(global | binary()) -> atom(). jwt_auth_only_rule(Host) -> ejabberd_config:get_option({jwt_auth_only_rule, Host}). -spec jwt_jid_field() -> binary(). jwt_jid_field() -> jwt_jid_field(global). -spec jwt_jid_field(global | binary()) -> binary(). jwt_jid_field(Host) -> ejabberd_config:get_option({jwt_jid_field, Host}). -spec jwt_key() -> jose_jwk:key() | 'undefined'. jwt_key() -> jwt_key(global). -spec jwt_key(global | binary()) -> jose_jwk:key() | 'undefined'. jwt_key(Host) -> ejabberd_config:get_option({jwt_key, Host}). -spec language() -> binary(). language() -> language(global). -spec language(global | binary()) -> binary(). language(Host) -> ejabberd_config:get_option({language, Host}). -spec ldap_backups() -> [binary()]. ldap_backups() -> ldap_backups(global). -spec ldap_backups(global | binary()) -> [binary()]. ldap_backups(Host) -> ejabberd_config:get_option({ldap_backups, Host}). -spec ldap_base() -> binary(). ldap_base() -> ldap_base(global). -spec ldap_base(global | binary()) -> binary(). ldap_base(Host) -> ejabberd_config:get_option({ldap_base, Host}). -spec ldap_deref_aliases() -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases() -> ldap_deref_aliases(global). -spec ldap_deref_aliases(global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases(Host) -> ejabberd_config:get_option({ldap_deref_aliases, Host}). -spec ldap_dn_filter() -> {binary(),[binary()]}. ldap_dn_filter() -> ldap_dn_filter(global). -spec ldap_dn_filter(global | binary()) -> {binary(),[binary()]}. ldap_dn_filter(Host) -> ejabberd_config:get_option({ldap_dn_filter, Host}). -spec ldap_encrypt() -> 'none' | 'starttls' | 'tls'. ldap_encrypt() -> ldap_encrypt(global). -spec ldap_encrypt(global | binary()) -> 'none' | 'starttls' | 'tls'. ldap_encrypt(Host) -> ejabberd_config:get_option({ldap_encrypt, Host}). -spec ldap_filter() -> binary(). ldap_filter() -> ldap_filter(global). -spec ldap_filter(global | binary()) -> binary(). ldap_filter(Host) -> ejabberd_config:get_option({ldap_filter, Host}). -spec ldap_password() -> binary(). ldap_password() -> ldap_password(global). -spec ldap_password(global | binary()) -> binary(). ldap_password(Host) -> ejabberd_config:get_option({ldap_password, Host}). -spec ldap_port() -> 1..1114111. ldap_port() -> ldap_port(global). -spec ldap_port(global | binary()) -> 1..1114111. ldap_port(Host) -> ejabberd_config:get_option({ldap_port, Host}). -spec ldap_rootdn() -> binary(). ldap_rootdn() -> ldap_rootdn(global). -spec ldap_rootdn(global | binary()) -> binary(). ldap_rootdn(Host) -> ejabberd_config:get_option({ldap_rootdn, Host}). -spec ldap_servers() -> [binary()]. ldap_servers() -> ldap_servers(global). -spec ldap_servers(global | binary()) -> [binary()]. ldap_servers(Host) -> ejabberd_config:get_option({ldap_servers, Host}). -spec ldap_tls_cacertfile() -> 'undefined' | binary(). ldap_tls_cacertfile() -> ldap_tls_cacertfile(global). -spec ldap_tls_cacertfile(global | binary()) -> 'undefined' | binary(). ldap_tls_cacertfile(Host) -> ejabberd_config:get_option({ldap_tls_cacertfile, Host}). -spec ldap_tls_certfile() -> 'undefined' | binary(). ldap_tls_certfile() -> ldap_tls_certfile(global). -spec ldap_tls_certfile(global | binary()) -> 'undefined' | binary(). ldap_tls_certfile(Host) -> ejabberd_config:get_option({ldap_tls_certfile, Host}). -spec ldap_tls_depth() -> 'undefined' | non_neg_integer(). ldap_tls_depth() -> ldap_tls_depth(global). -spec ldap_tls_depth(global | binary()) -> 'undefined' | non_neg_integer(). ldap_tls_depth(Host) -> ejabberd_config:get_option({ldap_tls_depth, Host}). -spec ldap_tls_verify() -> 'false' | 'hard' | 'soft'. ldap_tls_verify() -> ldap_tls_verify(global). -spec ldap_tls_verify(global | binary()) -> 'false' | 'hard' | 'soft'. ldap_tls_verify(Host) -> ejabberd_config:get_option({ldap_tls_verify, Host}). -spec ldap_uids() -> [{binary(),binary()}]. ldap_uids() -> ldap_uids(global). -spec ldap_uids(global | binary()) -> [{binary(),binary()}]. ldap_uids(Host) -> ejabberd_config:get_option({ldap_uids, Host}). -spec listen() -> [ejabberd_listener:listener()]. listen() -> ejabberd_config:get_option({listen, global}). -spec log_rotate_count() -> non_neg_integer(). log_rotate_count() -> ejabberd_config:get_option({log_rotate_count, global}). -spec log_rotate_size() -> 'infinity' | pos_integer(). log_rotate_size() -> ejabberd_config:get_option({log_rotate_size, global}). -spec loglevel() -> ejabberd_logger:loglevel(). loglevel() -> ejabberd_config:get_option({loglevel, global}). -spec max_fsm_queue() -> 'undefined' | pos_integer(). max_fsm_queue() -> max_fsm_queue(global). -spec max_fsm_queue(global | binary()) -> 'undefined' | pos_integer(). max_fsm_queue(Host) -> ejabberd_config:get_option({max_fsm_queue, Host}). -spec modules() -> [{module(),gen_mod:opts(),integer()}]. modules() -> modules(global). -spec modules(global | binary()) -> [{module(),gen_mod:opts(),integer()}]. modules(Host) -> ejabberd_config:get_option({modules, Host}). -spec negotiation_timeout() -> pos_integer(). negotiation_timeout() -> ejabberd_config:get_option({negotiation_timeout, global}). -spec net_ticktime() -> pos_integer(). net_ticktime() -> ejabberd_config:get_option({net_ticktime, global}). -spec new_sql_schema() -> boolean(). new_sql_schema() -> ejabberd_config:get_option({new_sql_schema, global}). -spec oauth_access() -> 'none' | acl:acl(). oauth_access() -> oauth_access(global). -spec oauth_access(global | binary()) -> 'none' | acl:acl(). oauth_access(Host) -> ejabberd_config:get_option({oauth_access, Host}). -spec oauth_cache_life_time() -> 'infinity' | pos_integer(). oauth_cache_life_time() -> ejabberd_config:get_option({oauth_cache_life_time, global}). -spec oauth_cache_missed() -> boolean(). oauth_cache_missed() -> ejabberd_config:get_option({oauth_cache_missed, global}). -spec oauth_cache_rest_failure_life_time() -> 'infinity' | pos_integer(). oauth_cache_rest_failure_life_time() -> ejabberd_config:get_option({oauth_cache_rest_failure_life_time, global}). -spec oauth_cache_size() -> 'infinity' | pos_integer(). oauth_cache_size() -> ejabberd_config:get_option({oauth_cache_size, global}). -spec oauth_client_id_check() -> 'allow' | 'db' | 'deny'. oauth_client_id_check() -> oauth_client_id_check(global). -spec oauth_client_id_check(global | binary()) -> 'allow' | 'db' | 'deny'. oauth_client_id_check(Host) -> ejabberd_config:get_option({oauth_client_id_check, Host}). -spec oauth_db_type() -> atom(). oauth_db_type() -> ejabberd_config:get_option({oauth_db_type, global}). -spec oauth_expire() -> pos_integer(). oauth_expire() -> ejabberd_config:get_option({oauth_expire, global}). -spec oauth_use_cache() -> boolean(). oauth_use_cache() -> ejabberd_config:get_option({oauth_use_cache, global}). -spec oom_killer() -> boolean(). oom_killer() -> ejabberd_config:get_option({oom_killer, global}). -spec oom_queue() -> pos_integer(). oom_queue() -> ejabberd_config:get_option({oom_queue, global}). -spec oom_watermark() -> 1..255. oom_watermark() -> ejabberd_config:get_option({oom_watermark, global}). -spec outgoing_s2s_families() -> ['inet' | 'inet6',...]. outgoing_s2s_families() -> outgoing_s2s_families(global). -spec outgoing_s2s_families(global | binary()) -> ['inet' | 'inet6',...]. outgoing_s2s_families(Host) -> ejabberd_config:get_option({outgoing_s2s_families, Host}). -spec outgoing_s2s_ipv4_address() -> 'undefined' | inet:ip4_address(). outgoing_s2s_ipv4_address() -> outgoing_s2s_ipv4_address(global). -spec outgoing_s2s_ipv4_address(global | binary()) -> 'undefined' | inet:ip4_address(). outgoing_s2s_ipv4_address(Host) -> ejabberd_config:get_option({outgoing_s2s_ipv4_address, Host}). -spec outgoing_s2s_ipv6_address() -> 'undefined' | inet:ip6_address(). outgoing_s2s_ipv6_address() -> outgoing_s2s_ipv6_address(global). -spec outgoing_s2s_ipv6_address(global | binary()) -> 'undefined' | inet:ip6_address(). outgoing_s2s_ipv6_address(Host) -> ejabberd_config:get_option({outgoing_s2s_ipv6_address, Host}). -spec outgoing_s2s_port() -> 1..1114111. outgoing_s2s_port() -> outgoing_s2s_port(global). -spec outgoing_s2s_port(global | binary()) -> 1..1114111. outgoing_s2s_port(Host) -> ejabberd_config:get_option({outgoing_s2s_port, Host}). -spec outgoing_s2s_timeout() -> 'infinity' | pos_integer(). outgoing_s2s_timeout() -> outgoing_s2s_timeout(global). -spec outgoing_s2s_timeout(global | binary()) -> 'infinity' | pos_integer(). outgoing_s2s_timeout(Host) -> ejabberd_config:get_option({outgoing_s2s_timeout, Host}). -spec pam_service() -> binary(). pam_service() -> pam_service(global). -spec pam_service(global | binary()) -> binary(). pam_service(Host) -> ejabberd_config:get_option({pam_service, Host}). -spec pam_userinfotype() -> 'jid' | 'username'. pam_userinfotype() -> pam_userinfotype(global). -spec pam_userinfotype(global | binary()) -> 'jid' | 'username'. pam_userinfotype(Host) -> ejabberd_config:get_option({pam_userinfotype, Host}). -spec pgsql_users_number_estimate() -> boolean(). pgsql_users_number_estimate() -> pgsql_users_number_estimate(global). -spec pgsql_users_number_estimate(global | binary()) -> boolean(). pgsql_users_number_estimate(Host) -> ejabberd_config:get_option({pgsql_users_number_estimate, Host}). -spec queue_dir() -> 'undefined' | binary(). queue_dir() -> ejabberd_config:get_option({queue_dir, global}). -spec queue_type() -> 'file' | 'ram'. queue_type() -> queue_type(global). -spec queue_type(global | binary()) -> 'file' | 'ram'. queue_type(Host) -> ejabberd_config:get_option({queue_type, Host}). -spec redis_connect_timeout() -> pos_integer(). redis_connect_timeout() -> ejabberd_config:get_option({redis_connect_timeout, global}). -spec redis_db() -> non_neg_integer(). redis_db() -> ejabberd_config:get_option({redis_db, global}). -spec redis_password() -> string(). redis_password() -> ejabberd_config:get_option({redis_password, global}). -spec redis_pool_size() -> pos_integer(). redis_pool_size() -> ejabberd_config:get_option({redis_pool_size, global}). -spec redis_port() -> 1..1114111. redis_port() -> ejabberd_config:get_option({redis_port, global}). -spec redis_queue_type() -> 'file' | 'ram'. redis_queue_type() -> ejabberd_config:get_option({redis_queue_type, global}). -spec redis_server() -> string(). redis_server() -> ejabberd_config:get_option({redis_server, global}). -spec registration_timeout() -> 'infinity' | pos_integer(). registration_timeout() -> ejabberd_config:get_option({registration_timeout, global}). -spec resource_conflict() -> 'acceptnew' | 'closenew' | 'closeold' | 'setresource'. resource_conflict() -> resource_conflict(global). -spec resource_conflict(global | binary()) -> 'acceptnew' | 'closenew' | 'closeold' | 'setresource'. resource_conflict(Host) -> ejabberd_config:get_option({resource_conflict, Host}). -spec router_cache_life_time() -> 'infinity' | pos_integer(). router_cache_life_time() -> ejabberd_config:get_option({router_cache_life_time, global}). -spec router_cache_missed() -> boolean(). router_cache_missed() -> ejabberd_config:get_option({router_cache_missed, global}). -spec router_cache_size() -> 'infinity' | pos_integer(). router_cache_size() -> ejabberd_config:get_option({router_cache_size, global}). -spec router_db_type() -> atom(). router_db_type() -> ejabberd_config:get_option({router_db_type, global}). -spec router_use_cache() -> boolean(). router_use_cache() -> ejabberd_config:get_option({router_use_cache, global}). -spec rpc_timeout() -> pos_integer(). rpc_timeout() -> ejabberd_config:get_option({rpc_timeout, global}). -spec s2s_access() -> 'all' | acl:acl(). s2s_access() -> s2s_access(global). -spec s2s_access(global | binary()) -> 'all' | acl:acl(). s2s_access(Host) -> ejabberd_config:get_option({s2s_access, Host}). -spec s2s_cafile() -> 'undefined' | binary(). s2s_cafile() -> s2s_cafile(global). -spec s2s_cafile(global | binary()) -> 'undefined' | binary(). s2s_cafile(Host) -> ejabberd_config:get_option({s2s_cafile, Host}). -spec s2s_ciphers() -> 'undefined' | binary(). s2s_ciphers() -> s2s_ciphers(global). -spec s2s_ciphers(global | binary()) -> 'undefined' | binary(). s2s_ciphers(Host) -> ejabberd_config:get_option({s2s_ciphers, Host}). -spec s2s_dhfile() -> 'undefined' | binary(). s2s_dhfile() -> s2s_dhfile(global). -spec s2s_dhfile(global | binary()) -> 'undefined' | binary(). s2s_dhfile(Host) -> ejabberd_config:get_option({s2s_dhfile, Host}). -spec s2s_dns_retries() -> non_neg_integer(). s2s_dns_retries() -> s2s_dns_retries(global). -spec s2s_dns_retries(global | binary()) -> non_neg_integer(). s2s_dns_retries(Host) -> ejabberd_config:get_option({s2s_dns_retries, Host}). -spec s2s_dns_timeout() -> 'infinity' | pos_integer(). s2s_dns_timeout() -> s2s_dns_timeout(global). -spec s2s_dns_timeout(global | binary()) -> 'infinity' | pos_integer(). s2s_dns_timeout(Host) -> ejabberd_config:get_option({s2s_dns_timeout, Host}). -spec s2s_max_retry_delay() -> pos_integer(). s2s_max_retry_delay() -> ejabberd_config:get_option({s2s_max_retry_delay, global}). -spec s2s_protocol_options() -> 'undefined' | binary(). s2s_protocol_options() -> s2s_protocol_options(global). -spec s2s_protocol_options(global | binary()) -> 'undefined' | binary(). s2s_protocol_options(Host) -> ejabberd_config:get_option({s2s_protocol_options, Host}). -spec s2s_queue_type() -> 'file' | 'ram'. s2s_queue_type() -> s2s_queue_type(global). -spec s2s_queue_type(global | binary()) -> 'file' | 'ram'. s2s_queue_type(Host) -> ejabberd_config:get_option({s2s_queue_type, Host}). -spec s2s_timeout() -> 'infinity' | pos_integer(). s2s_timeout() -> s2s_timeout(global). -spec s2s_timeout(global | binary()) -> 'infinity' | pos_integer(). s2s_timeout(Host) -> ejabberd_config:get_option({s2s_timeout, Host}). -spec s2s_tls_compression() -> 'false' | 'true' | 'undefined'. s2s_tls_compression() -> s2s_tls_compression(global). -spec s2s_tls_compression(global | binary()) -> 'false' | 'true' | 'undefined'. s2s_tls_compression(Host) -> ejabberd_config:get_option({s2s_tls_compression, Host}). -spec s2s_use_starttls() -> 'false' | 'optional' | 'required' | 'true'. s2s_use_starttls() -> s2s_use_starttls(global). -spec s2s_use_starttls(global | binary()) -> 'false' | 'optional' | 'required' | 'true'. s2s_use_starttls(Host) -> ejabberd_config:get_option({s2s_use_starttls, Host}). -spec s2s_zlib() -> boolean(). s2s_zlib() -> s2s_zlib(global). -spec s2s_zlib(global | binary()) -> boolean(). s2s_zlib(Host) -> ejabberd_config:get_option({s2s_zlib, Host}). -spec shaper() -> #{atom()=>ejabberd_shaper:shaper_rate()}. shaper() -> ejabberd_config:get_option({shaper, global}). -spec shaper_rules() -> [{atom(),[ejabberd_shaper:shaper_rule()]}]. shaper_rules() -> shaper_rules(global). -spec shaper_rules(global | binary()) -> [{atom(),[ejabberd_shaper:shaper_rule()]}]. shaper_rules(Host) -> ejabberd_config:get_option({shaper_rules, Host}). -spec sm_cache_life_time() -> 'infinity' | pos_integer(). sm_cache_life_time() -> ejabberd_config:get_option({sm_cache_life_time, global}). -spec sm_cache_missed() -> boolean(). sm_cache_missed() -> ejabberd_config:get_option({sm_cache_missed, global}). -spec sm_cache_size() -> 'infinity' | pos_integer(). sm_cache_size() -> ejabberd_config:get_option({sm_cache_size, global}). -spec sm_db_type() -> atom(). sm_db_type() -> sm_db_type(global). -spec sm_db_type(global | binary()) -> atom(). sm_db_type(Host) -> ejabberd_config:get_option({sm_db_type, Host}). -spec sm_use_cache() -> boolean(). sm_use_cache() -> sm_use_cache(global). -spec sm_use_cache(global | binary()) -> boolean(). sm_use_cache(Host) -> ejabberd_config:get_option({sm_use_cache, Host}). -spec sql_connect_timeout() -> pos_integer(). sql_connect_timeout() -> sql_connect_timeout(global). -spec sql_connect_timeout(global | binary()) -> pos_integer(). sql_connect_timeout(Host) -> ejabberd_config:get_option({sql_connect_timeout, Host}). -spec sql_database() -> 'undefined' | binary(). sql_database() -> sql_database(global). -spec sql_database(global | binary()) -> 'undefined' | binary(). sql_database(Host) -> ejabberd_config:get_option({sql_database, Host}). -spec sql_keepalive_interval() -> 'undefined' | pos_integer(). sql_keepalive_interval() -> sql_keepalive_interval(global). -spec sql_keepalive_interval(global | binary()) -> 'undefined' | pos_integer(). sql_keepalive_interval(Host) -> ejabberd_config:get_option({sql_keepalive_interval, Host}). -spec sql_odbc_driver() -> binary(). sql_odbc_driver() -> sql_odbc_driver(global). -spec sql_odbc_driver(global | binary()) -> binary(). sql_odbc_driver(Host) -> ejabberd_config:get_option({sql_odbc_driver, Host}). -spec sql_password() -> binary(). sql_password() -> sql_password(global). -spec sql_password(global | binary()) -> binary(). sql_password(Host) -> ejabberd_config:get_option({sql_password, Host}). -spec sql_pool_size() -> pos_integer(). sql_pool_size() -> sql_pool_size(global). -spec sql_pool_size(global | binary()) -> pos_integer(). sql_pool_size(Host) -> ejabberd_config:get_option({sql_pool_size, Host}). -spec sql_port() -> 1..1114111. sql_port() -> sql_port(global). -spec sql_port(global | binary()) -> 1..1114111. sql_port(Host) -> ejabberd_config:get_option({sql_port, Host}). -spec sql_prepared_statements() -> boolean(). sql_prepared_statements() -> sql_prepared_statements(global). -spec sql_prepared_statements(global | binary()) -> boolean(). sql_prepared_statements(Host) -> ejabberd_config:get_option({sql_prepared_statements, Host}). -spec sql_query_timeout() -> pos_integer(). sql_query_timeout() -> sql_query_timeout(global). -spec sql_query_timeout(global | binary()) -> pos_integer(). sql_query_timeout(Host) -> ejabberd_config:get_option({sql_query_timeout, Host}). -spec sql_queue_type() -> 'file' | 'ram'. sql_queue_type() -> sql_queue_type(global). -spec sql_queue_type(global | binary()) -> 'file' | 'ram'. sql_queue_type(Host) -> ejabberd_config:get_option({sql_queue_type, Host}). -spec sql_server() -> binary(). sql_server() -> sql_server(global). -spec sql_server(global | binary()) -> binary(). sql_server(Host) -> ejabberd_config:get_option({sql_server, Host}). -spec sql_ssl() -> boolean(). sql_ssl() -> sql_ssl(global). -spec sql_ssl(global | binary()) -> boolean(). sql_ssl(Host) -> ejabberd_config:get_option({sql_ssl, Host}). -spec sql_ssl_cafile() -> 'undefined' | binary(). sql_ssl_cafile() -> sql_ssl_cafile(global). -spec sql_ssl_cafile(global | binary()) -> 'undefined' | binary(). sql_ssl_cafile(Host) -> ejabberd_config:get_option({sql_ssl_cafile, Host}). -spec sql_ssl_certfile() -> 'undefined' | binary(). sql_ssl_certfile() -> sql_ssl_certfile(global). -spec sql_ssl_certfile(global | binary()) -> 'undefined' | binary(). sql_ssl_certfile(Host) -> ejabberd_config:get_option({sql_ssl_certfile, Host}). -spec sql_ssl_verify() -> boolean(). sql_ssl_verify() -> sql_ssl_verify(global). -spec sql_ssl_verify(global | binary()) -> boolean(). sql_ssl_verify(Host) -> ejabberd_config:get_option({sql_ssl_verify, Host}). -spec sql_start_interval() -> pos_integer(). sql_start_interval() -> sql_start_interval(global). -spec sql_start_interval(global | binary()) -> pos_integer(). sql_start_interval(Host) -> ejabberd_config:get_option({sql_start_interval, Host}). -spec sql_type() -> 'mssql' | 'mysql' | 'odbc' | 'pgsql' | 'sqlite'. sql_type() -> sql_type(global). -spec sql_type(global | binary()) -> 'mssql' | 'mysql' | 'odbc' | 'pgsql' | 'sqlite'. sql_type(Host) -> ejabberd_config:get_option({sql_type, Host}). -spec sql_username() -> binary(). sql_username() -> sql_username(global). -spec sql_username(global | binary()) -> binary(). sql_username(Host) -> ejabberd_config:get_option({sql_username, Host}). -spec trusted_proxies() -> 'all' | [{inet:ip4_address() | inet:ip6_address(),byte()}]. trusted_proxies() -> ejabberd_config:get_option({trusted_proxies, global}). -spec use_cache() -> boolean(). use_cache() -> use_cache(global). -spec use_cache(global | binary()) -> boolean(). use_cache(Host) -> ejabberd_config:get_option({use_cache, Host}). -spec validate_stream() -> boolean(). validate_stream() -> ejabberd_config:get_option({validate_stream, global}). -spec version() -> binary(). version() -> ejabberd_config:get_option({version, global}). -spec websocket_origin() -> [binary()]. websocket_origin() -> ejabberd_config:get_option({websocket_origin, global}). -spec websocket_ping_interval() -> pos_integer(). websocket_ping_interval() -> ejabberd_config:get_option({websocket_ping_interval, global}). -spec websocket_timeout() -> pos_integer(). websocket_timeout() -> ejabberd_config:get_option({websocket_timeout, global}). ejabberd-21.12/src/mod_service_log.erl0000644000232200023220000000707714154362354020300 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_service_log.erl %%% Author : Alexey Shchepin %%% Purpose : Copy user messages to logger service %%% Created : 24 Aug 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_service_log). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, log_user_send/1, mod_options/1, log_user_receive/1, mod_opt_type/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). start(Host, _Opts) -> ejabberd_hooks:add(user_send_packet, Host, ?MODULE, log_user_send, 50), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, log_user_receive, 50), ok. stop(Host) -> ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, log_user_send, 50), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, log_user_receive, 50), ok. depends(_Host, _Opts) -> []. -spec log_user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. log_user_send({Packet, C2SState}) -> From = xmpp:get_from(Packet), log_packet(Packet, From#jid.lserver), {Packet, C2SState}. -spec log_user_receive({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. log_user_receive({Packet, C2SState}) -> To = xmpp:get_to(Packet), log_packet(Packet, To#jid.lserver), {Packet, C2SState}. -spec log_packet(stanza(), binary()) -> ok. log_packet(Packet, Host) -> Loggers = mod_service_log_opt:loggers(Host), ForwardedMsg = #message{from = jid:make(Host), id = p1_rand:get_string(), sub_els = [#forwarded{ sub_els = [Packet]}]}, lists:foreach( fun(Logger) -> ejabberd_router:route(xmpp:set_to(ForwardedMsg, jid:make(Logger))) end, Loggers). mod_opt_type(loggers) -> econf:list(econf:domain()). mod_options(_) -> [{loggers, []}]. mod_doc() -> #{desc => ?T("This module forwards copies of all stanzas " "to remote XMPP servers or components. " "Every stanza is encapsulated into " "element as described in " "https://xmpp.org/extensions/xep-0297.html" "[XEP-0297: Stanza Forwarding]."), opts => [{loggers, #{value => "[Domain, ...]", desc => ?T("A list of servers or connected components " "to which stanzas will be forwarded.")}}], example => ["modules:", " ...", " mod_service_log:", " loggers:", " - xmpp-server.tld", " - component.domain.tld", " ..."]}. ejabberd-21.12/src/mod_vcard_opt.erl0000644000232200023220000000567014154362354017755 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_vcard_opt). -export([allow_return_all/1]). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([host/1]). -export([hosts/1]). -export([matches/1]). -export([name/1]). -export([search/1]). -export([use_cache/1]). -export([vcard/1]). -spec allow_return_all(gen_mod:opts() | global | binary()) -> boolean(). allow_return_all(Opts) when is_map(Opts) -> gen_mod:get_opt(allow_return_all, Opts); allow_return_all(Host) -> gen_mod:get_module_opt(Host, mod_vcard, allow_return_all). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_vcard, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_vcard, db_type). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_vcard, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_vcard, hosts). -spec matches(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). matches(Opts) when is_map(Opts) -> gen_mod:get_opt(matches, Opts); matches(Host) -> gen_mod:get_module_opt(Host, mod_vcard, matches). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_vcard, name). -spec search(gen_mod:opts() | global | binary()) -> boolean(). search(Opts) when is_map(Opts) -> gen_mod:get_opt(search, Opts); search(Host) -> gen_mod:get_module_opt(Host, mod_vcard, search). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_vcard, use_cache). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_vcard, vcard). ejabberd-21.12/src/ejabberd_ctl.erl0000644000232200023220000007677314154362354017551 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_ctl.erl %%% Author : Alexey Shchepin %%% Purpose : ejabberd command line admin tool %%% Created : 11 Jan 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% Does not support commands that have arguments with ctypes: list, tuple -module(ejabberd_ctl). -behaviour(gen_server). -author('alexey@process-one.net'). -export([start/0, start_link/0, process/1, process2/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ejabberd_ctl.hrl"). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -define(DEFAULT_VERSION, 1000000). -record(state, {}). %%----------------------------- %% Module %%----------------------------- start() -> disable_logging(), [SNode, Timeout, Args] = case init:get_plain_arguments() of [SNode2, "--no-timeout" | Args2] -> [SNode2, infinity, Args2]; [SNode3 | Args3] -> [SNode3, 60000, Args3]; _ -> print_usage(?DEFAULT_VERSION), halt(?STATUS_USAGE) end, SNode1 = case string:tokens(SNode, "@") of [_Node, _Server] -> SNode; _ -> case net_kernel:longnames() of true -> lists:flatten([SNode, "@", inet_db:gethostname(), ".", inet_db:res_option(domain)]); false -> lists:flatten([SNode, "@", inet_db:gethostname()]); _ -> SNode end end, Node = list_to_atom(SNode1), Status = case ejabberd_cluster:call(Node, ?MODULE, process, [Args], Timeout) of {badrpc, Reason} -> print("Failed RPC connection to the node ~p: ~p~n", [Node, Reason]), %% TODO: show minimal start help ?STATUS_BADRPC; {invalid_version, V} -> print("Invalid API version number: ~p~n", [V]), ?STATUS_ERROR; S -> S end, halt(Status). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%----------------------------- %% Process %%----------------------------- -spec process([string()]) -> non_neg_integer(). process(Args) -> process(Args, ?DEFAULT_VERSION). -spec process([string()], non_neg_integer()) -> non_neg_integer(). %% The commands status, stop and restart are defined here to ensure %% they are usable even if ejabberd is completely stopped. process(["status"], _Version) -> {InternalStatus, ProvidedStatus} = init:get_status(), print("The node ~p is ~p with status: ~p~n", [node(), InternalStatus, ProvidedStatus]), case lists:keymember(ejabberd, 1, application:which_applications()) of false -> EjabberdLogPath = ejabberd_logger:get_log_path(), print("ejabberd is not running in that node~n" "Check for error messages: ~ts~n" "or other files in that directory.~n", [EjabberdLogPath]), ?STATUS_ERROR; true -> print("ejabberd ~ts is running in that node~n", [ejabberd_option:version()]), ?STATUS_SUCCESS end; process(["stop"], _Version) -> %%ejabberd_cover:stop(), init:stop(), ?STATUS_SUCCESS; process(["restart"], _Version) -> init:restart(), ?STATUS_SUCCESS; %% TODO: Mnesia operations should not be hardcoded in ejabberd_ctl module. %% For now, I leave them there to avoid breaking those commands for people that %% may be using it (as format of response is going to change). process(["mnesia"], _Version) -> print("~p~n", [mnesia:system_info(all)]), ?STATUS_SUCCESS; process(["mnesia", "info"], _Version) -> mnesia:info(), ?STATUS_SUCCESS; process(["mnesia", Arg], _Version) -> case catch mnesia:system_info(list_to_atom(Arg)) of {'EXIT', Error} -> print("Error: ~p~n", [Error]); Return -> print("~p~n", [Return]) end, ?STATUS_SUCCESS; %% The arguments --long and --dual are not documented because they are %% automatically selected depending in the number of columns of the shell process(["help" | Mode], Version) -> {MaxC, ShCode} = get_shell_info(), case Mode of [] -> print_usage_help(MaxC, ShCode), ?STATUS_SUCCESS; ["--dual"] -> print_usage(dual, MaxC, ShCode, Version), ?STATUS_USAGE; ["--long"] -> print_usage(long, MaxC, ShCode, Version), ?STATUS_USAGE; ["tags"] -> print_usage_tags(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["--tags"] -> % deprecated in favor of "tags" print_usage_tags(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["commands"] -> print_usage_tags_long(MaxC, ShCode, Version), ?STATUS_SUCCESS; ["--tags", Tag] -> % deprecated in favor of simply "Tag" print_usage_tags(Tag, MaxC, ShCode, Version), ?STATUS_SUCCESS; [String | _] -> case determine_string_type(String, Version) of no_idea -> io:format("No tag or command matches '~ts'~n", [String]); both -> print_usage_tags(String, MaxC, ShCode, Version), print_usage_commands2(String, MaxC, ShCode, Version); tag -> print_usage_tags(String, MaxC, ShCode, Version); command -> print_usage_commands2(String, MaxC, ShCode, Version) end, ?STATUS_SUCCESS end; process(["--version", Arg | Args], _) -> Version = try list_to_integer(Arg) catch _:_ -> throw({invalid_version, Arg}) end, process(Args, Version); process(Args, Version) -> {String, Code} = process2(Args, [], Version), case String of [] -> ok; _ -> io:format("~ts~n", [String]) end, Code. %% @spec (Args::[string()], AccessCommands) -> {String::string(), Code::integer()} process2(Args, AccessCommands) -> process2(Args, AccessCommands, ?DEFAULT_VERSION). %% @spec (Args::[string()], AccessCommands, Version) -> {String::string(), Code::integer()} process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) -> process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server), list_to_binary(Pass), true}, Version); process2(Args, AccessCommands, Version) -> process2(Args, AccessCommands, noauth, Version). process2(Args, AccessCommands, Auth, Version) -> case try_run_ctp(Args, Auth, AccessCommands, Version) of {String, wrong_command_arguments} when is_list(String) -> io:format(lists:flatten(["\n" | String]++["\n"])), [CommandString | _] = Args, process(["help" | [CommandString]], Version), {lists:flatten(String), ?STATUS_ERROR}; {String, Code} when is_list(String) and is_integer(Code) -> {lists:flatten(String), Code}; String when is_list(String) -> {lists:flatten(String), ?STATUS_SUCCESS}; Code when is_integer(Code) -> {"", Code}; Other -> {"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR} end. determine_string_type(String, Version) -> TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(String, 1, TagsCommands) of {value, {String, CNs}} -> CNs; false -> [] end, AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], Cmds = filter_commands(AllCommandsNames, String), case {CommandsNames, Cmds} of {[], []} -> no_idea; {[], _} -> command; {_, []} -> tag; {_, _} -> both end. %%----------------------------- %% Command calling %%----------------------------- %% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} try_run_ctp(Args, Auth, AccessCommands, Version) -> try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of false when Args /= [] -> try_call_command(Args, Auth, AccessCommands, Version); false -> print_usage(Version), {"", ?STATUS_USAGE}; Status -> {"", Status} catch exit:Why -> print_usage(Version), {io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE}; Error:Why -> %% In this case probably ejabberd is not started, so let's show Status process(["status"], Version), print("~n", []), {io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE} end. %% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} try_call_command(Args, Auth, AccessCommands, Version) -> try call_command(Args, Auth, AccessCommands, Version) of {Reason, wrong_command_arguments} -> {Reason, ?STATUS_ERROR}; Res -> Res catch throw:{error, unknown_command} -> KnownCommands = [Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version)], UnknownCommand = list_to_atom(hd(Args)), {io_lib:format( "Error: unknown command '~ts'. Did you mean '~ts'?", [hd(Args), misc:best_match(UnknownCommand, KnownCommands)]), ?STATUS_ERROR}; throw:Error -> {io_lib:format("~p", [Error]), ?STATUS_ERROR}; ?EX_RULE(A, Why, Stack) -> StackTrace = ?EX_STACK(Stack), {io_lib:format("Unhandled exception occurred executing the command:~n** ~ts", [misc:format_exception(2, A, Why, StackTrace)]), ?STATUS_ERROR} end. %% @spec (Args::[string()], Auth, AccessCommands, Version) -> string() | integer() | {string(), integer()} | {error, ErrorType} call_command([CmdString | Args], Auth, _AccessCommands, Version) -> CmdStringU = ejabberd_regexp:greplace( list_to_binary(CmdString), <<"-">>, <<"_">>), Command = list_to_atom(binary_to_list(CmdStringU)), {ArgsFormat, _, ResultFormat} = ejabberd_commands:get_command_format(Command, Auth, Version), case (catch format_args(Args, ArgsFormat)) of ArgsFormatted when is_list(ArgsFormatted) -> CI = case Auth of {U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S}; _ -> #{} end, CI2 = CI#{caller_module => ?MODULE}, Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, CI2, Version), format_result(Result, ResultFormat); {'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} -> {NumCompa, TextCompa} = case {length(A1), length(A2)} of {L1, L2} when L1 < L2 -> {L2-L1, "less argument"}; {L1, L2} when L1 > L2 -> {L1-L2, "more argument"} end, process(["help" | [CmdString]]), {io_lib:format("Error: the command '~ts' requires ~p ~ts.", [CmdString, NumCompa, TextCompa]), wrong_command_arguments} end. %%----------------------------- %% Format arguments %%----------------------------- format_args(Args, ArgsFormat) -> lists:foldl( fun({{_ArgName, ArgFormat}, Arg}, Res) -> Formatted = format_arg(Arg, ArgFormat), Res ++ [Formatted] end, [], lists:zip(ArgsFormat, Args)). format_arg(Arg, integer) -> format_arg2(Arg, "~d"); format_arg(Arg, binary) -> unicode:characters_to_binary(Arg, utf8); format_arg("", string) -> ""; format_arg(Arg, string) -> NumChars = integer_to_list(length(Arg)), Parse = "~" ++ NumChars ++ "c", format_arg2(Arg, Parse). format_arg2(Arg, Parse)-> {ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg), Arg2. %%----------------------------- %% Format result %%----------------------------- format_result({error, ErrorAtom}, _) -> {io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)}; %% An error should always be allowed to return extended error to help with API. %% Extended error is of the form: %% {error, type :: atom(), code :: int(), Desc :: string()} format_result({error, ErrorAtom, Code, Msg}, _) -> {io_lib:format("Error: ~p: ~s", [ErrorAtom, Msg]), make_status(Code)}; format_result(Atom, {_Name, atom}) -> io_lib:format("~p", [Atom]); format_result(Int, {_Name, integer}) -> io_lib:format("~p", [Int]); format_result([A|_]=String, {_Name, string}) when is_list(String) and is_integer(A) -> io_lib:format("~ts", [String]); format_result(Binary, {_Name, string}) when is_binary(Binary) -> io_lib:format("~ts", [binary_to_list(Binary)]); format_result(Atom, {_Name, string}) when is_atom(Atom) -> io_lib:format("~ts", [atom_to_list(Atom)]); format_result(Integer, {_Name, string}) when is_integer(Integer) -> io_lib:format("~ts", [integer_to_list(Integer)]); format_result(Other, {_Name, string}) -> io_lib:format("~p", [Other]); format_result(Code, {_Name, rescode}) -> make_status(Code); format_result({Code, Text}, {_Name, restuple}) -> {io_lib:format("~ts", [Text]), make_status(Code)}; %% The result is a list of something: [something()] format_result([], {_Name, {list, _ElementsDef}}) -> ""; format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) -> %% Start formatting the first element [format_result(FirstElement, ElementsDef) | %% If there are more elements, put always first a newline character lists:map( fun(Element) -> ["\n" | format_result(Element, ElementsDef)] end, Elements)]; %% The result is a tuple with several elements: {something1(), something2(),...} %% NOTE: the elements in the tuple are separated with tabular characters, %% if a string is empty, it will be difficult to notice in the shell, %% maybe a different separation character should be used, like ;;? format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) -> ElementsList = tuple_to_list(ElementsTuple), [{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef), [format_result(FirstE, FirstD) | lists:map( fun({Element, ElementDef}) -> ["\t" | format_result(Element, ElementDef)] end, ElementsAndDef)]; format_result(404, {_Name, _}) -> make_status(not_found). make_status(ok) -> ?STATUS_SUCCESS; make_status(true) -> ?STATUS_SUCCESS; make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR; make_status(Code) when is_integer(Code), Code > 0 -> Code; make_status(Error) -> io:format("Error: ~p~n", [Error]), ?STATUS_ERROR. get_list_commands(Version) -> try ejabberd_commands:list_commands(Version) of Commands -> [tuple_command_help(Command) || {N,_,_}=Command <- Commands, %% Don't show again those commands, because they are already %% announced by ejabberd_ctl itself N /= status, N /= stop, N /= restart] catch exit:_ -> [] end. %% Return: {string(), [string()], string()} tuple_command_help({Name, _Args, Desc}) -> {Args, _, _} = ejabberd_commands:get_command_format(Name, admin), Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args], Prepend = case is_supported_args(Args) of true -> ""; false -> "*" end, CallString = atom_to_list(Name), {CallString, Arguments, Prepend ++ Desc}. is_supported_args(Args) -> lists:all( fun({_Name, Format}) -> (Format == integer) or (Format == string) or (Format == binary) end, Args). %%----------------------------- %% Print help %%----------------------------- %% Commands are Bold -define(B1, "\e[1m"). -define(B2, "\e[21m"). -define(C(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end). %% Arguments are Dim -define(D1, "\e[2m"). -define(D2, "\e[22m"). -define(A(S), case ShCode of true -> [?D1, S, ?D2]; false -> S end). %% Tags are Underline -define(U1, "\e[4m"). -define(U2, "\e[24m"). -define(G(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end). %% B are Nothing -define(N1, "\e[0m"). -define(N2, "\e[0m"). -define(B(S), case ShCode of true -> [?N1, S, ?N2]; false -> S end). print_usage(Version) -> {MaxC, ShCode} = get_shell_info(), print_usage(dual, MaxC, ShCode, Version). print_usage(HelpMode, MaxC, ShCode, Version) -> AllCommands = [ {"help", ["[arguments]"], "Get help"}, {"status", [], "Get ejabberd status"}, {"stop", [], "Stop ejabberd"}, {"restart", [], "Restart ejabberd"}, {"mnesia", ["[info]"], "show information of Mnesia system"}] ++ get_list_commands(Version), print( ["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("nodename"), "] [--version ", ?A("api_version"), "] ", ?C("command"), " [", ?A("arguments"), "]\n" "\n" "Available commands in this ejabberd node:\n"], []), print_usage_commands(HelpMode, MaxC, ShCode, AllCommands). print_usage_commands(HelpMode, MaxC, ShCode, Commands) -> CmdDescsSorted = lists:keysort(1, Commands), %% What is the length of the largest command? {CmdArgsLenDescsSorted, Lens} = lists:mapfoldl( fun({Cmd, Args, Desc}, Lengths) -> Len = length(Cmd) + lists:foldl(fun(Arg, R) -> R + 1 + length(Arg) end, 0, Args), {{Cmd, Args, Len, Desc}, [Len | Lengths]} end, [], CmdDescsSorted), MaxCmdLen = case Lens of [] -> 80; _ -> lists:max(Lens) end, %% For each command in the list of commands %% Convert its definition to a line FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode), print([FmtCmdDescs], []). %% Get some info about the shell: %% how many columns of width %% and guess if it supports text formatting codes. get_shell_info() -> %% This function was introduced in OTP R12B-0 try io:columns() of {ok, C} -> {C-2, true}; {error, enotsup} -> {78, false} catch _:_ -> {78, false} end. %% Erlang/OTP 20.0 introduced string:find/2, but we must support old 19.3 string_find([], _SearchPattern) -> nomatch; string_find([A | String], [A]) -> String; string_find([_ | String], SearchPattern) -> string_find(String, SearchPattern). %% Split this command description in several lines of proper length prepare_description(DescInit, MaxC, Desc) -> case string_find(Desc, "\n") of nomatch -> prepare_description2(DescInit, MaxC, Desc); _ -> Desc end. prepare_description2(DescInit, MaxC, Desc) -> Words = string:tokens(Desc, " "), prepare_long_line(DescInit, MaxC, Words). prepare_long_line(DescInit, MaxC, Words) -> MaxSegmentLen = MaxC - DescInit, MarginString = lists:duplicate(DescInit, $\s), % Put spaces [FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words), MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments), [FirstSegment | MoreSegmentsMixed]. mix_desc_segments(MarginString, Segments) -> [["\n", MarginString, Segment] || Segment <- Segments]. split_desc_segments(MaxL, Words) -> join(MaxL, Words). %% Join words in a segment, %% but stop adding to a segment if adding this word would pass L join(L, Words) -> join(L, Words, 0, [], []). join(_Len, [], _CurSegLen, CurSeg, AllSegs) -> lists:reverse([CurSeg | AllSegs]); join(Len, [Word | Tail], CurSegLen, CurSeg, AllSegs) -> WordLen = length(Word), SegSize = WordLen + CurSegLen + 1, {NewCurSeg, NewAllSegs, NewCurSegLen} = if SegSize < Len -> {[CurSeg, " ", Word], AllSegs, SegSize}; true -> {Word, [CurSeg | AllSegs], WordLen} end, NewLen = case string:str(Word, "\n") of 0 -> NewCurSegLen; _ -> 0 end, join(Len, Tail, NewLen, NewCurSeg, NewAllSegs). format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) when MaxC - MaxCmdLen < 40 -> %% If the space available for descriptions is too narrow, enforce long help mode format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long); format_command_lines(CALD, _MaxCmdLen, _MaxC, ShCode, short) -> lists:map( fun({Cmd, Args, _CmdArgsL, _Desc}) -> [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n"] end, CALD); format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) -> lists:map( fun({Cmd, Args, CmdArgsL, Desc}) -> DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc), [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], lists:duplicate(MaxCmdLen - CmdArgsL + 1, $\s), DescFmt, "\n"] end, CALD); format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) -> lists:map( fun({Cmd, Args, _CmdArgsL, Desc}) -> DescFmt = prepare_description(13, MaxC, Desc), [" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n", " ", DescFmt, "\n"] end, CALD). %%----------------------------- %% Print Tags %%----------------------------- print_usage_tags(MaxC, ShCode, Version) -> print("Available tags and list of commands:", []), TagsCommands = ejabberd_commands:get_tags_commands(Version), lists:foreach( fun({Tag, Commands} = _TagCommands) -> print(["\n\n ", ?G(Tag), "\n "], []), Words = lists:sort(Commands), Desc = prepare_long_line(5, MaxC, Words), print(?C(Desc), []) end, TagsCommands), print("\n\n", []). print_usage_tags_long(MaxC, ShCode, Version) -> print("Available tags and commands details:", []), TagsCommands = ejabberd_commands:get_tags_commands(Version), print("\n", []), lists:foreach( fun({Tag, CommandsNames} = _TagCommands) -> print(["\n ", ?G(Tag), "\n"], []), CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, tuple_command_help({Name, Args, Desc}) end, CommandsNames), print_usage_commands(short, MaxC, ShCode, CommandsList) end, TagsCommands), print("\n", []). print_usage_tags(Tag, MaxC, ShCode, Version) -> print(["Available commands with tag ", ?G(Tag), ":", "\n", "\n"], []), HelpMode = long, TagsCommands = ejabberd_commands:get_tags_commands(Version), CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of {value, {Tag, CNs}} -> CNs; false -> [] end, CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, tuple_command_help({Name, Args, Desc}) end, CommandsNames), print_usage_commands(HelpMode, MaxC, ShCode, CommandsList), print("\n", []). %%----------------------------- %% Print usage of 'help' command %%----------------------------- print_usage_help(MaxC, ShCode) -> LongDesc = ["This special ", ?C("help"), " command provides help of ejabberd commands.\n\n" "The format is:\n ", ?B("ejabberdctl"), " ", ?C("help"), " [", ?A("tags"), " | ", ?A("commands"), " | ", ?G("tag"), " | ", ?C("command"), " | ", ?C("com?*"), "]\n\n" "The optional arguments:\n" " ",?A("tags")," Show all tags and commands names in each tag\n" " ",?A("commands")," Show all tags and commands details in each tag\n" " ",?G("tag")," Show commands related to this tag\n" " ",?C("command")," Show detailed description of this command\n" " ",?C("com?*")," Show commands that match this glob.\n" " (? will match a simple character, and\n" " * will match several characters)\n" "\n", "Some example usages:\n", " ejabberdctl ", ?C("help"), "\n", " ejabberdctl ", ?C("help"), " ", ?A("tags"), "\n", " ejabberdctl ", ?C("help"), " ", ?A("commands"), "\n", " ejabberdctl ", ?C("help"), " ", ?G("accounts"), "\n", " ejabberdctl ", ?C("help"), " ", ?C("register"), "\n", " ejabberdctl ", ?C("help"), " ", ?C("regist*"), "\n", "\n", "Please note that 'ejabberdctl' shows all ejabberd commands,\n", "even those that cannot be used in the shell with ejabberdctl.\n", "Those commands can be identified because their description starts with: *"], ArgsDef = [], C = #ejabberd_commands{ name = help, desc = "Show help of ejabberd commands", longdesc = lists:flatten(LongDesc), args = ArgsDef, result = {help, string}}, print_usage_command2("help", C, MaxC, ShCode). %%----------------------------- %% Print usage command %%----------------------------- %% @spec (CmdSubString::string(), MaxC::integer(), ShCode::boolean(), Version) -> ok print_usage_commands2(CmdSubString, MaxC, ShCode, Version) -> %% Get which command names match this substring AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)], Cmds = filter_commands(AllCommandsNames, CmdSubString), case Cmds of [] -> io:format("Error: no command found that match '~ts'~n", [CmdSubString]); _ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version) end. print_usage_commands3([Cmd], MaxC, ShCode, Version) -> print_usage_command(Cmd, MaxC, ShCode, Version); print_usage_commands3(Cmds, MaxC, ShCode, Version) -> CommandsList = lists:map( fun(NameString) -> C = ejabberd_commands:get_command_definition( list_to_atom(NameString), Version), #ejabberd_commands{name = Name, args = Args, desc = Desc} = C, tuple_command_help({Name, Args, Desc}) end, Cmds), print_usage_commands(long, MaxC, ShCode, CommandsList), %% que aqui solo muestre un par de lineas ok. filter_commands(All, SubString) -> case lists:member(SubString, All) of true -> [SubString]; false -> filter_commands_regexp(All, SubString) end. filter_commands_regexp(All, Glob) -> RegExp = ejabberd_regexp:sh_to_awk(list_to_binary(Glob)), lists:filter( fun(Command) -> case ejabberd_regexp:run(list_to_binary(Command), RegExp) of match -> true; nomatch -> false end end, All). %% @spec (Cmd::string(), MaxC::integer(), ShCode::boolean(), Version) -> ok print_usage_command(Cmd, MaxC, ShCode, Version) -> Name = list_to_atom(Cmd), C = ejabberd_commands:get_command_definition(Name, Version), print_usage_command2(Cmd, C, MaxC, ShCode). print_usage_command2(Cmd, C, MaxC, ShCode) -> #ejabberd_commands{ tags = TagsAtoms, definer = Definer, desc = Desc, args = ArgsDef, longdesc = LongDesc, result = ResultDef} = C, NameFmt = [" ", ?B("Command Name"), ": ", ?C(Cmd), "\n"], %% Initial indentation of result is 13 = length(" Arguments: ") Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef], ArgsMargin = lists:duplicate(13, $\s), ArgsListFmt = case Args of [] -> "\n"; _ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args] end, ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt], %% Initial indentation of result is 11 = length(" Returns: ") ResultFmt = format_usage_ctype(ResultDef, 11), ReturnsFmt = [" ",?B("Returns"),": ", ResultFmt], XmlrpcFmt = "", %%+++ [" ",?B("XML-RPC"),": ", format_usage_xmlrpc(ArgsDef, ResultDef), "\n\n"], TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])], IsDefinerMod = case Definer of unknown -> true; _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) end, ModuleFmt = case IsDefinerMod of true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"]; false -> [] end, DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)], LongDescFmt = case LongDesc of "" -> ""; _ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"] end, NoteEjabberdctl = case is_supported_args(ArgsDef) of true -> ""; false -> [" ", ?B("Note:"), " This command cannot be executed using ejabberdctl. Try ejabberd_xmlrpc.\n\n"] end, case Cmd of "help" -> ok; _ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt, "\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], []) end, print([LongDescFmt, NoteEjabberdctl], []). format_usage_ctype(Type, _Indentation) when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)-> io_lib:format("~p", [Type]); format_usage_ctype({Name, Type}, _Indentation) when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)-> io_lib:format("~p::~p", [Name, Type]); format_usage_ctype({Name, {list, ElementDef}}, Indentation) -> NameFmt = atom_to_list(Name), Indentation2 = Indentation + length(NameFmt) + 4, ElementFmt = format_usage_ctype(ElementDef, Indentation2), [NameFmt, "::[ ", ElementFmt, " ]"]; format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) -> NameFmt = atom_to_list(Name), Indentation2 = Indentation + length(NameFmt) + 4, ElementsFmt = format_usage_tuple(ElementsDef, Indentation2), [NameFmt, "::{ " | ElementsFmt]. format_usage_tuple([], _Indentation) -> []; format_usage_tuple([ElementDef], Indentation) -> [format_usage_ctype(ElementDef, Indentation) , " }"]; format_usage_tuple([ElementDef | ElementsDef], Indentation) -> ElementFmt = format_usage_ctype(ElementDef, Indentation), MarginString = lists:duplicate(Indentation, $\s), % Put spaces [ElementFmt, ",\n", MarginString, format_usage_tuple(ElementsDef, Indentation)]. print(Format, Args) -> io:format(lists:flatten(Format), Args). -ifdef(LAGER). disable_logging() -> ok. -else. disable_logging() -> logger:set_primary_config(level, none). -endif. ejabberd-21.12/src/ejabberd.erl0000644000232200023220000001426114154362354016667 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd.erl %%% Author : Alexey Shchepin %%% Purpose : ejabberd wrapper: start / stop %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd). -author('alexey@process-one.net'). -compile({no_auto_import, [{halt, 0}]}). -protocol({xep, 4, '2.9'}). -protocol({xep, 86, '1.0'}). -protocol({xep, 106, '1.1'}). -protocol({xep, 170, '1.0'}). -protocol({xep, 205, '1.0'}). -protocol({xep, 212, '1.0'}). -protocol({xep, 216, '1.0'}). -protocol({xep, 243, '1.0'}). -protocol({xep, 270, '1.0'}). -export([start/0, stop/0, halt/0, start_app/1, start_app/2, get_pid_file/0, check_apps/0, module_name/1, is_loaded/0]). -include("logger.hrl"). start() -> application:ensure_all_started(ejabberd). stop() -> application:stop(ejabberd). halt() -> ejabberd_logger:flush(), erlang:halt(1, [{flush, true}]). %% @spec () -> false | string() get_pid_file() -> case os:getenv("EJABBERD_PID_PATH") of false -> false; "" -> false; Path -> Path end. start_app(App) -> start_app(App, temporary). start_app(App, Type) -> StartFlag = not is_loaded(), start_app(App, Type, StartFlag). is_loaded() -> Apps = application:which_applications(), lists:keymember(ejabberd, 1, Apps). start_app(App, Type, StartFlag) when is_atom(App) -> start_app([App], Type, StartFlag); start_app([App|Apps], Type, StartFlag) -> case application:start(App,Type) of ok -> start_app(Apps, Type, StartFlag); {error, {already_started, _}} -> start_app(Apps, Type, StartFlag); {error, {not_started, DepApp}} -> case lists:member(DepApp, [App|Apps]) of true -> Reason = io_lib:format( "Failed to start Erlang application '~ts': " "circular dependency with '~ts' detected", [App, DepApp]), exit_or_halt(Reason, StartFlag); false -> start_app([DepApp,App|Apps], Type, StartFlag) end; {error, Why} -> Reason = io_lib:format( "Failed to start Erlang application '~ts': ~ts. ~ts", [App, format_error(Why), hint()]), exit_or_halt(Reason, StartFlag) end; start_app([], _Type, _StartFlag) -> ok. check_app_modules(App, StartFlag) -> case application:get_key(App, modules) of {ok, Mods} -> lists:foreach( fun(Mod) -> case code:which(Mod) of non_existing -> File = get_module_file(App, Mod), Reason = io_lib:format( "Couldn't find file ~ts needed " "for Erlang application '~ts'. ~ts", [File, App, hint()]), exit_or_halt(Reason, StartFlag); _ -> ok end end, Mods); _ -> %% No modules? This is strange ok end. check_apps() -> spawn( fun() -> Apps = [ejabberd | [App || {App, _, _} <- application:which_applications(), App /= ejabberd]], ?DEBUG("Checking consistency of applications: ~ts", [misc:join_atoms(Apps, <<", ">>)]), misc:peach( fun(App) -> check_app_modules(App, true) end, Apps), ?DEBUG("All applications are intact", []), lists:foreach(fun erlang:garbage_collect/1, processes()) end). -spec exit_or_halt(iodata(), boolean()) -> no_return(). exit_or_halt(Reason, StartFlag) -> ?CRITICAL_MSG(Reason, []), if StartFlag -> %% Wait for the critical message is written in the console/log halt(); true -> erlang:error(application_start_failed) end. get_module_file(App, Mod) -> BaseName = atom_to_list(Mod), case code:lib_dir(App, ebin) of {error, _} -> BaseName; Dir -> filename:join([Dir, BaseName ++ ".beam"]) end. module_name([Dir, _, <> | _] = Mod) when H >= 65, H =< 90 -> Module = str:join([elixir_name(M) || M<-tl(Mod)], <<>>), Prefix = case elixir_name(Dir) of <<"Ejabberd">> -> <<"Elixir.Ejabberd.">>; Lib -> <<"Elixir.Ejabberd.", Lib/binary, ".">> end, misc:binary_to_atom(<>); module_name([<<"ejabberd">> | _] = Mod) -> Module = str:join([erlang_name(M) || M<-Mod], $_), misc:binary_to_atom(Module); module_name(Mod) when is_list(Mod) -> Module = str:join([erlang_name(M) || M<-tl(Mod)], $_), misc:binary_to_atom(Module). elixir_name(Atom) when is_atom(Atom) -> elixir_name(misc:atom_to_binary(Atom)); elixir_name(<>) when H >= 65, H =< 90 -> <>; elixir_name(<>) -> <<(H-32), T/binary>>. erlang_name(Atom) when is_atom(Atom) -> misc:atom_to_binary(Atom); erlang_name(Bin) when is_binary(Bin) -> Bin. format_error({Reason, File}) when is_list(Reason), is_list(File) -> Reason ++ ": " ++ File; format_error(Term) -> io_lib:format("~p", [Term]). hint() -> "This usually means that ejabberd or Erlang " "was compiled/installed incorrectly.". ejabberd-21.12/src/mod_http_api.erl0000644000232200023220000004431414154362354017602 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_http_api.erl %%% Author : Christophe romain %%% Purpose : Implements REST API for ejabberd using JSON data %%% Created : 15 Sep 2014 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_http_api). -author('cromain@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). -define(DEFAULT_API_VERSION, 0). -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). -define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_JSON, {<<"Content-Type">>, <<"application/json">>}). -define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, POST, OPTIONS">>}). -define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type, Authorization, X-Admin">>}). -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER(CType), [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). %% ------------------- %% Module control %% ------------------- start(_Host, _Opts) -> ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. %% ---------- %% basic auth %% ---------- extract_auth(#request{auth = HTTPAuth, ip = {IP, _}, opts = Opts}) -> Info = case HTTPAuth of {SJID, Pass} -> try jid:decode(SJID) of #jid{luser = User, lserver = Server} -> case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> #{usr => {User, Server, <<"">>}, caller_server => Server}; false -> {error, invalid_auth} end catch _:{bad_jid, _} -> {error, invalid_auth} end; {oauth, Token, _} -> case ejabberd_oauth:check_token(Token) of {ok, {U, S}, Scope} -> #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; {false, Reason} -> {error, Reason} end; invalid -> {error, invalid_auth}; _ -> #{} end, case Info of Map when is_map(Map) -> Tag = proplists:get_value(tag, Opts, <<>>), Map#{caller_module => ?MODULE, ip => IP, tag => Tag}; _ -> ?DEBUG("Invalid auth data: ~p", [Info]), Info end. %% ------------------ %% command processing %% ------------------ %process(Call, Request) -> % ?DEBUG("~p~n~p", [Call, Request]), ok; process(_, #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), badrequest_response(<<"Missing POST data">>); process([Call], #request{method = 'POST', data = Data, ip = IPPort} = Req) -> Version = get_api_version(Req), try Args = extract_args(Data), log(Call, Args, IPPort), perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> json_format({404, 44, <<"Command not found.">>}); _:{error,{_,invalid_json}} = _Err -> ?DEBUG("Bad Request: ~p", [_Err]), badrequest_response(<<"Invalid JSON input">>); ?EX_RULE(_Class, _Error, Stack) -> StackTrace = ?EX_STACK(Stack), ?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]), badrequest_response() end; process([Call], #request{method = 'GET', q = Data, ip = {IP, _}} = Req) -> Version = get_api_version(Req), try Args = case Data of [{nokey, <<>>}] -> []; _ -> Data end, log(Call, Args, IP), perform_call(Call, Args, Req, Version) catch %% TODO We need to refactor to remove redundant error return formatting throw:{error, unknown_command} -> json_format({404, 44, <<"Command not found.">>}); ?EX_RULE(_, _Error, Stack) -> StackTrace = ?EX_STACK(Stack), ?DEBUG("Bad Request: ~p ~p", [_Error, StackTrace]), badrequest_response() end; 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.">>). perform_call(Command, Args, Req, Version) -> case catch binary_to_existing_atom(Command, utf8) of Call when is_atom(Call) -> case extract_auth(Req) of {error, expired} -> invalid_token_response(); {error, not_found} -> invalid_token_response(); {error, invalid_auth} -> unauthorized_response(); Auth when is_map(Auth) -> Result = handle(Call, Auth, Args, Version), json_format(Result) end; _ -> json_error(404, 40, <<"Endpoint not found.">>) end. %% Be tolerant to make API more easily usable from command-line pipe. extract_args(<<"\n">>) -> []; extract_args(Data) -> case jiffy:decode(Data) of List when is_list(List) -> List; {List} when is_list(List) -> List; Other -> [Other] end. % get API version N from last "vN" element in URL path get_api_version(#request{path = Path}) -> get_api_version(lists:reverse(Path)); get_api_version([<<"v", String/binary>> | Tail]) -> case catch binary_to_integer(String) of N when is_integer(N) -> N; _ -> get_api_version(Tail) end; get_api_version([_Head | Tail]) -> get_api_version(Tail); get_api_version([]) -> ?DEFAULT_API_VERSION. %% ---------------- %% command handlers %% ---------------- %% TODO Check accept types of request before decided format of reply. % generic ejabberd command handler handle(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> Args2 = [{misc:binary_to_atom(Key), Value} || {Key, Value} <- Args], try handle2(Call, Auth, Args2, Version) catch throw:not_found -> {404, <<"not_found">>}; throw:{not_found, Why} when is_atom(Why) -> {404, misc:atom_to_binary(Why)}; throw:{not_found, Msg} -> {404, iolist_to_binary(Msg)}; throw:not_allowed -> {401, <<"not_allowed">>}; throw:{not_allowed, Why} when is_atom(Why) -> {401, misc:atom_to_binary(Why)}; throw:{not_allowed, Msg} -> {401, iolist_to_binary(Msg)}; throw:{error, account_unprivileged} -> {403, 31, <<"Command need to be run with admin privilege.">>}; throw:{error, access_rules_unauthorized} -> {403, 32, <<"AccessRules: Account does not have the right to perform the operation.">>}; throw:{invalid_parameter, Msg} -> {400, iolist_to_binary(Msg)}; throw:{error, Why} when is_atom(Why) -> {400, misc:atom_to_binary(Why)}; throw:{error, Msg} -> {400, iolist_to_binary(Msg)}; throw:Error when is_atom(Error) -> {400, misc:atom_to_binary(Error)}; throw:Msg when is_list(Msg); is_binary(Msg) -> {400, iolist_to_binary(Msg)}; ?EX_RULE(Class, Error, Stack) -> StackTrace = ?EX_STACK(Stack), ?ERROR_MSG("REST API Error: " "~ts(~p) -> ~p:~p ~p", [Call, hide_sensitive_args(Args), Class, Error, StackTrace]), {500, <<"internal_error">>} end. handle2(Call, Auth, Args, Version) when is_atom(Call), is_list(Args) -> {ArgsF, ArgsR, _ResultF} = ejabberd_commands:get_command_format(Call, Auth, Version), ArgsFormatted = format_args(Call, rename_old_args(Args, ArgsR), ArgsF), case ejabberd_commands:execute_command2(Call, ArgsFormatted, Auth, Version) of {error, Error} -> throw(Error); Res -> format_command_result(Call, Auth, Res, Version) end. rename_old_args(Args, []) -> Args; rename_old_args(Args, [{OldName, NewName} | ArgsR]) -> Args2 = case lists:keytake(OldName, 1, Args) of {value, {OldName, Value}, ArgsTail} -> [{NewName, Value} | ArgsTail]; false -> Args end, rename_old_args(Args2, ArgsR). get_elem_delete(Call, A, L, F) -> case proplists:get_all_values(A, L) of [Value] -> {Value, proplists:delete(A, L)}; [_, _ | _] -> ?INFO_MSG("Command ~ts call rejected, it has duplicate attribute ~w", [Call, A]), throw({invalid_parameter, io_lib:format("Request have duplicate argument: ~w", [A])}); [] -> case F of {list, _} -> {[], L}; _ -> ?INFO_MSG("Command ~ts call rejected, missing attribute ~w", [Call, A]), throw({invalid_parameter, io_lib:format("Request have missing argument: ~w", [A])}) end end. format_args(Call, Args, ArgsFormat) -> {ArgsRemaining, R} = lists:foldl(fun ({ArgName, ArgFormat}, {Args1, Res}) -> {ArgValue, Args2} = get_elem_delete(Call, ArgName, Args1, ArgFormat), Formatted = format_arg(ArgValue, ArgFormat), {Args2, Res ++ [Formatted]} end, {Args, []}, ArgsFormat), case ArgsRemaining of [] -> R; L when is_list(L) -> ExtraArgs = [N || {N, _} <- L], ?INFO_MSG("Command ~ts call rejected, it has unknown arguments ~w", [Call, ExtraArgs]), throw({invalid_parameter, io_lib:format("Request have unknown arguments: ~w", [ExtraArgs])}) 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) -> [format_arg(Element, ElementDefFormat) || Element <- Elements]; format_arg({[{Name, Value}]}, {tuple, [{_Tuple1N, Tuple1S}, {_Tuple2N, Tuple2S}]}) when Tuple1S == binary; Tuple1S == string -> {format_arg(Name, Tuple1S), format_arg(Value, Tuple2S)}; format_arg({Elements}, {tuple, ElementsDef}) when is_list(Elements) -> F = lists:map(fun({TElName, TElDef}) -> case lists:keyfind(atom_to_binary(TElName, latin1), 1, Elements) of {_, Value} -> format_arg(Value, TElDef); _ when TElDef == binary; TElDef == string -> <<"">>; _ -> ?ERROR_MSG("Missing field ~p in tuple ~p", [TElName, Elements]), throw({invalid_parameter, io_lib:format("Missing field ~w in tuple ~w", [TElName, Elements])}) end end, ElementsDef), list_to_tuple(F); format_arg(Elements, {list, ElementsDef}) when is_list(Elements) and is_atom(ElementsDef) -> [format_arg(Element, ElementsDef) || Element <- Elements]; format_arg(Arg, integer) when is_integer(Arg) -> Arg; format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); format_arg(Arg, binary) when is_binary(Arg) -> Arg; format_arg(Arg, string) when is_list(Arg) -> Arg; format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); format_arg(undefined, binary) -> <<>>; format_arg(undefined, string) -> ""; format_arg(Arg, Format) -> ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]), throw({invalid_parameter, io_lib:format("Arg ~w is not in format ~w", [Arg, Format])}). process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); (Y) -> Y end, Str)). %% ---------------- %% internal helpers %% ---------------- format_command_result(Cmd, Auth, Result, Version) -> {_, _, ResultFormat} = ejabberd_commands:get_command_format(Cmd, Auth, Version), case {ResultFormat, Result} of {{_, rescode}, V} when V == true; V == ok -> {200, 0}; {{_, rescode}, _} -> {200, 1}; {_, {error, ErrorAtom, Code, Msg}} -> format_error_result(ErrorAtom, Code, Msg); {{_, restuple}, {V, Text}} when V == true; V == ok -> {200, iolist_to_binary(Text)}; {{_, restuple}, {ErrorAtom, Msg}} -> format_error_result(ErrorAtom, 0, Msg); {{_, {list, _}}, _V} -> {_, L} = format_result(Result, ResultFormat), {200, L}; {{_, {tuple, _}}, _V} -> {_, T} = format_result(Result, ResultFormat), {200, T}; _ -> {200, {[format_result(Result, ResultFormat)]}} end. format_result(Atom, {Name, atom}) -> {misc:atom_to_binary(Name), misc:atom_to_binary(Atom)}; format_result(Int, {Name, integer}) -> {misc:atom_to_binary(Name), Int}; format_result([String | _] = StringList, {Name, string}) when is_list(String) -> Binarized = iolist_to_binary(string:join(StringList, "\n")), {misc:atom_to_binary(Name), Binarized}; format_result(String, {Name, string}) -> {misc:atom_to_binary(Name), iolist_to_binary(String)}; format_result(Code, {Name, rescode}) -> {misc:atom_to_binary(Name), Code == true orelse Code == ok}; format_result({Code, Text}, {Name, restuple}) -> {misc:atom_to_binary(Name), {[{<<"res">>, Code == true orelse Code == ok}, {<<"text">>, iolist_to_binary(Text)}]}}; format_result(Code, {Name, restuple}) -> {misc:atom_to_binary(Name), {[{<<"res">>, Code == true orelse Code == ok}, {<<"text">>, <<"">>}]}}; format_result(Els, {Name, {list, {_, {tuple, [{_, atom}, _]}} = Fmt}}) -> {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; format_result(Els, {Name, {list, {_, {tuple, [{name, string}, {value, _}]}} = Fmt}}) -> {misc:atom_to_binary(Name), {[format_result(El, Fmt) || El <- Els]}}; format_result(Els, {Name, {list, Def}}) -> {misc:atom_to_binary(Name), [element(2, format_result(El, Def)) || El <- Els]}; format_result(Tuple, {_Name, {tuple, [{_, atom}, ValFmt]}}) -> {Name2, Val} = Tuple, {_, Val2} = format_result(Val, ValFmt), {misc:atom_to_binary(Name2), Val2}; format_result(Tuple, {_Name, {tuple, [{name, string}, {value, _} = ValFmt]}}) -> {Name2, Val} = Tuple, {_, Val2} = format_result(Val, ValFmt), {iolist_to_binary(Name2), Val2}; format_result(Tuple, {Name, {tuple, Def}}) -> Els = lists:zip(tuple_to_list(Tuple), Def), {misc:atom_to_binary(Name), {[format_result(El, ElDef) || {El, ElDef} <- Els]}}; format_result(404, {_Name, _}) -> "not_found". format_error_result(conflict, Code, Msg) -> {409, Code, iolist_to_binary(Msg)}; format_error_result(not_exists, Code, Msg) -> {404, Code, iolist_to_binary(Msg)}; format_error_result(_ErrorAtom, Code, Msg) -> {500, Code, iolist_to_binary(Msg)}. unauthorized_response() -> json_error(401, 10, <<"You are not authorized to call this command.">>). invalid_token_response() -> json_error(401, 10, <<"Oauth Token is invalid or expired.">>). %% outofscope_response() -> %% json_error(401, 11, <<"Token does not grant usage to command required scope.">>). badrequest_response() -> badrequest_response(<<"400 Bad Request">>). badrequest_response(Body) -> json_response(400, jiffy:encode(Body)). json_format({Code, Result}) -> json_response(Code, jiffy:encode(Result)); json_format({HTMLCode, JSONErrorCode, Message}) -> json_error(HTMLCode, JSONErrorCode, Message). json_response(Code, Body) when is_integer(Code) -> {Code, ?HEADER(?CT_JSON), Body}. %% HTTPCode, JSONCode = integers %% message is binary json_error(HTTPCode, JSONCode, Message) -> {HTTPCode, ?HEADER(?CT_JSON), jiffy:encode({[{<<"status">>, <<"error">>}, {<<"code">>, JSONCode}, {<<"message">>, Message}]}) }. log(Call, Args, {Addr, Port}) -> AddrS = misc:ip_to_list({Addr, Port}), ?INFO_MSG("API call ~ts ~p from ~ts:~p", [Call, hide_sensitive_args(Args), AddrS, Port]); log(Call, Args, IP) -> ?INFO_MSG("API call ~ts ~p (~p)", [Call, hide_sensitive_args(Args), IP]). hide_sensitive_args(Args=[_H|_T]) -> lists:map( fun({<<"password">>, Password}) -> {<<"password">>, ejabberd_config:may_hide_data(Password)}; ({<<"newpass">>,NewPassword}) -> {<<"newpass">>, ejabberd_config:may_hide_data(NewPassword)}; (E) -> E end, Args); hide_sensitive_args(NonListArgs) -> NonListArgs. mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides a ReST API to call ejabberd commands " "using JSON data."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers]."), "", ?T("To use a specific API version N, when defining the URL path " "in the request_handlers, add a 'vN'. " "For example: '/api/v2: mod_http_api'"), "", ?T("To run a command, send a POST request to the corresponding " "URL: 'http://localhost:5280/api/'")], example => ["listen:", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " /api: mod_http_api", "", "modules:", " mod_http_api: {}"]}. ejabberd-21.12/src/mod_roster_opt.erl0000644000232200023220000000414714154362354020172 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_roster_opt). -export([access/1]). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([store_current_id/1]). -export([use_cache/1]). -export([versioning/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_roster, access). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_roster, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_roster, db_type). -spec store_current_id(gen_mod:opts() | global | binary()) -> boolean(). store_current_id(Opts) when is_map(Opts) -> gen_mod:get_opt(store_current_id, Opts); store_current_id(Host) -> gen_mod:get_module_opt(Host, mod_roster, store_current_id). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_roster, use_cache). -spec versioning(gen_mod:opts() | global | binary()) -> boolean(). versioning(Opts) when is_map(Opts) -> gen_mod:get_opt(versioning, Opts); versioning(Host) -> gen_mod:get_module_opt(Host, mod_roster, versioning). ejabberd-21.12/src/ejabberd_auth_jwt.erl0000644000232200023220000001240414154362354020571 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_jwt.erl %%% Author : Mickael Remond %%% Purpose : Authentication using JWT tokens %%% Created : 16 Mar 2019 by Mickael Remond %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_jwt). -author('mremond@process-one.net'). -behaviour(ejabberd_auth). -export([start/1, stop/1, check_password/4, store_type/1, plain_password_required/1, user_exists/2, use_cache/1 ]). %% 'ejabberd_hooks' callback: -export([check_decoded_jwt/5]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> %% We add our default JWT verifier with hook priority 100. %% So if you need to check or verify your custom JWT before the %% default verifier, It's better to use this hook with priority %% little than 100 and return bool() or {stop, bool()} in your own %% callback function. ejabberd_hooks:add(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100), case ejabberd_option:jwt_key(Host) of undefined -> ?ERROR_MSG("Option jwt_key is not configured for ~ts: " "JWT authentication won't work", [Host]); _ -> ok end. stop(Host) -> ejabberd_hooks:delete(check_decoded_jwt, Host, ?MODULE, check_decoded_jwt, 100). plain_password_required(_Host) -> true. store_type(_Host) -> external. -spec check_password(binary(), binary(), binary(), binary()) -> {ets_cache:tag(), boolean() | {stop, boolean()}}. check_password(User, AuthzId, Server, Token) -> %% MREMOND: Should we move the AuthzId check at a higher level in %% the call stack? if AuthzId /= <<>> andalso AuthzId /= User -> {nocache, false}; true -> if Token == <<"">> -> {nocache, false}; true -> Res = check_jwt_token(User, Server, Token), Rule = ejabberd_option:jwt_auth_only_rule(Server), case acl:match_rule(Server, Rule, jid:make(User, Server, <<"">>)) of deny -> {nocache, Res}; allow -> {nocache, {stop, Res}} end end end. user_exists(_User, _Host) -> {nocache, false}. use_cache(_) -> false. %%%---------------------------------------------------------------------- %%% 'ejabberd_hooks' callback %%%---------------------------------------------------------------------- check_decoded_jwt(true, Fields, _Signature, Server, User) -> JidField = ejabberd_option:jwt_jid_field(Server), case maps:find(JidField, Fields) of {ok, SJid} when is_binary(SJid) -> try JID = jid:decode(SJid), JID#jid.luser == User andalso JID#jid.lserver == Server catch error:{bad_jid, _} -> false end; _ -> % error | {ok, _UnknownType} false end; check_decoded_jwt(Acc, _, _, _, _) -> Acc. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- check_jwt_token(User, Server, Token) -> JWK = ejabberd_option:jwt_key(Server), try jose_jwt:verify(JWK, Token) of {true, {jose_jwt, Fields}, Signature} -> Now = erlang:system_time(second), ?DEBUG("jwt verify at system timestamp ~p: ~p - ~p~n", [Now, Fields, Signature]), case maps:find(<<"exp">>, Fields) of error -> %% No expiry in token => We consider token invalid: false; {ok, Exp} -> if Exp > Now -> ejabberd_hooks:run_fold( check_decoded_jwt, Server, true, [Fields, Signature, Server, User] ); true -> %% return false, if token has expired false end end; {false, _, _} -> false catch error:{badarg, _} -> false end. ejabberd-21.12/src/mod_mix_sql.erl0000644000232200023220000001667614154362354017460 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 1 Dec 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix_sql). -behaviour(mod_mix). %% API -export([init/2]). -export([set_channel/6, get_channels/2, get_channel/3, del_channel/3]). -export([set_participant/6, get_participant/4, get_participants/3, del_participant/4]). -export([subscribe/5, unsubscribe/4, unsubscribe/5, get_subscribed/4]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> %% TODO ok. set_channel(LServer, Channel, Service, CreatorJID, Hidden, Key) -> {User, Domain, _} = jid:tolower(CreatorJID), RawJID = jid:encode(jid:remove_resource(CreatorJID)), case ?SQL_UPSERT(LServer, "mix_channel", ["!channel=%(Channel)s", "!service=%(Service)s", "username=%(User)s", "domain=%(Domain)s", "jid=%(RawJID)s", "hidden=%(Hidden)b", "hmac_key=%(Key)s"]) of ok -> ok; _Err -> {error, db_failure} end. get_channels(LServer, Service) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(channel)s, @(hidden)b from mix_channel " "where service=%(Service)s")) of {selected, Ret} -> {ok, [Channel || {Channel, Hidden} <- Ret, Hidden == false]}; _Err -> {error, db_failure} end. get_channel(LServer, Channel, Service) -> SQL = ?SQL("select @(jid)s, @(hidden)b, @(hmac_key)s from mix_channel " "where channel=%(Channel)s and service=%(Service)s"), case ejabberd_sql:sql_query(LServer, SQL) of {selected, [{RawJID, Hidden, Key}]} -> try jid:decode(RawJID) of JID -> {ok, {JID, Hidden, Key}} catch _:{bad_jid, _} -> report_corrupted(jid, SQL), {error, db_failure} end; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. del_channel(LServer, Channel, Service) -> F = fun() -> ejabberd_sql:sql_query_t( ?SQL("delete from mix_channel where " "channel=%(Channel)s and service=%(Service)s")), ejabberd_sql:sql_query_t( ?SQL("delete from mix_participant where " "channel=%(Channel)s and service=%(Service)s")), ejabberd_sql:sql_query_t( ?SQL("delete from mix_subscription where " "channel=%(Channel)s and service=%(Service)s")) end, case ejabberd_sql:sql_transaction(LServer, F) of {atomic, _} -> ok; _Err -> {error, db_failure} end. set_participant(LServer, Channel, Service, JID, ID, Nick) -> {User, Domain, _} = jid:tolower(JID), RawJID = jid:encode(jid:remove_resource(JID)), case ?SQL_UPSERT(LServer, "mix_participant", ["!channel=%(Channel)s", "!service=%(Service)s", "!username=%(User)s", "!domain=%(Domain)s", "jid=%(RawJID)s", "id=%(ID)s", "nick=%(Nick)s"]) of ok -> ok; _Err -> {error, db_failure} end. get_participant(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( LServer, ?SQL("select @(id)s, @(nick)s from mix_participant " "where channel=%(Channel)s and service=%(Service)s " "and username=%(User)s and domain=%(Domain)s")) of {selected, [Ret]} -> {ok, Ret}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. get_participants(LServer, Channel, Service) -> SQL = ?SQL("select @(jid)s, @(id)s, @(nick)s from mix_participant " "where channel=%(Channel)s and service=%(Service)s"), case ejabberd_sql:sql_query(LServer, SQL) of {selected, Ret} -> {ok, lists:filtermap( fun({RawJID, ID, Nick}) -> try jid:decode(RawJID) of JID -> {true, {JID, ID, Nick}} catch _:{bad_jid, _} -> report_corrupted(jid, SQL), false end end, Ret)}; _Err -> {error, db_failure} end. del_participant(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( LServer, ?SQL("delete from mix_participant where " "channel=%(Channel)s and service=%(Service)s " "and username=%(User)s and domain=%(Domain)s")) of {updated, _} -> ok; _Err -> {error, db_failure} end. subscribe(_LServer, _Channel, _Service, _JID, []) -> ok; subscribe(LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), RawJID = jid:encode(jid:remove_resource(JID)), F = fun() -> lists:foreach( fun(Node) -> ?SQL_UPSERT_T( "mix_subscription", ["!channel=%(Channel)s", "!service=%(Service)s", "!username=%(User)s", "!domain=%(Domain)s", "!node=%(Node)s", "jid=%(RawJID)s"]) end, Nodes) end, case ejabberd_sql:sql_transaction(LServer, F) of {atomic, _} -> ok; _Err -> {error, db_failure} end. get_subscribed(LServer, Channel, Service, Node) -> SQL = ?SQL("select @(jid)s from mix_subscription " "where channel=%(Channel)s and service=%(Service)s " "and node=%(Node)s"), case ejabberd_sql:sql_query(LServer, SQL) of {selected, Ret} -> {ok, lists:filtermap( fun({RawJID}) -> try jid:decode(RawJID) of JID -> {true, JID} catch _:{bad_jid, _} -> report_corrupted(jid, SQL), false end end, Ret)}; _Err -> {error, db_failure} end. unsubscribe(LServer, Channel, Service, JID) -> {User, Domain, _} = jid:tolower(JID), case ejabberd_sql:sql_query( LServer, ?SQL("delete from mix_subscription " "where channel=%(Channel)s and service=%(Service)s " "and username=%(User)s and domain=%(Domain)s")) of {updated, _} -> ok; _Err -> {error, db_failure} end. unsubscribe(_LServer, _Channel, _Service, _JID, []) -> ok; unsubscribe(LServer, Channel, Service, JID, Nodes) -> {User, Domain, _} = jid:tolower(JID), F = fun() -> lists:foreach( fun(Node) -> ejabberd_sql:sql_query_t( ?SQL("delete from mix_subscription " "where channel=%(Channel)s " "and service=%(Service)s " "and username=%(User)s " "and domain=%(Domain)s " "and node=%(Node)s")) end, Nodes) end, case ejabberd_sql:sql_transaction(LServer, F) of {atomic, ok} -> ok; _Err -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec report_corrupted(atom(), #sql_query{}) -> ok. report_corrupted(Column, SQL) -> ?ERROR_MSG("Corrupted value of '~ts' column returned by " "SQL request: ~ts", [Column, SQL#sql_query.hash]). ejabberd-21.12/src/mod_last.erl0000644000232200023220000003061514154362354016734 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_last.erl %%% Author : Alexey Shchepin %%% Purpose : jabber:iq:last support (XEP-0012) %%% Created : 24 Oct 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_last). -author('alexey@process-one.net'). -protocol({xep, 12, '2.0'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq/1, export/1, process_sm_iq/1, on_presence_update/4, import_info/0, import/5, import_start/2, store_last_info/4, get_last_info/2, remove_user/2, mod_opt_type/1, mod_options/1, mod_doc/0, register_user/2, depends/2, privacy_check_packet/4]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("mod_last.hrl"). -include("translate.hrl"). -define(LAST_CACHE, last_activity_cache). -type c2s_state() :: ejabberd_c2s:state(). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), #last_activity{}) -> ok | pass. -callback get_last(binary(), binary()) -> {ok, {non_neg_integer(), binary()}} | error | {error, any()}. -callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}. -callback remove_user(binary(), binary()) -> any(). -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_LAST, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_LAST, ?MODULE, process_sm_iq), ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, privacy_check_packet, 30), ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, on_presence_update, 50). stop(Host) -> ejabberd_hooks:delete(register_user, Host, ?MODULE, register_user, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE, on_presence_update, 50), ejabberd_hooks:delete(privacy_check_packet, Host, ?MODULE, privacy_check_packet, 30), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_LAST), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_LAST). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). %%% %%% Uptime of ejabberd node %%% -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq(#iq{type = get} = IQ) -> xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}). -spec get_node_uptime() -> non_neg_integer(). %% @doc Get the uptime of the ejabberd node, expressed in seconds. %% When ejabberd is starting, ejabberd_config:start/0 stores the datetime. get_node_uptime() -> NodeStart = ejabberd_config:get_node_start(), erlang:monotonic_time(second) - NodeStart. %%% %%% Serve queries about user last online %%% -spec process_sm_iq(iq()) -> iq(). process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) -> User = To#jid.luser, Server = To#jid.lserver, {Subscription, _Ask, _Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, Server, {none, none, []}, [User, Server, From]), if (Subscription == both) or (Subscription == from) or (From#jid.luser == To#jid.luser) and (From#jid.lserver == To#jid.lserver) -> Pres = xmpp:set_from_to(#presence{}, To, From), case ejabberd_hooks:run_fold(privacy_check_packet, Server, allow, [To, Pres, out]) of allow -> get_last_iq(IQ, User, Server); deny -> xmpp:make_error(IQ, xmpp:err_forbidden()) end; true -> Txt = ?T("Not subscribed"), xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. -spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}. privacy_check_packet(allow, C2SState, #iq{from = From, to = To, type = T} = IQ, in) when T == get; T == set -> case xmpp:has_subtag(IQ, #last{}) of true -> #jid{luser = LUser, lserver = LServer} = To, {Sub, _, _} = ejabberd_hooks:run_fold( roster_get_jid_info, LServer, {none, none, []}, [LUser, LServer, From]), if Sub == from; Sub == both -> Pres = #presence{from = To, to = From}, case ejabberd_hooks:run_fold( privacy_check_packet, allow, [C2SState, Pres, out]) of allow -> allow; deny -> {stop, deny} end; true -> {stop, deny} end; false -> allow end; privacy_check_packet(Acc, _, _, _) -> Acc. -spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found | {error, any()}. get_last(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?LAST_CACHE, {LUser, LServer}, fun() -> Mod:get_last(LUser, LServer) end); false -> Mod:get_last(LUser, LServer) end, case Res of {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status}; error -> not_found; Err -> Err end. -spec get_last_iq(iq(), binary(), binary()) -> iq(). get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) -> case ejabberd_sm:get_user_resources(LUser, LServer) of [] -> case get_last(LUser, LServer) of {error, _Reason} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); not_found -> Txt = ?T("No info about last activity found"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); {ok, TimeStamp, Status} -> TimeStamp2 = erlang:system_time(second), Sec = TimeStamp2 - TimeStamp, xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status}) end; _ -> xmpp:make_iq_result(IQ, #last{seconds = 0}) end. -spec register_user(binary(), binary()) -> any(). register_user(User, Server) -> on_presence_update( User, Server, <<"RegisterResource">>, <<"Registered but didn't login">>). -spec on_presence_update(binary(), binary(), binary(), binary()) -> any(). on_presence_update(User, Server, _Resource, Status) -> TimeStamp = erlang:system_time(second), store_last_info(User, Server, TimeStamp, Status). -spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any(). store_last_info(User, Server, TimeStamp, Status) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of true -> ets_cache:update( ?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}}, fun() -> Mod:store_last_info(LUser, LServer, TimeStamp, Status) end, cache_nodes(Mod, LServer)); false -> Mod:store_last_info(LUser, LServer, TimeStamp, Status) end. -spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found. get_last_info(LUser, LServer) -> case get_last(LUser, LServer) of {error, _Reason} -> not_found; Res -> Res end. -spec remove_user(binary(), binary()) -> any(). remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?LAST_CACHE, CacheOpts); false -> ets_cache:delete(?LAST_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_last_opt:cache_size(Opts), CacheMissed = mod_last_opt:cache_missed(Opts), LifeTime = mod_last_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_last_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. import_info() -> [{<<"last">>, 3}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) -> TS = case TimeStamp of <<"">> -> 0; _ -> binary_to_integer(TimeStamp) end, LA = #last_activity{us = {LUser, LServer}, timestamp = TS, status = State}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, LA). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). depends(_Host, _Opts) -> []. mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0012.html" "[XEP-0012: Last Activity]. It can be used " "to discover when a disconnected user last accessed " "the server, to know when a connected user was last " "active on the server, or to query the uptime of the ejabberd server."), opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. ejabberd-21.12/src/str.erl0000644000232200023220000002005514154362354015737 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : str.erl %%% Author : Evgeniy Khramtsov %%% Purpose : Provide binary string manipulations %%% Created : 23 Feb 2012 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(str). -author('ekhramtsov@process-one.net'). %% API -export([equal/2, concat/2, rchr/2, str/2, rstr/2, span/2, cspan/2, copies/2, words/1, words/2, sub_word/2, sub_word/3, strip/1, strip/2, len/1, tokens/2, left/2, left/3, right/2, right/3, centre/2, centre/3, sub_string/2, sub_string/3, to_upper/1, join/2, substr/2, chr/2, chars/3, chars/2, substr/3, strip/3, to_lower/1, to_float/1, prefix/2, suffix/2, format/2, to_integer/1, sha/1, to_hexlist/1, translate_and_format/3]). %%%=================================================================== %%% API %%%=================================================================== -spec len(binary()) -> non_neg_integer(). len(B) -> byte_size(B). -spec equal(binary(), binary()) -> boolean(). equal(B1, B2) -> B1 == B2. -spec concat(binary(), binary()) -> binary(). concat(B1, B2) -> <>. -spec rchr(binary(), char()) -> non_neg_integer(). rchr(B, C) -> string:rchr(binary_to_list(B), C). -spec str(binary(), binary()) -> non_neg_integer(). str(B1, B2) -> case binary:match(B1, B2) of {R, _Len} -> R+1; _ -> 0 end. -spec rstr(binary(), binary()) -> non_neg_integer(). rstr(B1, B2) -> string:rstr(binary_to_list(B1), binary_to_list(B2)). -spec span(binary(), binary()) -> non_neg_integer(). span(B1, B2) -> string:span(binary_to_list(B1), binary_to_list(B2)). -spec cspan(binary(), binary()) -> non_neg_integer(). cspan(B1, B2) -> string:cspan(binary_to_list(B1), binary_to_list(B2)). -spec copies(binary(), non_neg_integer()) -> binary(). copies(B, N) -> binary:copy(B, N). -spec words(binary()) -> pos_integer(). words(B) -> string:words(binary_to_list(B)). -spec words(binary(), char()) -> pos_integer(). words(B, C) -> string:words(binary_to_list(B), C). -spec sub_word(binary(), integer()) -> binary(). sub_word(B, N) -> iolist_to_binary(string:sub_word(binary_to_list(B), N)). -spec sub_word(binary(), integer(), char()) -> binary(). sub_word(B, N, C) -> iolist_to_binary(string:sub_word(binary_to_list(B), N, C)). -spec strip(binary()) -> binary(). strip(B) -> iolist_to_binary(string:strip(binary_to_list(B))). -spec strip(binary(), both | left | right) -> binary(). strip(B, D) -> iolist_to_binary(string:strip(binary_to_list(B), D)). -spec left(binary(), non_neg_integer()) -> binary(). left(B, N) -> iolist_to_binary(string:left(binary_to_list(B), N)). -spec left(binary(), non_neg_integer(), char()) -> binary(). left(B, N, C) -> iolist_to_binary(string:left(binary_to_list(B), N, C)). -spec right(binary(), non_neg_integer()) -> binary(). right(B, N) -> iolist_to_binary(string:right(binary_to_list(B), N)). -spec right(binary(), non_neg_integer(), char()) -> binary(). right(B, N, C) -> iolist_to_binary(string:right(binary_to_list(B), N, C)). -spec centre(binary(), non_neg_integer()) -> binary(). centre(B, N) -> iolist_to_binary(string:centre(binary_to_list(B), N)). -spec centre(binary(), non_neg_integer(), char()) -> binary(). centre(B, N, C) -> iolist_to_binary(string:centre(binary_to_list(B), N, C)). -spec sub_string(binary(), pos_integer()) -> binary(). sub_string(B, N) -> iolist_to_binary(string:sub_string(binary_to_list(B), N)). -spec sub_string(binary(), pos_integer(), pos_integer()) -> binary(). sub_string(B, S, E) -> iolist_to_binary(string:sub_string(binary_to_list(B), S, E)). -spec to_upper(binary()) -> binary(); (char()) -> char(). to_upper(B) when is_binary(B) -> iolist_to_binary(string:to_upper(binary_to_list(B))); to_upper(C) -> string:to_upper(C). -spec join([binary()], binary() | char()) -> binary(). join(L, Sep) -> iolist_to_binary(join_s(L, Sep)). -spec substr(binary(), pos_integer()) -> binary(). substr(B, N) -> binary_part(B, N-1, byte_size(B)-N+1). -spec chr(binary(), char()) -> non_neg_integer(). chr(B, C) -> string:chr(binary_to_list(B), C). -spec chars(char(), non_neg_integer(), binary()) -> binary(). chars(C, N, B) -> iolist_to_binary(string:chars(C, N, binary_to_list(B))). -spec chars(char(), non_neg_integer()) -> binary(). chars(C, N) -> iolist_to_binary(string:chars(C, N)). -spec substr(binary(), pos_integer(), non_neg_integer()) -> binary(). substr(B, S, E) -> binary_part(B, S-1, E). -spec strip(binary(), both | left | right, char()) -> binary(). strip(B, D, C) -> iolist_to_binary(string:strip(binary_to_list(B), D, C)). -spec to_lower(binary()) -> binary(); (char()) -> char(). to_lower(B) when is_binary(B) -> iolist_to_binary(string:to_lower(binary_to_list(B))); to_lower(C) -> string:to_lower(C). -spec tokens(binary(), binary()) -> [binary()]. tokens(B1, B2) -> [iolist_to_binary(T) || T <- string:tokens(binary_to_list(B1), binary_to_list(B2))]. -spec to_float(binary()) -> {float(), binary()} | {error, no_float}. to_float(B) -> case string:to_float(binary_to_list(B)) of {error, R} -> {error, R}; {Float, Rest} -> {Float, iolist_to_binary(Rest)} end. -spec to_integer(binary()) -> {integer(), binary()} | {error, no_integer}. to_integer(B) -> case string:to_integer(binary_to_list(B)) of {error, R} -> {error, R}; {Int, Rest} -> {Int, iolist_to_binary(Rest)} end. -spec prefix(binary(), binary()) -> boolean(). prefix(Prefix, B) -> Size = byte_size(Prefix), case B of <> -> true; _ -> false end. -spec suffix(binary(), binary()) -> boolean(). suffix(B1, B2) -> lists:suffix(binary_to_list(B1), binary_to_list(B2)). -spec format(io:format(), list()) -> binary(). format(Format, Args) -> unicode:characters_to_binary(io_lib:format(Format, Args)). -spec translate_and_format(binary(), binary(), list()) -> binary(). translate_and_format(Lang, Format, Args) -> format(unicode:characters_to_list(translate:translate(Lang, Format)), Args). -spec sha(iodata()) -> binary(). sha(Text) -> Bin = crypto:hash(sha, Text), to_hexlist(Bin). -spec to_hexlist(binary()) -> binary(). to_hexlist(S) when is_list(S) -> to_hexlist(iolist_to_binary(S)); to_hexlist(Bin) when is_binary(Bin) -> << <<(digit_to_xchar(N div 16)), (digit_to_xchar(N rem 16))>> || <> <= Bin >>. %%%=================================================================== %%% Internal functions %%%=================================================================== join_s([], _Sep) -> []; join_s([H|T], Sep) -> [H, [[Sep, X] || X <- T]]. digit_to_xchar(D) when (D >= 0) and (D < 10) -> D + $0; digit_to_xchar(D) -> D + $a - 10. ejabberd-21.12/src/mod_offline_mnesia.erl0000644000232200023220000001615014154362354020745 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_offline_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_offline_mnesia). -behaviour(mod_offline). -export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, remove_old_messages/2, remove_user/2, read_message_headers/2, read_message/3, remove_message/3, read_all_messages/2, remove_all_messages/2, count_messages/2, import/1]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_offline.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, offline_msg, [{disc_only_copies, [node()]}, {type, bag}, {attributes, record_info(fields, offline_msg)}]). store_message(#offline_msg{packet = Pkt} = OffMsg) -> El = xmpp:encode(Pkt), mnesia:dirty_write(OffMsg#offline_msg{packet = El}). pop_messages(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> Rs = mnesia:wread({offline_msg, US}), mnesia:delete({offline_msg, US}), Rs end, case mnesia:transaction(F) of {atomic, L} -> {ok, lists:keysort(#offline_msg.timestamp, L)}; {aborted, Reason} -> {error, Reason} end. remove_expired_messages(_LServer) -> TimeStamp = erlang:timestamp(), F = fun () -> mnesia:write_lock_table(offline_msg), mnesia:foldl(fun (Rec, _Acc) -> case Rec#offline_msg.expire of never -> ok; TS -> if TS < TimeStamp -> mnesia:delete_object(Rec); true -> ok end end end, ok, offline_msg) end, mnesia:transaction(F). remove_old_messages(Days, _LServer) -> S = erlang:system_time(second) - 60 * 60 * 24 * Days, MegaSecs1 = S div 1000000, Secs1 = S rem 1000000, TimeStamp = {MegaSecs1, Secs1, 0}, F = fun () -> mnesia:write_lock_table(offline_msg), mnesia:foldl(fun (#offline_msg{timestamp = TS} = Rec, _Acc) when TS < TimeStamp -> mnesia:delete_object(Rec); (_Rec, _Acc) -> ok end, ok, offline_msg) end, mnesia:transaction(F). remove_user(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> mnesia:delete({offline_msg, US}) end, mnesia:transaction(F). read_message_headers(LUser, LServer) -> Msgs = mnesia:dirty_read({offline_msg, {LUser, LServer}}), Hdrs = lists:map( fun(#offline_msg{from = From, to = To, packet = Pkt, timestamp = TS}) -> Seq = now_to_integer(TS), {Seq, From, To, TS, Pkt} end, Msgs), lists:keysort(1, Hdrs). read_message(LUser, LServer, I) -> US = {LUser, LServer}, TS = integer_to_now(I), case mnesia:dirty_match_object( offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of [Msg|_] -> {ok, Msg}; _ -> error end. remove_message(LUser, LServer, I) -> US = {LUser, LServer}, TS = integer_to_now(I), case mnesia:dirty_match_object( offline_msg, #offline_msg{us = US, timestamp = TS, _ = '_'}) of [] -> {error, notfound}; Msgs -> lists:foreach( fun(Msg) -> mnesia:dirty_delete_object(Msg) end, Msgs) end. read_all_messages(LUser, LServer) -> US = {LUser, LServer}, lists:keysort(#offline_msg.timestamp, mnesia:dirty_read({offline_msg, US})). remove_all_messages(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> mnesia:write_lock_table(offline_msg), lists:foreach(fun (Msg) -> mnesia:delete_object(Msg) end, mnesia:dirty_read({offline_msg, US})) end, mnesia:transaction(F). count_messages(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> count_mnesia_records(US) end, {cache, case mnesia:async_dirty(F) of I when is_integer(I) -> I; _ -> 0 end}. import(#offline_msg{} = Msg) -> mnesia:dirty_write(Msg). need_transform({offline_msg, {U, S}, _, _, _, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'offline_msg' will be converted to binary", []), true; need_transform({offline_msg, _, _, _, _, _, _, _}) -> true; need_transform(_) -> false. transform({offline_msg, {U, S}, Timestamp, Expire, From, To, _, Packet}) -> #offline_msg{us = {U, S}, timestamp = Timestamp, expire = Expire, from = From ,to = To, packet = Packet}; transform(#offline_msg{us = {U, S}, from = From, to = To, packet = El} = R) -> R#offline_msg{us = {iolist_to_binary(U), iolist_to_binary(S)}, from = jid_to_binary(From), to = jid_to_binary(To), packet = fxml:to_xmlel(El)}. %%%=================================================================== %%% Internal functions %%%=================================================================== %% Return the number of records matching a given match expression. %% This function is intended to be used inside a Mnesia transaction. %% The count has been written to use the fewest possible memory by %% getting the record by small increment and by using continuation. -define(BATCHSIZE, 100). count_mnesia_records(US) -> MatchExpression = #offline_msg{us = US, _ = '_'}, case mnesia:select(offline_msg, [{MatchExpression, [], [[]]}], ?BATCHSIZE, read) of {Result, Cont} -> Count = length(Result), count_records_cont(Cont, Count); '$end_of_table' -> 0 end. count_records_cont(Cont, Count) -> case mnesia:select(Cont) of {Result, Cont} -> NewCount = Count + length(Result), count_records_cont(Cont, NewCount); '$end_of_table' -> Count end. jid_to_binary(#jid{user = U, server = S, resource = R, luser = LU, lserver = LS, lresource = LR}) -> #jid{user = iolist_to_binary(U), server = iolist_to_binary(S), resource = iolist_to_binary(R), luser = iolist_to_binary(LU), lserver = iolist_to_binary(LS), lresource = iolist_to_binary(LR)}. now_to_integer({MS, S, US}) -> (MS * 1000000 + S) * 1000000 + US. integer_to_now(Int) -> Secs = Int div 1000000, USec = Int rem 1000000, MSec = Secs div 1000000, Sec = Secs rem 1000000, {MSec, Sec, USec}. ejabberd-21.12/src/node_flat.erl0000644000232200023220000011141014154362354017056 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : node_flat.erl %%% Author : Christophe Romain %%% Purpose : Standard PubSub node plugin %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the default PubSub plugin. %%%

It is used as a default for all unknown PubSub node type. It can serve %%% as a developer basis and reference to build its own custom pubsub node %%% types.

%%%

PubSub plugin nodes are using the {@link gen_node} behaviour.

-module(node_flat). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.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/2, remove_extra_items/3, remove_expired_items/2, 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_last_items/3, get_only_item/2, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1, can_fetch_item/2, is_subscribed/1, transform/1]). init(_Host, _ServerHost, _Opts) -> %pubsub_subscription:init(Host, ServerHost, Opts), ejabberd_mnesia:create(?MODULE, pubsub_state, [{disc_copies, [node()]}, {index, [nodeidx]}, {type, ordered_set}, {attributes, record_info(fields, pubsub_state)}]), ejabberd_mnesia:create(?MODULE, pubsub_item, [{disc_only_copies, [node()]}, {index, [nodeidx]}, {attributes, record_info(fields, pubsub_item)}]), ejabberd_mnesia:create(?MODULE, pubsub_orphan, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_orphan)}]), ItemsFields = record_info(fields, pubsub_item), case mnesia:table_info(pubsub_item, attributes) of ItemsFields -> ok; _ -> mnesia:transform_table(pubsub_item, ignore, ItemsFields) end, ok. terminate(_Host, _ServerHost) -> ok. options() -> [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, {purge_offline, false}, {persist_items, true}, {max_items, ?MAXITEMS}, {subscribe, true}, {access_model, open}, {roster_groups_allowed, []}, {publish_model, publishers}, {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, {presence_based_delivery, false}, {itemreply, none}]. features() -> [<<"create-nodes">>, <<"auto-create">>, <<"access-authorize">>, <<"delete-nodes">>, <<"delete-items">>, <<"get-pending">>, <<"instant-nodes">>, <<"manage-subscriptions">>, <<"modify-affiliations">>, <<"outcast-affiliation">>, <<"persistent-items">>, <<"multi-items">>, <<"publish">>, <<"publish-only-affiliation">>, <<"publish-options">>, <<"purge-nodes">>, <<"retract-items">>, <<"retrieve-affiliations">>, <<"retrieve-items">>, <<"retrieve-subscriptions">>, <<"subscribe">>, %%<<"subscription-options">>, <<"subscription-notifications">>]. %% @doc Checks if the current user has the permission to create the requested node %%

In flat node, any unused node name is allowed. The access parameter is also %% checked. This parameter depends on the value of the %% access_createnode ACL value in ejabberd config file.

create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> LOwner = jid:tolower(Owner), Allowed = case LOwner of {<<"">>, Host, <<"">>} -> true; % pubsub service always allowed _ -> acl:match_rule(ServerHost, Access, LOwner) =:= allow end, {result, Allowed}. create_node(Nidx, Owner) -> OwnerKey = jid:tolower(jid:remove_resource(Owner)), set_state(#pubsub_state{stateid = {OwnerKey, Nidx}, nodeidx = Nidx, affiliation = owner}), {result, {default, broadcast}}. delete_node(Nodes) -> Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> lists:map(fun (S) -> {J, S} end, Ss) end, Reply = lists:map(fun (#pubsub_node{id = Nidx} = PubsubNode) -> {result, States} = get_states(Nidx), lists:foreach(fun (State) -> del_items(Nidx, State#pubsub_state.items), del_state(State#pubsub_state{items = []}) end, States), del_orphan_items(Nidx), {PubsubNode, lists:flatmap(Tr, States)} end, Nodes), {result, {default, broadcast, Reply}}. %% @doc

Accepts or rejects subcription requests on a PubSub node.

%%

The mechanism works as follow: %%

    %%
  • The main PubSub module prepares the subscription and passes the %% result of the preparation as a record.
  • %%
  • This function gets the prepared record and several other parameters and %% can decide to:
      %%
    • reject the subscription;
    • %%
    • allow it as is, letting the main module perform the database %% persistence;
    • %%
    • allow it, modifying the record. The main module will store the %% modified record;
    • %%
    • allow it, but perform the needed persistence operations.
    %%

%%

The selected behaviour depends on the return parameter: %%

    %%
  • {error, Reason}: an IQ error result will be returned. No %% subscription will actually be performed.
  • %%
  • true: Subscribe operation is allowed, based on the %% unmodified record passed in parameter SubscribeResult. If this %% parameter contains an error, no subscription will be performed.
  • %%
  • {true, PubsubState}: Subscribe operation is allowed, but %% the {@link mod_pubsub:pubsubState()} record returned replaces the value %% passed in parameter SubscribeResult.
  • %%
  • {true, done}: Subscribe operation is allowed, but the %% {@link mod_pubsub:pubsubState()} will be considered as already stored and %% no further persistence operation will be performed. This case is used, %% when the plugin module is doing the persistence by itself or when it want %% to completely disable persistence.
%%

%%

In the default plugin module, the record is unchanged.

subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, _Options) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, GenState = get_state(Nidx, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Nidx, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, Subscriptions = SubState#pubsub_state.subscriptions, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), PendingSubscription = lists:any(fun ({pending, _}) -> true; (_) -> false end, Subscriptions), Owner = Affiliation == owner, if not Authorized -> {error, mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_invalid_jid())}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; PendingSubscription -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_pending_subscription())}; (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and (not RosterGroup) and (not Owner) -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> {error, mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; %%ForbiddenAnonymous -> %% % Requesting entity is anonymous %% {error, xmpp:err_forbidden()}; true -> %%SubId = pubsub_subscription:add_subscription(Subscriber, Nidx, Options), {NewSub, SubId} = case Subscriptions of [{subscribed, Id}|_] -> {subscribed, Id}; [] -> Id = pubsub_subscription:make_subid(), Sub = case AccessModel of authorize -> pending; _ -> subscribed end, set_state(SubState#pubsub_state{subscriptions = [{Sub, Id} | Subscriptions]}), {Sub, Id} end, case {NewSub, SendLast} of {subscribed, never} -> {result, {default, subscribed, SubId}}; {subscribed, _} -> {result, {default, subscribed, SubId, send_last}}; {_, _} -> {result, {default, pending, SubId}} end end. %% @doc

Unsubscribe the Subscriber from the Node.

unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, GenState = get_state(Nidx, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Nidx, SubKey) end, Subscriptions = lists:filter(fun ({_Sub, _SubId}) -> true; (_SubId) -> false end, SubState#pubsub_state.subscriptions), SubIdExists = case SubId of <<>> -> false; Binary when is_binary(Binary) -> true; _ -> false end, if %% Requesting entity is prohibited from unsubscribing entity not Authorized -> {error, xmpp:err_forbidden()}; %% Entity did not specify SubId %%SubId == "", ?? -> %% {error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; %% Invalid subscription identifier %%InvalidSubId -> %% {error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; %% Requesting entity is not a subscriber Subscriptions == [] -> {error, mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())}; %% Subid supplied, so use that. SubIdExists -> Sub = first_in_list(fun ({_, S}) when S == SubId -> true; (_) -> false end, SubState#pubsub_state.subscriptions), case Sub of {value, S} -> delete_subscriptions(SubState, [S]), {result, default}; false -> {error, mod_pubsub:extended_error(xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())} end; %% Asking to remove all subscriptions to the given node SubId == all -> delete_subscriptions(SubState, Subscriptions), {result, default}; %% No subid supplied, but there's only one matching subscription length(Subscriptions) == 1 -> delete_subscriptions(SubState, Subscriptions), {result, default}; %% No subid and more than one possible subscription match. true -> {error, mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())} end. delete_subscriptions(SubState, Subscriptions) -> NewSubs = lists:foldl(fun ({Subscription, SubId}, Acc) -> %%pubsub_subscription:delete_subscription(SubKey, Nidx, SubId), Acc -- [{Subscription, SubId}] end, SubState#pubsub_state.subscriptions, Subscriptions), case {SubState#pubsub_state.affiliation, NewSubs} of {none, []} -> del_state(SubState); _ -> set_state(SubState#pubsub_state{subscriptions = NewSubs}) end. %% @doc

Publishes the item passed as parameter.

%%

The mechanism works as follow: %%

    %%
  • The main PubSub module prepares the item to publish and passes the %% result of the preparation as a {@link mod_pubsub:pubsubItem()} record.
  • %%
  • This function gets the prepared record and several other parameters and can decide to:
      %%
    • reject the publication;
    • %%
    • allow the publication as is, letting the main module perform the database persistence;
    • %%
    • allow the publication, modifying the record. The main module will store the modified record;
    • %%
    • allow it, but perform the needed persistence operations.
    %%

%%

The selected behaviour depends on the return parameter: %%

    %%
  • {error, Reason}: an iq error result will be return. No %% publication is actually performed.
  • %%
  • true: Publication operation is allowed, based on the %% unmodified record passed in parameter Item. If the Item %% parameter contains an error, no subscription will actually be %% performed.
  • %%
  • {true, Item}: Publication operation is allowed, but the %% {@link mod_pubsub:pubsubItem()} record returned replaces the value passed %% in parameter Item. The persistence will be performed by the main %% module.
  • %%
  • {true, done}: Publication operation is allowed, but the %% {@link mod_pubsub:pubsubItem()} will be considered as already stored and %% no further persistence operation will be performed. This case is used, %% when the plugin module is doing the persistence by itself or when it want %% to completely disable persistence.
%%

%%

In the default plugin module, the record is unchanged.

publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, _PubOpts) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), SubState = case SubKey of GenKey -> GenState; _ -> get_state(Nidx, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, Subscribed = case PublishModel of subscribers -> is_subscribed(GenState#pubsub_state.subscriptions) orelse is_subscribed(SubState#pubsub_state.subscriptions); _ -> undefined end, if not ((PublishModel == open) or (PublishModel == publishers) and ((Affiliation == owner) or (Affiliation == publisher) or (Affiliation == publish_only)) or (Subscribed == true)) -> {error, xmpp:err_forbidden()}; true -> if MaxItems > 0; MaxItems == unlimited -> Now = erlang:timestamp(), case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> set_item(OldItem#pubsub_item{ modification = {Now, SubKey}, payload = Payload}), {result, {default, broadcast, []}}; {result, _} -> {error, xmpp:err_forbidden()}; _ -> Items = [ItemId | GenState#pubsub_state.items], {result, {NI, OI}} = remove_extra_items(Nidx, MaxItems, Items), set_state(GenState#pubsub_state{items = NI}), set_item(#pubsub_item{ itemid = {ItemId, Nidx}, nodeidx = Nidx, creation = {Now, GenKey}, modification = {Now, SubKey}, payload = Payload}), {result, {default, broadcast, OI}} end; true -> {result, {default, broadcast, []}} end end. remove_extra_items(Nidx, MaxItems) -> {result, States} = get_states(Nidx), Records = States ++ mnesia:read({pubsub_orphan, Nidx}), ItemIds = lists:flatmap(fun(#pubsub_state{items = Is}) -> Is; (#pubsub_orphan{items = Is}) -> Is end, Records), remove_extra_items(Nidx, MaxItems, ItemIds). %% @doc

This function is used to remove extra items, most notably when the %% maximum number of items has been reached.

%%

This function is used internally by the core PubSub module, as no %% permission check is performed.

%%

In the default plugin module, the oldest items are removed, but other %% rules can be used.

%%

If another PubSub plugin wants to delegate the item removal (and if the %% plugin is using the default pubsub storage), it can implements this function like this: %% ```remove_extra_items(Nidx, MaxItems, ItemIds) -> %% node_default:remove_extra_items(Nidx, MaxItems, ItemIds).'''

remove_extra_items(_Nidx, unlimited, ItemIds) -> {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> NewItems = lists:sublist(ItemIds, MaxItems), OldItems = lists:nthtail(length(NewItems), ItemIds), del_items(Nidx, OldItems), {result, {NewItems, OldItems}}. remove_expired_items(_Nidx, infinity) -> {result, []}; remove_expired_items(Nidx, Seconds) -> Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), ExpT = misc:usec_to_now( erlang:system_time(microsecond) - (Seconds * 1000000)), ExpItems = lists:filtermap( fun(#pubsub_item{itemid = {ItemId, _}, modification = {ModT, _}}) when ModT < ExpT -> {true, ItemId}; (#pubsub_item{}) -> false end, Items), del_items(Nidx, ExpItems), {result, ExpItems}. %% @doc

Triggers item deletion.

%%

Default plugin: The user performing the deletion must be the node owner %% or a publisher, or PublishModel being open.

delete_item(Nidx, Publisher, PublishModel, ItemId) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), #pubsub_state{affiliation = Affiliation, items = Items} = GenState, Allowed = Affiliation == publisher orelse Affiliation == owner orelse (PublishModel == open andalso case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}}} -> true; _ -> false end), if not Allowed -> {error, xmpp:err_forbidden()}; true -> case lists:member(ItemId, Items) of true -> del_item(Nidx, ItemId), set_state(GenState#pubsub_state{items = lists:delete(ItemId, Items)}), {result, {default, broadcast}}; false -> case Affiliation of owner -> {result, States} = get_states(Nidx), Records = States ++ mnesia:read({pubsub_orphan, Nidx}), lists:foldl(fun (#pubsub_state{items = RI} = S, Res) -> case lists:member(ItemId, RI) of true -> NI = lists:delete(ItemId, RI), del_item(Nidx, ItemId), mnesia:write(S#pubsub_state{items = NI}), {result, {default, broadcast}}; false -> Res end; (#pubsub_orphan{items = RI} = S, Res) -> case lists:member(ItemId, RI) of true -> NI = lists:delete(ItemId, RI), del_item(Nidx, ItemId), mnesia:write(S#pubsub_orphan{items = NI}), {result, {default, broadcast}}; false -> Res end end, {error, xmpp:err_item_not_found()}, Records); _ -> {error, xmpp:err_forbidden()} end end end. purge_node(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case GenState of #pubsub_state{affiliation = owner} -> {result, States} = get_states(Nidx), lists:foreach(fun (#pubsub_state{items = []}) -> ok; (#pubsub_state{items = Items} = S) -> del_items(Nidx, Items), set_state(S#pubsub_state{items = []}) end, States), del_orphan_items(Nidx), {result, {default, broadcast}}; _ -> {error, xmpp:err_forbidden()} end. %% @doc

Return the current affiliations for the given user

%%

The default module reads affiliations in the main Mnesia %% pubsub_state table. If a plugin stores its data in the same %% table, it should return an empty list, as the affiliation will be read by %% the default PubSub module. Otherwise, it should return its own affiliation, %% that will be added to the affiliation stored in the main %% pubsub_state table.

get_entity_affiliations(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> case NodeTree:get_node(N) of #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, A} | Acc]; _ -> Acc end end, [], States), {result, Reply}. get_node_affiliations(Nidx) -> {result, States} = get_states(Nidx), Tr = fun (#pubsub_state{stateid = {J, _}, affiliation = A}) -> {J, A} end, {result, lists:map(Tr, States)}. get_affiliation(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), #pubsub_state{affiliation = Affiliation} = get_state(Nidx, GenKey), {result, Affiliation}. set_affiliation(Nidx, Owner, Affiliation) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case {Affiliation, GenState#pubsub_state.subscriptions} of {none, []} -> {result, del_state(GenState)}; _ -> {result, set_state(GenState#pubsub_state{affiliation = Affiliation})} end. %% @doc

Return the current subscriptions for the given user

%%

The default module reads subscriptions in the main Mnesia %% pubsub_state table. If a plugin stores its data in the same %% table, it should return an empty list, as the affiliation will be read by %% the default PubSub module. Otherwise, it should return its own affiliation, %% that will be added to the affiliation stored in the main %% pubsub_state table.

get_entity_subscriptions(Host, Owner) -> {U, D, _} = SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = case SubKey of GenKey -> mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); _ -> mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) ++ mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) end, NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> case NodeTree:get_node(N) of #pubsub_node{nodeid = {Host, _}} = Node -> lists:foldl(fun ({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, J} | Acc2] end, Acc, Ss); _ -> Acc end end, [], States), {result, Reply}. get_node_subscriptions(Nidx) -> {result, States} = get_states(Nidx), Tr = fun (#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> lists:foldl(fun ({S, SubId}, Acc) -> [{J, S, SubId} | Acc] end, [], Subscriptions) end, {result, lists:flatmap(Tr, States)}. get_subscriptions(Nidx, Owner) -> SubKey = jid:tolower(Owner), SubState = get_state(Nidx, SubKey), {result, SubState#pubsub_state.subscriptions}. set_subscriptions(Nidx, Owner, Subscription, SubId) -> SubKey = jid:tolower(Owner), SubState = get_state(Nidx, SubKey), case {SubId, SubState#pubsub_state.subscriptions} of {_, []} -> case Subscription of none -> {error, mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_not_subscribed())}; _ -> new_subscription(Nidx, Owner, Subscription, SubState) end; {<<>>, [{_, SID}]} -> case Subscription of none -> unsub_with_subid(SubState, SID); _ -> replace_subscription({Subscription, SID}, SubState) end; {<<>>, [_ | _]} -> {error, mod_pubsub:extended_error((xmpp:err_bad_request()), mod_pubsub:err_subid_required())}; _ -> case Subscription of none -> unsub_with_subid(SubState, SubId); _ -> replace_subscription({Subscription, SubId}, SubState) end end. replace_subscription(NewSub, SubState) -> NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. replace_subscription(_, [], Acc) -> Acc; replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). new_subscription(_Nidx, _Owner, Sub, SubState) -> %%SubId = pubsub_subscription:add_subscription(Owner, Nidx, []), SubId = pubsub_subscription:make_subid(), Subs = SubState#pubsub_state.subscriptions, set_state(SubState#pubsub_state{subscriptions = [{Sub, SubId} | Subs]}), {result, {Sub, SubId}}. unsub_with_subid(SubState, SubId) -> %%pubsub_subscription:delete_subscription(SubState#pubsub_state.stateid, Nidx, SubId), NewSubs = [{S, Sid} || {S, Sid} <- SubState#pubsub_state.subscriptions, SubId =/= Sid], case {NewSubs, SubState#pubsub_state.affiliation} of {[], none} -> {result, del_state(SubState)}; _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} end. %% @doc

Returns a list of Owner's nodes on Host with pending %% subscriptions.

get_pending_nodes(Host, Owner) -> GenKey = jid:remove_resource(jid:tolower(Owner)), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, affiliation = owner, _ = '_'}), NodeIdxs = [Nidx || #pubsub_state{stateid = {_, Nidx}} <- States], NodeTree = mod_pubsub:tree(Host), Reply = mnesia:foldl(fun (#pubsub_state{stateid = {_, Nidx}} = S, Acc) -> case lists:member(Nidx, NodeIdxs) of true -> case get_nodes_helper(NodeTree, S) of {value, Node} -> [Node | Acc]; false -> Acc end; false -> Acc end end, [], pubsub_state), {result, Reply}. get_nodes_helper(NodeTree, #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> HasPending = fun ({pending, _}) -> true; (pending) -> true; (_) -> false end, case lists:any(HasPending, Subs) of true -> case NodeTree:get_node(N) of #pubsub_node{nodeid = {_, Node}} -> {value, Node}; _ -> false end; false -> false end. %% @doc Returns the list of stored states for a given node. %%

For the default PubSub module, states are stored in Mnesia database.

%%

We can consider that the pubsub_state table have been created by the main %% mod_pubsub module.

%%

PubSub plugins can store the states where they wants (for example in a %% relational database).

%%

If a PubSub plugin wants to delegate the states storage to the default node, %% they can implement this function like this: %% ```get_states(Nidx) -> %% node_default:get_states(Nidx).'''

get_states(Nidx) -> States = case catch mnesia:index_read(pubsub_state, Nidx, #pubsub_state.nodeidx) of List when is_list(List) -> List; _ -> [] end, {result, States}. %% @doc

Returns a state (one state list), given its reference.

get_state(Nidx, Key) -> StateId = {Key, Nidx}, case catch mnesia:read({pubsub_state, StateId}) of [State] when is_record(State, pubsub_state) -> State; _ -> #pubsub_state{stateid = StateId, nodeidx = Nidx} end. %% @doc

Write a state into database.

set_state(State) when is_record(State, pubsub_state) -> mnesia:write(State). %set_state(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. %% @doc

Delete a state from database.

del_state(#pubsub_state{stateid = {Key, Nidx}, items = Items}) -> case Items of [] -> ok; _ -> Orphan = #pubsub_orphan{nodeid = Nidx, items = case mnesia:read({pubsub_orphan, Nidx}) of [#pubsub_orphan{items = ItemIds}] -> lists:usort(ItemIds++Items); _ -> Items end}, mnesia:write(Orphan) end, mnesia:delete({pubsub_state, {Key, Nidx}}). %% @doc Returns the list of stored items for a given node. %%

For the default PubSub module, items are stored in Mnesia database.

%%

We can consider that the pubsub_item table have been created by the main %% mod_pubsub module.

%%

PubSub plugins can store the items where they wants (for example in a %% relational database), or they can even decide not to persist any items.

get_items(Nidx, _From, undefined) -> RItems = lists:keysort(#pubsub_item.creation, mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)), {result, {RItems, undefined}}; get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, 'after' = After, before = Before}) -> case lists:keysort(#pubsub_item.creation, mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx)) of [] -> {result, {[], #rsm_set{count = 0}}}; RItems -> Count = length(RItems), Limit = case Max of undefined -> ?MAXITEMS; _ -> Max end, {Offset, ItemsPage} = case {IncIndex, Before, After} of {undefined, undefined, undefined} -> {0, lists:sublist(RItems, Limit)}; {I, undefined, undefined} -> SubList = lists:nthtail(I, RItems), {I, lists:sublist(SubList, Limit)}; {_, <<>>, undefined} -> %% 2.5 Requesting the Last Page in a Result Set SubList = lists:reverse(RItems), {Count-Limit, lists:reverse(lists:sublist(SubList, Limit))}; {_, Stamp, undefined} -> BeforeNow = encode_stamp(Stamp), {NewIndex, SubList} = extract_sublist(before_now, BeforeNow, 0, lists:reverse(RItems)), {Count-NewIndex-Limit, lists:reverse(lists:sublist(SubList, Limit))}; {_, undefined, Stamp} -> AfterNow = encode_stamp(Stamp), {NewIndex, SubList} = extract_sublist(after_now, AfterNow, 0, RItems), {NewIndex, lists:sublist(SubList, Limit)} end, Rsm = rsm_page(Count, IncIndex, Offset, ItemsPage), {result, {ItemsPage, Rsm}} end. get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), SubState = get_state(Nidx, SubKey), Affiliation = GenState#pubsub_state.affiliation, BareSubscriptions = GenState#pubsub_state.subscriptions, FullSubscriptions = SubState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, BareSubscriptions) orelse can_fetch_item(Affiliation, FullSubscriptions), if %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; (AccessModel == presence) and not PresenceSubscription -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and not RosterGroup -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and not Whitelisted -> {error, mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; (AccessModel == authorize) and not Whitelisted -> {error, xmpp:err_forbidden()}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_items(Nidx, JID, RSM) end. extract_sublist(A, Now, Index, [#pubsub_item{creation = {Creation, _}} | RItems]) when ((A == before_now) and (Creation >= Now)) or ((A == after_now) and (Creation =< Now)) -> extract_sublist(A, Now, Index+1, RItems); extract_sublist(_, _, Index, RItems) -> {Index, RItems}. get_only_item(Nidx, From) -> get_last_items(Nidx, From, 1). get_last_items(Nidx, _From, Count) when Count > 0 -> Items = mnesia:index_read(pubsub_item, Nidx, #pubsub_item.nodeidx), LastItems = lists:reverse(lists:keysort(#pubsub_item.modification, Items)), {result, lists:sublist(LastItems, Count)}; get_last_items(_Nidx, _From, _Count) -> {result, []}. %% @doc

Returns an item (one item list), given its reference.

get_item(Nidx, ItemId) -> case mnesia:read({pubsub_item, {ItemId, Nidx}}) of [Item] when is_record(Item, pubsub_item) -> {result, Item}; _ -> {error, xmpp:err_item_not_found()} end. get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), Affiliation = GenState#pubsub_state.affiliation, Subscriptions = GenState#pubsub_state.subscriptions, Whitelisted = can_fetch_item(Affiliation, Subscriptions), if %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, mod_pubsub:extended_error(xmpp:err_bad_request(), "subid-required")}; %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, mod_pubsub:extended_error(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; (AccessModel == presence) and not PresenceSubscription -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and not RosterGroup -> {error, mod_pubsub:extended_error((xmpp:err_not_authorized()), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and not Whitelisted -> {error, mod_pubsub:extended_error((xmpp:err_not_allowed()), mod_pubsub:err_closed_node())}; (AccessModel == authorize) and not Whitelisted -> {error, xmpp:err_forbidden()}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_item(Nidx, ItemId) end. %% @doc

Write an item into database.

set_item(Item) when is_record(Item, pubsub_item) -> mnesia:write(Item). %set_item(_) -> {error, ?ERR_INTERNAL_SERVER_ERROR}. %% @doc

Delete an item from database.

del_item(Nidx, ItemId) -> mnesia:delete({pubsub_item, {ItemId, Nidx}}). del_items(Nidx, ItemIds) -> lists:foreach(fun (ItemId) -> del_item(Nidx, ItemId) end, ItemIds). del_orphan_items(Nidx) -> case mnesia:read({pubsub_orphan, Nidx}) of [#pubsub_orphan{items = ItemIds}] -> del_items(Nidx, ItemIds), mnesia:delete({pubsub_orphan, Nidx}); _ -> ok end. get_item_name(_Host, _Node, Id) -> {result, Id}. %% @doc

Return the path of the node. In flat it's just node id.

node_to_path(Node) -> {result, [Node]}. path_to_node(Path) -> {result, case Path of %% default slot [Node] -> iolist_to_binary(Node); %% handle old possible entries, used when migrating database content to new format [Node | _] when is_binary(Node) -> iolist_to_binary(str:join([<<"">> | Path], <<"/">>)); %% default case (used by PEP for example) _ -> iolist_to_binary(Path) end}. can_fetch_item(owner, _) -> true; can_fetch_item(member, _) -> true; can_fetch_item(publisher, _) -> true; can_fetch_item(publish_only, _) -> false; can_fetch_item(outcast, _) -> false; can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions). %can_fetch_item(_Affiliation, _Subscription) -> false. is_subscribed(Subscriptions) -> lists:any(fun ({subscribed, _SubId}) -> true; (_) -> false end, Subscriptions). first_in_list(_Pred, []) -> false; first_in_list(Pred, [H | T]) -> case Pred(H) of true -> {value, H}; _ -> first_in_list(Pred, T) end. rsm_page(Count, _, _, []) -> #rsm_set{count = Count}; rsm_page(Count, Index, Offset, Items) -> FirstItem = hd(Items), LastItem = lists:last(Items), First = decode_stamp(element(1, FirstItem#pubsub_item.creation)), Last = decode_stamp(element(1, LastItem#pubsub_item.creation)), #rsm_set{count = Count, index = Index, first = #rsm_first{index = Offset, data = First}, last = Last}. encode_stamp(Stamp) -> try xmpp_util:decode_timestamp(Stamp) catch _:{bad_timestamp, _} -> Stamp % We should return a proper error to the client instead. end. decode_stamp(Stamp) -> xmpp_util:encode_timestamp(Stamp). transform({pubsub_state, {Id, Nidx}, Is, A, Ss}) -> {pubsub_state, {Id, Nidx}, Nidx, Is, A, Ss}; transform({pubsub_item, {Id, Nidx}, C, M, P}) -> {pubsub_item, {Id, Nidx}, Nidx, C, M, P}; transform(Rec) -> Rec. ejabberd-21.12/src/ELDAPv3.asn1db0000644000232200023220000010613414154362354016616 0ustar debalancedebalancecXM $bWLAhhd compresseddfalsehdmemorybhdownergd nonode@nohostwhdheirdnonehdnamed asn1_ELDAPv3hdsizea9hdnoded nonode@nohosthd named_tabledfalsehdtypedsethdkeyposahd protectiond protectedhd major_versionahd minor_versionahd extended_infojbWLAhdMatchingRuleAssertionhdtypedefdtrueadMatchingRuleAssertionhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead matchingRulehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajahd ComponentTypeadtypehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajahd ComponentTypead matchValuehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajahd ComponentTypead dnAttributeshdtypelhdtagdCONTEXTadIMPLICITajdBOOLEANjjdnohdDEFAULTdfalselhdCONTEXTajajjjdnobWLAhdAttributeValueAssertionhdtypedefdtruea?dAttributeValueAssertionhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypea@d attributeDeschdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeaAdassertionValuehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajajjjdnobWLAhdAttributeDescriptionhdtypedefdtruea0dAttributeDescriptionhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobbWLAhdEXTERNALhdtypedefdfalsed undefineddEXTERNALhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefinedddirect-referencehdtypejdOBJECT IDENTIFIERjjdnodOPTIONALlhd UNIVERSALajd undefinedhd ComponentTyped undefineddindirect-referencehdtypejdINTEGERjjdnodOPTIONALlhd UNIVERSALajd undefinedhd ComponentTyped undefinedddata-value-descriptorhdtypejdObjectDescriptorjjdnodOPTIONALd undefinedd undefinedhd ComponentTyped undefineddencodinghdtypejhdCHOICElhd ComponentTyped undefineddsingle-ASN1-typehdtypelhdtagdCONTEXTadEXPLICITa jdANYjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTyped undefinedd octet-alignedhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTyped undefinedd arbitraryhdtypelhdtagdCONTEXTadIMPLICITajhd BIT STRINGjjjdnod mandatorylhdCONTEXTajd undefinedjjjdnod mandatoryd undefinedd undefinedjjjdnobWLAhdReferralhdtypedefdtrueadReferralhdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnotbWLAhdLDAPURLhdtypedefdtrueadLDAPURLhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobWLAhdAttributeValuehdtypedefdtruea=dAttributeValuehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobWLAhdExtendedResponsehdtypedefdtruebdExtendedResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeaLd resultCodehdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDl'hdsuccessahdoperationsErrorahd protocolErrorahdtimeLimitExceededahdsizeLimitExceededahd compareFalseahd compareTrueahdauthMethodNotSupportedahdstrongAuthRequiredahdreferrala hdadminLimitExceededa hdunavailableCriticalExtensiona hdconfidentialityRequireda hdsaslBindInProgressahdnoSuchAttributeahdundefinedAttributeTypeahdinappropriateMatchingahdconstraintViolationahdattributeOrValueExistsahdinvalidAttributeSyntaxahd noSuchObjecta hd aliasProblema!hdinvalidDNSyntaxa"hdaliasDereferencingProblema$hdinappropriateAuthenticationa0hdinvalidCredentialsa1hdinsufficientAccessRightsa2hdbusya3hd unavailablea4hdunwillingToPerforma5hd loopDetecta6hdnamingViolationa@hdobjectClassViolationaAhdnotAllowedOnNonLeafaBhdnotAllowedOnRDNaChdentryAlreadyExistsaDhdobjectClassModsProhibitedaEhdaffectsMultipleDSAsaGhdotheraPjjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead matchedDNhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead errorMessagehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadreferralhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenceadELDAPv3dReferraljjdnodOPTIONALlhdCONTEXTajahd ComponentTypebd responseNamehdtypelhdtagdCONTEXTa dIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTa jahd ComponentTypebdresponsehdtypelhdtagdCONTEXTa dIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTa jajjjdnobWLAhdModifyDNResponsehdtypedefdtrueb dModifyDNResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenceb dELDAPv3d LDAPResultjjdnobWLAhd AttributeTypehdtypedefdtruea.d AttributeTypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnoZbWLAhdPasswdModifyRequestValuehdtypedefdtrueb#dPasswdModifyRequestValuehdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeb$d userIdentityhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajahd ComponentTypeb%d oldPasswdhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajahd ComponentTypeb&d newPasswdhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdCompareResponsehdtypedefdtruebdCompareResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferencebdELDAPv3d LDAPResultjjdno|bWLAhd DelRequesthdtypedefdtruead DelRequesthdtypelhdtagd APPLICATIONa dIMPLICITajd OCTET STRINGjjdno;bWLAhd AttributeListhdtypedefdtruead AttributeListhdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadtypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadvalshdtypelhdtagd UNIVERSALadIMPLICITa jhdSET OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnod mandatorylhd UNIVERSALajajjjdnojjdnobWLAhd AddRequesthdtypedefdtruead AddRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadentryhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead attributeshdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d AttributeListjjdnod mandatorylhd UNIVERSALajajjjdnoqbWLAhd BindRequesthdtypedefdtruead BindRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadversionhdtypelhdtagd UNIVERSALadIMPLICITajdINTEGERlhd ValueRangehaajjdnod mandatorylhd UNIVERSALajahd ComponentTypeadnamehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadauthenticationhdtypejhdExternaltypereferenced undefineddELDAPv3dAuthenticationChoicejjdnod mandatorylhdCONTEXTahdCONTEXTajajjjdnobWLAhdAttributeDescriptionListhdtypedefdtruea:dAttributeDescriptionListhdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnobWLAhdSubstringFilterhdtypedefdtrueadSubstringFilterhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadtypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead substringshdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypejhdCHOICElhd ComponentTypeadinitialhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadanyhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadfinalhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedjjjdnojjdnod mandatorylhd UNIVERSALajajjjdnozbWLAhd LDAPStringhdtypedefdtruea&d LDAPStringhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnoEbWLAhdPasswdModifyResponseValuehdtypedefdtrueb(dPasswdModifyResponseValuehdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeb)d genPasswdhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdSearchResultDonehdtypedefdtrueadSearchResultDonehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d LDAPResultjjdnobWLAhdAssertionValuehdtypedefdtrueaCdAssertionValuehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobWLAhdAbandonRequesthdtypedefdtruebdAbandonRequesthdtypelhdtagd APPLICATIONadIMPLICITajdINTEGERlhd ValueRangehabjjdnobWLAhdCompareRequesthdtypedefdtruebdCompareRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypebdentryhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypebdavahdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferencebdELDAPv3dAttributeValueAssertionjjdnod mandatorylhd UNIVERSALajajjjdnozbWLAhd UnbindRequesthdtypedefdtruead UnbindRequesthdtypelhdtagd APPLICATIONadIMPLICITajdNULLjjdnobWLAhd SearchRequesthdtypedefdtruead SearchRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead baseObjecthdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadscopehdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDlhd baseObjectahd singleLevelahd wholeSubtreeajjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead derefAliaseshdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDlhdneverDerefAliasesahdderefInSearchingahdderefFindingBaseObjahd derefAlwaysajjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead sizeLimithdtypelhdtagd UNIVERSALadIMPLICITajdINTEGERlhd ValueRangehabjjdnod mandatorylhd UNIVERSALajahd ComponentTypead timeLimithdtypelhdtagd UNIVERSALadIMPLICITajdINTEGERlhd ValueRangehabjjdnod mandatorylhd UNIVERSALajahd ComponentTypead typesOnlyhdtypelhdtagd UNIVERSALadIMPLICITajdBOOLEANjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadfilterhdtypejhdExternaltypereferenced undefineddELDAPv3dFilterjjdnod mandatoryl hdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTahdCONTEXTa jahd ComponentTypead attributeshdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dAttributeDescriptionListjjdnod mandatorylhd UNIVERSALajajjjdnoZbWLAhdTYPE-IDENTIFIERhdclassdefdtrued undefineddTYPE-IDENTIFIERhd objectclasslhdfixedtypevaluefielddidhdtypehdtagd UNIVERSALadIMPLICITadOBJECT IDENTIFIERjjdnodUNIQUEd MANDATORYhd typefielddTyped MANDATORYjhd WITH SYNTAXlhdtypefieldreferencedTyped IDENTIFIEDdBYhdvaluefieldreferencedidjAbWLAhdControlhdtypedefdtrueadControlhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead controlTypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead criticalityhdtypelhdtagd UNIVERSALadIMPLICITajdBOOLEANjjdnohdDEFAULTdfalselhd UNIVERSALajahd ComponentTypead controlValuehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhd UNIVERSALajajjjdnobWLAhdSearchResultEntryhdtypedefdtrueadSearchResultEntryhdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead objectNamehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead attributeshdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dPartialAttributeListjjdnod mandatorylhd UNIVERSALajajjjdnoHbWLAhd EMBEDDED PDVhdtypedefdfalsed undefinedd EMBEDDED PDVhdtypelhdtagd UNIVERSALa dIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddidentificationhdtypejhdCHOICElhd ComponentTyped undefineddsyntaxeshdtypejhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddabstracthdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransferhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddsyntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddpresentation-context-idhdtypejdINTEGERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddcontext-negotiationhdtypejhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddpresentation-context-idhdtypejdINTEGERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransfer-syntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransfer-syntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddfixedhdtypejdNULLjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefinedd data-valuehdtypejd OCTET STRINGjjdnod mandatoryd undefinedd undefinedjjjdnobWLAhdModifyResponsehdtypedefdtrueadModifyResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenceadELDAPv3d LDAPResultjjdnobWLAhdSearchResultReferencehdtypedefdtrueadSearchResultReferencehdtypelhdtagd APPLICATIONadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnobWLAhd DelResponsehdtypedefdtruebd DelResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferencebdELDAPv3d LDAPResultjjdnobWLAhd AddResponsehdtypedefdtruead AddResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenceadELDAPv3d LDAPResultjjdno2bWLAhd BindResponsehdtypedefdtruead BindResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeaLd resultCodehdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDl'hdsuccessahdoperationsErrorahd protocolErrorahdtimeLimitExceededahdsizeLimitExceededahd compareFalseahd compareTrueahdauthMethodNotSupportedahdstrongAuthRequiredahdreferrala hdadminLimitExceededa hdunavailableCriticalExtensiona hdconfidentialityRequireda hdsaslBindInProgressahdnoSuchAttributeahdundefinedAttributeTypeahdinappropriateMatchingahdconstraintViolationahdattributeOrValueExistsahdinvalidAttributeSyntaxahd noSuchObjecta hd aliasProblema!hdinvalidDNSyntaxa"hdaliasDereferencingProblema$hdinappropriateAuthenticationa0hdinvalidCredentialsa1hdinsufficientAccessRightsa2hdbusya3hd unavailablea4hdunwillingToPerforma5hd loopDetecta6hdnamingViolationa@hdobjectClassViolationaAhdnotAllowedOnNonLeafaBhdnotAllowedOnRDNaChdentryAlreadyExistsaDhdobjectClassModsProhibitedaEhdaffectsMultipleDSAsaGhdotheraPjjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead matchedDNhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead errorMessagehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadreferralhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dReferraljjdnodOPTIONALlhdCONTEXTajahd ComponentTypeadserverSaslCredshdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdAuthenticationChoicehdtypedefdtrueadAuthenticationChoicehdtypejhdCHOICElhd ComponentTypeadsimplehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadsaslhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dSaslCredentialsjjdnod mandatorylhdCONTEXTajd undefinedjjjdnobWLAhd AttributehdtypedefdtrueaEd Attributehdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeaFdtypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeaGdvalshdtypelhdtagd UNIVERSALadIMPLICITa jhdSET OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnod mandatorylhd UNIVERSALajajjjdnoIbWLAhdPartialAttributeListhdtypedefdtrueadPartialAttributeListhdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadtypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadvalshdtypelhdtagd UNIVERSALadIMPLICITa jhdSET OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnod mandatorylhd UNIVERSALajajjjdnojjdnobWLAhd LDAPResulthdtypedefdtrueaKd LDAPResulthdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeaLd resultCodehdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDl'hdsuccessahdoperationsErrorahd protocolErrorahdtimeLimitExceededahdsizeLimitExceededahd compareFalseahd compareTrueahdauthMethodNotSupportedahdstrongAuthRequiredahdreferrala hdadminLimitExceededa hdunavailableCriticalExtensiona hdconfidentialityRequireda hdsaslBindInProgressahdnoSuchAttributeahdundefinedAttributeTypeahdinappropriateMatchingahdconstraintViolationahdattributeOrValueExistsahdinvalidAttributeSyntaxahd noSuchObjecta hd aliasProblema!hdinvalidDNSyntaxa"hdaliasDereferencingProblema$hdinappropriateAuthenticationa0hdinvalidCredentialsa1hdinsufficientAccessRightsa2hdbusya3hd unavailablea4hdunwillingToPerforma5hd loopDetecta6hdnamingViolationa@hdobjectClassViolationaAhdnotAllowedOnNonLeafaBhdnotAllowedOnRDNaChdentryAlreadyExistsaDhdobjectClassModsProhibitedaEhdaffectsMultipleDSAsaGhdotheraPjjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead matchedDNhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead errorMessagehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadreferralhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenceadELDAPv3dReferraljjdnodOPTIONALlhdCONTEXTajajjjdnoUbWLAhdmaxInthdvaluedefdtruea$dmaxInthdtypejdINTEGERjjdnobdELDAPv3bWLAhd ModifyRequesthdtypedefdtruead ModifyRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadobjecthdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead modificationhdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead operationhdtypelhdtagd UNIVERSALa dIMPLICITajhd ENUMERATEDlhdaddahddeleteahdreplaceajjjdnod mandatorylhd UNIVERSALa jahd ComponentTypead modificationhdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dAttributeTypeAndValuesjjdnod mandatorylhd UNIVERSALajajjjdnojjdnod mandatorylhd UNIVERSALajajjjdnobWLAhd MessageIDhdtypedefdtruea"d MessageIDhdtypelhdtagd UNIVERSALadIMPLICITajdINTEGERlhd ValueRangehabjjdnobWLAhdAttributeTypeAndValueshdtypedefdtrueadAttributeTypeAndValueshdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypeadtypehdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypeadvalshdtypelhdtagd UNIVERSALadIMPLICITa jhdSET OFhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnojjdnod mandatorylhd UNIVERSALajajjjdnobWLAhdControlshdtypedefdtrueadControlshdtypelhdtagd UNIVERSALadIMPLICITa jhd SEQUENCE OFhdtypelhdtagd UNIVERSALadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dControljjdnojjdnobWLAhdSaslCredentialshdtypedefdtrueadSaslCredentialshdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypead mechanismhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypead credentialshdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhd UNIVERSALajajjjdnobWLAhdRelativeLDAPDNhdtypedefdtruea,dRelativeLDAPDNhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnorbWLAhdLDAPDNhdtypedefdtruea*dLDAPDNhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobWLAhdABSTRACT-SYNTAXhdclassdefdtrued undefineddABSTRACT-SYNTAXhd objectclasslhdfixedtypevaluefielddidhdtypehdtagd UNIVERSALadIMPLICITadOBJECT IDENTIFIERjjdnodUNIQUEd MANDATORYhd typefielddTyped MANDATORYhdfixedtypevaluefielddpropertyhdtypehdtagd UNIVERSALadIMPLICITahd BIT STRINGjjjdnod undefinedhdDEFAULTkjhd WITH SYNTAXlhdtypefieldreferencedTyped IDENTIFIEDdBYhdvaluefieldreferencedidldHASdPROPERTYhdvaluefieldreferencedpropertyjjRbWLAhdCHARACTER STRINGhdtypedefdfalsed undefineddCHARACTER STRINGhdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddidentificationhdtypejhdCHOICElhd ComponentTyped undefineddsyntaxeshdtypejhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddabstracthdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransferhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddsyntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddpresentation-context-idhdtypejdINTEGERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddcontext-negotiationhdtypejhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTyped undefineddpresentation-context-idhdtypejdINTEGERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransfer-syntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddtransfer-syntaxhdtypejdOBJECT IDENTIFIERjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefineddfixedhdtypejdNULLjjdnod mandatoryd undefinedd undefinedjjjdnod mandatoryd undefinedd undefinedhd ComponentTyped undefinedd string-valuehdtypejd OCTET STRINGjjdnod mandatoryd undefinedd undefinedjjjdnoabWLAhd LDAPMessagehdtypedefdtruea d LDAPMessagehdtypelhdtagd UNIVERSALadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypea d messageIDhdtypelhdtagd UNIVERSALadIMPLICITajdINTEGERlhd ValueRangehabjjdnod mandatorylhd UNIVERSALajahd ComponentTypea d protocolOphdtypejhdCHOICElhd ComponentTypea d bindRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d BindRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypea d bindResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d BindResponsejjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead unbindRequesthdtypelhdtagd APPLICATIONadIMPLICITajdNULLjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead searchRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d SearchRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypeadsearchResEntryhdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dSearchResultEntryjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead searchResDonehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dSearchResultDonejjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead searchResRefhdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dSearchResultReferencejjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead modifyRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d ModifyRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypeadmodifyResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dModifyResponsejjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead addRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d AddRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead addResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d AddResponsejjdnod mandatorylhd APPLICATIONa jd undefinedhd ComponentTypead delRequesthdtypelhdtagd APPLICATIONa dIMPLICITajd OCTET STRINGjjdnod mandatorylhd APPLICATIONa jd undefinedhd ComponentTypead delResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenced undefineddELDAPv3d DelResponsejjdnod mandatorylhd APPLICATIONa jd undefinedhd ComponentTypead modDNRequesthdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dModifyDNRequestjjdnod mandatorylhd APPLICATIONa jd undefinedhd ComponentTypead modDNResponsehdtypelhdtagd APPLICATIONa dIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dModifyDNResponsejjdnod mandatorylhd APPLICATIONa jd undefinedhd ComponentTypeadcompareRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dCompareRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypeadcompareResponsehdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dCompareResponsejjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypeadabandonRequesthdtypelhdtagd APPLICATIONadIMPLICITajdINTEGERlhd ValueRangehabjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead extendedReqhdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dExtendedRequestjjdnod mandatorylhd APPLICATIONajd undefinedhd ComponentTypead extendedResphdtypelhdtagd APPLICATIONadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dExtendedResponsejjdnod mandatorylhd APPLICATIONajd undefinedjjjdnod mandatorylhd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONa hd APPLICATIONa hd APPLICATIONa hd APPLICATIONa hd APPLICATIONa hd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONahd APPLICATIONajahd ComponentTypea dcontrolshdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dControlsjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdMODULEh dmoduleadELDAPv3jdIMPLICIThdexportsdallhdimportsjd undefinedhl1d LDAPMessaged MessageIDd LDAPStringdLDAPOIDdLDAPDNdRelativeLDAPDNd AttributeTypedAttributeDescriptiondAttributeDescriptionListdAttributeValuedAttributeValueAssertiondAssertionValued AttributedMatchingRuleIdd LDAPResultdReferraldLDAPURLdControlsdControld BindRequestdAuthenticationChoicedSaslCredentialsd BindResponsed UnbindRequestd SearchRequestdFilterdSubstringFilterdMatchingRuleAssertiondSearchResultEntrydPartialAttributeListdSearchResultReferencedSearchResultDoned ModifyRequestdAttributeTypeAndValuesdModifyResponsed AddRequestd AttributeListd AddResponsed DelRequestd DelResponsedModifyDNRequestdModifyDNResponsedCompareRequestdCompareResponsedAbandonRequestdExtendedRequestdExtendedResponsedPasswdModifyRequestValuedPasswdModifyResponseValuejldmaxIntdpasswdModifyOIDjjjjjbWLAhdExtendedRequesthdtypedefdtruebdExtendedRequesthdtypelhdtagd APPLICATIONadIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypebd requestNamehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajahd ComponentTypebd requestValuehdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdModifyDNRequesthdtypedefdtruebdModifyDNRequesthdtypelhdtagd APPLICATIONa dIMPLICITa jhdSEQUENCEdfalsedfalsed undefinedlhd ComponentTypebdentryhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypebdnewrdnhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnod mandatorylhd UNIVERSALajahd ComponentTypebd deleteoldrdnhdtypelhdtagd UNIVERSALadIMPLICITajdBOOLEANjjdnod mandatorylhd UNIVERSALajahd ComponentTypebd newSuperiorhdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnodOPTIONALlhdCONTEXTajajjjdnobWLAhdMatchingRuleIdhdtypedefdtrueaIdMatchingRuleIdhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnotbWLAhdLDAPOIDhdtypedefdtruea(dLDAPOIDhdtypelhdtagd UNIVERSALadIMPLICITajd OCTET STRINGjjdnobWLAhdpasswdModifyOIDhdvaluedefdtrueb!dpasswdModifyOIDhdtypejhdExternaltypereferenceb!dELDAPv3dLDAPOIDjjdnok1.3.6.1.4.1.4203.1.11.1dELDAPv3bWLAhdFilterhdtypedefdtrueadFilterhdtypejhdCHOICEl hd ComponentTypeadandhdtypelhdtagdCONTEXTadIMPLICITa jhdSET OFhdtypejhdExternaltypereferenced undefineddELDAPv3dFilterjjdnojjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadorhdtypelhdtagdCONTEXTadIMPLICITa jhdSET OFhdtypejhdExternaltypereferenceadELDAPv3dFilterjjdnojjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadnothdtypelhdtagdCONTEXTadEXPLICITa jhdExternaltypereferenceadELDAPv3dFilterjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypead equalityMatchhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dAttributeValueAssertionjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypead substringshdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dSubstringFilterjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadgreaterOrEqualhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenceadELDAPv3dAttributeValueAssertionjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypead lessOrEqualhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenceadELDAPv3dAttributeValueAssertionjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadpresenthdtypelhdtagdCONTEXTadIMPLICITajd OCTET STRINGjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypead approxMatchhdtypelhdtagdCONTEXTadIMPLICITa jhdExternaltypereferenceadELDAPv3dAttributeValueAssertionjjdnod mandatorylhdCONTEXTajd undefinedhd ComponentTypeadextensibleMatchhdtypelhdtagdCONTEXTa dIMPLICITa jhdExternaltypereferenced undefineddELDAPv3dMatchingRuleAssertionjjdnod mandatorylhdCONTEXTa jd undefinedjjjdnoejabberd-21.12/src/ejabberd_logger.erl0000644000232200023220000003016514154362354020227 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_logger.erl %%% Author : Evgeniy Khramtsov %%% Purpose : ejabberd logger wrapper %%% Created : 12 May 2013 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2013-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%%------------------------------------------------------------------- -module(ejabberd_logger). -compile({no_auto_import, [get/0]}). %% API -export([start/0, get/0, set/1, get_log_path/0, flush/0]). -export([convert_loglevel/1, loglevels/0]). -ifndef(LAGER). -export([progress_filter/2]). -endif. %% Deprecated functions -export([restart/0, reopen_log/0, rotate_log/0]). -deprecated([{restart, 0}, {reopen_log, 0}, {rotate_log, 0}]). -type loglevel() :: none | emergency | alert | critical | error | warning | notice | info | debug. -define(is_loglevel(L), ((L == none) or (L == emergency) or (L == alert) or (L == critical) or (L == error) or (L == warning) or (L == notice) or (L == info) or (L == debug))). -export_type([loglevel/0]). %%%=================================================================== %%% API %%%=================================================================== -spec get_log_path() -> string(). get_log_path() -> case ejabberd_config:env_binary_to_list(ejabberd, log_path) of {ok, Path} -> Path; undefined -> case os:getenv("EJABBERD_LOG_PATH") of false -> "ejabberd.log"; Path -> Path end end. -spec loglevels() -> [loglevel(), ...]. loglevels() -> [none, emergency, alert, critical, error, warning, notice, info, debug]. -spec convert_loglevel(0..5) -> loglevel(). convert_loglevel(0) -> none; convert_loglevel(1) -> critical; convert_loglevel(2) -> error; convert_loglevel(3) -> warning; convert_loglevel(4) -> info; convert_loglevel(5) -> debug. quiet_mode() -> case application:get_env(ejabberd, quiet) of {ok, true} -> true; _ -> false end. -spec get_integer_env(atom(), T) -> T. get_integer_env(Name, Default) -> case application:get_env(ejabberd, Name) of {ok, I} when is_integer(I), I>=0 -> I; {ok, infinity} -> infinity; undefined -> Default; {ok, Junk} -> error_logger:error_msg("wrong value for ~ts: ~p; " "using ~p as a fallback~n", [Name, Junk, Default]), Default end. -ifdef(LAGER). -spec get_string_env(atom(), T) -> T. get_string_env(Name, Default) -> case application:get_env(ejabberd, Name) of {ok, L} when is_list(L) -> L; undefined -> Default; {ok, Junk} -> error_logger:error_msg("wrong value for ~ts: ~p; " "using ~p as a fallback~n", [Name, Junk, Default]), Default end. -spec start() -> ok. start() -> start(info). start(Level) -> StartedApps = application:which_applications(5000), case lists:keyfind(logger, 1, StartedApps) of %% Elixir logger is started. We assume everything is in place %% to use lager to Elixir logger bridge. {logger, _, _} -> error_logger:info_msg("Ignoring ejabberd logger options, using Elixir Logger.", []), %% Do not start lager, we rely on Elixir Logger do_start_for_logger(Level); _ -> do_start(Level) end. do_start_for_logger(Level) -> application:load(sasl), application:set_env(sasl, sasl_error_logger, false), application:load(lager), application:set_env(lager, error_logger_redirect, false), application:set_env(lager, error_logger_whitelist, ['Elixir.Logger.ErrorHandler']), application:set_env(lager, crash_log, false), application:set_env(lager, handlers, [{elixir_logger_backend, [{level, Level}]}]), ejabberd:start_app(lager), ok. do_start(Level) -> application:load(sasl), application:set_env(sasl, sasl_error_logger, false), application:load(lager), ConsoleLog = get_log_path(), Dir = filename:dirname(ConsoleLog), ErrorLog = filename:join([Dir, "error.log"]), CrashLog = filename:join([Dir, "crash.log"]), LogRotateDate = get_string_env(log_rotate_date, ""), LogRotateSize = case get_integer_env(log_rotate_size, 10*1024*1024) of infinity -> 0; V -> V end, LogRotateCount = get_integer_env(log_rotate_count, 1), LogRateLimit = get_integer_env(log_rate_limit, 100), ConsoleLevel0 = case quiet_mode() of true -> critical; _ -> Level end, ConsoleLevel = case get_lager_version() >= "3.6.0" of true -> [{level, ConsoleLevel0}]; false -> ConsoleLevel0 end, application:set_env(lager, error_logger_hwm, LogRateLimit), application:set_env( lager, handlers, [{lager_console_backend, ConsoleLevel}, {lager_file_backend, [{file, ConsoleLog}, {level, Level}, {date, LogRotateDate}, {count, LogRotateCount}, {size, LogRotateSize}]}, {lager_file_backend, [{file, ErrorLog}, {level, error}, {date, LogRotateDate}, {count, LogRotateCount}, {size, LogRotateSize}]}]), application:set_env(lager, crash_log, CrashLog), application:set_env(lager, crash_log_date, LogRotateDate), application:set_env(lager, crash_log_size, LogRotateSize), application:set_env(lager, crash_log_count, LogRotateCount), ejabberd:start_app(lager), lists:foreach(fun(Handler) -> lager:set_loghwm(Handler, LogRateLimit) end, gen_event:which_handlers(lager_event)). -spec restart() -> ok. restart() -> Level = ejabberd_option:loglevel(), application:stop(lager), start(Level). -spec reopen_log() -> ok. reopen_log() -> ok. -spec rotate_log() -> ok. rotate_log() -> catch lager_crash_log ! rotate, lists:foreach( fun({lager_file_backend, File}) -> whereis(lager_event) ! {rotate, File}; (_) -> ok end, gen_event:which_handlers(lager_event)). -spec get() -> loglevel(). get() -> Handlers = get_lager_handlers(), lists:foldl(fun(lager_console_backend, _Acc) -> lager:get_loglevel(lager_console_backend); (elixir_logger_backend, _Acc) -> lager:get_loglevel(elixir_logger_backend); (_, Acc) -> Acc end, none, Handlers). -spec set(0..5 | loglevel()) -> ok. set(N) when is_integer(N), N>=0, N=<5 -> set(convert_loglevel(N)); set(Level) when ?is_loglevel(Level) -> case get() of Level -> ok; _ -> ConsoleLog = get_log_path(), QuietMode = quiet_mode(), lists:foreach( fun({lager_file_backend, File} = H) when File == ConsoleLog -> lager:set_loglevel(H, Level); (lager_console_backend = H) when not QuietMode -> lager:set_loglevel(H, Level); (elixir_logger_backend = H) -> lager:set_loglevel(H, Level); (_) -> ok end, get_lager_handlers()) end, case Level of debug -> xmpp:set_config([{debug, true}]); _ -> xmpp:set_config([{debug, false}]) end. get_lager_handlers() -> case catch gen_event:which_handlers(lager_event) of {'EXIT',noproc} -> []; Result -> Result end. -spec get_lager_version() -> string(). get_lager_version() -> Apps = application:loaded_applications(), case lists:keyfind(lager, 1, Apps) of {_, _, Vsn} -> Vsn; false -> "0.0.0" end. -spec flush() -> ok. flush() -> application:stop(lager), application:stop(sasl). -else. -include_lib("kernel/include/logger.hrl"). -spec start() -> ok | {error, term()}. start() -> start(info). start(Level) -> EjabberdLog = get_log_path(), Dir = filename:dirname(EjabberdLog), ErrorLog = filename:join([Dir, "error.log"]), LogRotateSize = get_integer_env(log_rotate_size, 10*1024*1024), LogRotateCount = get_integer_env(log_rotate_count, 1), Config = #{max_no_bytes => LogRotateSize, max_no_files => LogRotateCount, filesync_repeat_interval => no_repeat, file_check => 1000, sync_mode_qlen => 1000, drop_mode_qlen => 1000, flush_qlen => 5000}, FmtConfig = #{legacy_header => false, time_designator => $ , max_size => 100*1024, single_line => false}, FileFmtConfig = FmtConfig#{template => file_template()}, ConsoleFmtConfig = FmtConfig#{template => console_template()}, try ok = logger:set_primary_config(level, Level), DefaultHandlerId = get_default_handlerid(), ok = logger:update_formatter_config(DefaultHandlerId, ConsoleFmtConfig), case quiet_mode() of true -> ok = logger:set_handler_config(DefaultHandlerId, level, critical); _ -> ok end, case logger:add_primary_filter(progress_report, {fun ?MODULE:progress_filter/2, stop}) of ok -> ok; {error, {already_exist, _}} -> ok end, case logger:add_handler(ejabberd_log, logger_std_h, #{level => all, config => Config#{file => EjabberdLog}, formatter => {logger_formatter, FileFmtConfig}}) of ok -> ok; {error, {already_exist, _}} -> ok end, case logger:add_handler(error_log, logger_std_h, #{level => error, config => Config#{file => ErrorLog}, formatter => {logger_formatter, FileFmtConfig}}) of ok -> ok; {error, {already_exist, _}} -> ok end catch _:{Tag, Err} when Tag == badmatch; Tag == case_clause -> ?LOG_CRITICAL("Failed to set logging: ~p", [Err]), Err end. get_default_handlerid() -> Ids = logger:get_handler_ids(), case lists:member(default, Ids) of true -> default; false -> hd(Ids) end. -spec restart() -> ok. restart() -> ok. progress_filter(#{level:=info,msg:={report,#{label:={_,progress}}}} = Event, _) -> case get() of debug -> logger_filters:progress(Event#{level => debug}, log); _ -> stop end; progress_filter(Event, _) -> Event. console_template() -> [time, " [", level, "] " | msg()]. file_template() -> [time, " [", level, "] ", pid, {mfa, ["@", mfa, {line, [":", line], []}], []}, " " | msg()]. msg() -> [{logger_formatter, [[logger_formatter, title], ":", io_lib:nl()], []}, msg, io_lib:nl()]. -spec reopen_log() -> ok. reopen_log() -> ok. -spec rotate_log() -> ok. rotate_log() -> ok. -spec get() -> loglevel(). get() -> #{level := Level} = logger:get_primary_config(), Level. -spec set(0..5 | loglevel()) -> ok. set(N) when is_integer(N), N>=0, N=<5 -> set(convert_loglevel(N)); set(Level) when ?is_loglevel(Level) -> case get() of Level -> ok; PrevLevel -> ?LOG_NOTICE("Changing loglevel from '~s' to '~s'", [PrevLevel, Level]), logger:set_primary_config(level, Level), case Level of debug -> xmpp:set_config([{debug, true}]); _ -> xmpp:set_config([{debug, false}]) end end. -spec flush() -> ok. flush() -> lists:foreach( fun(#{id := HandlerId, module := logger_std_h}) -> logger_std_h:filesync(HandlerId); (#{id := HandlerId, module := logger_disk_log_h}) -> logger_disk_log_h:filesync(HandlerId); (_) -> ok end, logger:get_handler_config()). -endif. ejabberd-21.12/src/misc.erl0000644000232200023220000005117414154362354016070 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @doc %%% This is the place for some unsorted auxiliary functions %%% Some functions from jlib.erl are moved here %%% Mild rubbish heap is accepted ;) %%% @end %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(misc). %% API -export([add_delay_info/3, add_delay_info/4, unwrap_carbon/1, unwrap_mucsub_message/1, is_standalone_chat_state/1, tolower/1, term_to_base64/1, base64_to_term/1, ip_to_list/1, hex_to_bin/1, hex_to_base64/1, url_encode/1, expand_keyword/3, atom_to_binary/1, binary_to_atom/1, tuple_to_binary/1, l2i/1, i2l/1, i2l/2, expr_to_term/1, term_to_expr/1, now_to_usec/1, usec_to_now/1, encode_pid/1, decode_pid/2, compile_exprs/2, join_atoms/2, try_read_file/1, get_descr/2, css_dir/0, img_dir/0, js_dir/0, msgs_dir/0, sql_dir/0, lua_dir/0, read_css/1, read_img/1, read_js/1, read_lua/1, intersection/2, format_val/1, cancel_timer/1, unique_timestamp/0, is_mucsub_message/1, best_match/2, pmap/2, peach/2, format_exception/4, get_my_ipv4_address/0, get_my_ipv6_address/0, parse_ip_mask/1, crypto_hmac/3, crypto_hmac/4, uri_parse/1, match_ip_mask/3, format_hosts_list/1, format_cycle/1, delete_dir/1]). %% Deprecated functions -export([decode_base64/1, encode_base64/1]). -deprecated([{decode_base64, 1}, {encode_base64, 1}]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include_lib("kernel/include/file.hrl"). -type distance_cache() :: #{{string(), string()} => non_neg_integer()}. -ifdef(USE_OLD_HTTP_URI). uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> {ok, {Scheme, _UserInfo, Host, Port, Path, _Query}} = http_uri:parse(URL), {ok, Scheme, Host, Port, Path}. -else. uri_parse(URL) when is_binary(URL) -> uri_parse(binary_to_list(URL)); uri_parse(URL) -> case uri_string:parse(URL) of #{scheme := Scheme, host := Host, port := Port, path := Path} -> {ok, Scheme, Host, Port, Path}; #{scheme := "https", host := Host, path := Path} -> {ok, "https", Host, 443, Path}; #{scheme := "http", host := Host, path := Path} -> {ok, "http", Host, 80, Path} end. -endif. -ifdef(USE_OLD_CRYPTO_HMAC). crypto_hmac(Type, Key, Data) -> crypto:hmac(Type, Key, Data). crypto_hmac(Type, Key, Data, MacL) -> crypto:hmac(Type, Key, Data, MacL). -else. crypto_hmac(Type, Key, Data) -> crypto:mac(hmac, Type, Key, Data). crypto_hmac(Type, Key, Data, MacL) -> crypto:macN(hmac, Type, Key, Data, MacL). -endif. %%%=================================================================== %%% API %%%=================================================================== -spec add_delay_info(stanza(), jid(), erlang:timestamp()) -> stanza(). add_delay_info(Stz, From, Time) -> add_delay_info(Stz, From, Time, <<"">>). -spec add_delay_info(stanza(), jid(), erlang:timestamp(), binary()) -> stanza(). add_delay_info(Stz, From, Time, Desc) -> Delays = xmpp:get_subtags(Stz, #delay{stamp = {0,0,0}}), Matching = lists:any( fun(#delay{from = OldFrom}) when is_record(OldFrom, jid) -> jid:tolower(From) == jid:tolower(OldFrom); (_) -> false end, Delays), case Matching of true -> Stz; _ -> NewDelay = #delay{stamp = Time, from = From, desc = Desc}, xmpp:append_subtags(Stz, [NewDelay]) end. -spec unwrap_carbon(stanza()) -> xmpp_element(). unwrap_carbon(#message{} = Msg) -> try case xmpp:get_subtag(Msg, #carbons_sent{forwarded = #forwarded{}}) of #carbons_sent{forwarded = #forwarded{sub_els = [El]}} -> xmpp:decode(El, ?NS_CLIENT, [ignore_els]); _ -> case xmpp:get_subtag(Msg, #carbons_received{ forwarded = #forwarded{}}) of #carbons_received{forwarded = #forwarded{sub_els = [El]}} -> xmpp:decode(El, ?NS_CLIENT, [ignore_els]); _ -> Msg end end catch _:{xmpp_codec, _} -> Msg end; unwrap_carbon(Stanza) -> Stanza. -spec unwrap_mucsub_message(xmpp_element()) -> message() | false. unwrap_mucsub_message(#message{} = OuterMsg) -> case xmpp:get_subtag(OuterMsg, #ps_event{}) of #ps_event{ items = #ps_items{ node = Node, items = [ #ps_item{ sub_els = [#message{} = InnerMsg]} | _]}} when Node == ?NS_MUCSUB_NODES_MESSAGES; Node == ?NS_MUCSUB_NODES_SUBJECT -> InnerMsg; _ -> false end; unwrap_mucsub_message(_Packet) -> false. -spec is_mucsub_message(xmpp_element()) -> boolean(). is_mucsub_message(#message{} = OuterMsg) -> case xmpp:get_subtag(OuterMsg, #ps_event{}) of #ps_event{ items = #ps_items{ node = Node}} when Node == ?NS_MUCSUB_NODES_MESSAGES; Node == ?NS_MUCSUB_NODES_SUBJECT; Node == ?NS_MUCSUB_NODES_AFFILIATIONS; Node == ?NS_MUCSUB_NODES_CONFIG; Node == ?NS_MUCSUB_NODES_PARTICIPANTS; Node == ?NS_MUCSUB_NODES_PRESENCE; Node == ?NS_MUCSUB_NODES_SUBSCRIBERS -> true; _ -> false end; is_mucsub_message(_Packet) -> false. -spec is_standalone_chat_state(stanza()) -> boolean(). is_standalone_chat_state(Stanza) -> case unwrap_carbon(Stanza) of #message{body = [], subject = [], sub_els = Els} -> IgnoreNS = [?NS_CHATSTATES, ?NS_DELAY, ?NS_EVENT], Stripped = [El || El <- Els, not lists:member(xmpp:get_ns(El), IgnoreNS)], Stripped == []; _ -> false end. -spec tolower(binary()) -> binary(). tolower(B) -> iolist_to_binary(tolower_s(binary_to_list(B))). tolower_s([C | Cs]) -> if C >= $A, C =< $Z -> [C + 32 | tolower_s(Cs)]; true -> [C | tolower_s(Cs)] end; tolower_s([]) -> []. -spec term_to_base64(term()) -> binary(). term_to_base64(Term) -> encode_base64(term_to_binary(Term)). -spec base64_to_term(binary()) -> {term, term()} | error. base64_to_term(Base64) -> try binary_to_term(base64:decode(Base64), [safe]) of Term -> {term, Term} catch _:_ -> error end. -spec decode_base64(binary()) -> binary(). decode_base64(S) -> try base64:mime_decode(S) catch _:badarg -> <<>> end. -spec encode_base64(binary()) -> binary(). encode_base64(Data) -> base64:encode(Data). -spec ip_to_list(inet:ip_address() | undefined | {inet:ip_address(), inet:port_number()}) -> binary(). ip_to_list({local, _}) -> <<"unix">>; ip_to_list({IP, _Port}) -> ip_to_list(IP); %% This function clause could use inet_parse too: ip_to_list(undefined) -> <<"unknown">>; ip_to_list(IP) -> list_to_binary(inet_parse:ntoa(IP)). -spec hex_to_bin(binary()) -> binary(). hex_to_bin(Hex) -> hex_to_bin(binary_to_list(Hex), []). -spec hex_to_bin(list(), list()) -> binary(). hex_to_bin([], Acc) -> list_to_binary(lists:reverse(Acc)); hex_to_bin([H1, H2 | T], Acc) -> {ok, [V], []} = io_lib:fread("~16u", [H1, H2]), hex_to_bin(T, [V | Acc]). -spec hex_to_base64(binary()) -> binary(). hex_to_base64(Hex) -> base64:encode(hex_to_bin(Hex)). -spec url_encode(binary()) -> binary(). url_encode(A) -> url_encode(A, <<>>). -spec expand_keyword(iodata(), iodata(), iodata()) -> binary(). expand_keyword(Keyword, Input, Replacement) -> re:replace(Input, Keyword, Replacement, [{return, binary}, global]). binary_to_atom(Bin) -> erlang:binary_to_atom(Bin, utf8). tuple_to_binary(T) -> iolist_to_binary(tuple_to_list(T)). atom_to_binary(A) -> erlang:atom_to_binary(A, utf8). expr_to_term(Expr) -> Str = binary_to_list(<>), {ok, Tokens, _} = erl_scan:string(Str), {ok, Term} = erl_parse:parse_term(Tokens), Term. term_to_expr(Term) -> list_to_binary(io_lib:print(Term, 1, 999999, -1)). -spec now_to_usec(erlang:timestamp()) -> non_neg_integer(). now_to_usec({MSec, Sec, USec}) -> (MSec*1000000 + Sec)*1000000 + USec. -spec usec_to_now(non_neg_integer()) -> erlang:timestamp(). usec_to_now(Int) -> Secs = Int div 1000000, USec = Int rem 1000000, MSec = Secs div 1000000, Sec = Secs rem 1000000, {MSec, Sec, USec}. l2i(I) when is_integer(I) -> I; l2i(L) when is_binary(L) -> binary_to_integer(L). i2l(I) when is_integer(I) -> integer_to_binary(I); i2l(L) when is_binary(L) -> L. i2l(I, N) when is_integer(I) -> i2l(i2l(I), N); i2l(L, N) when is_binary(L) -> case str:len(L) of N -> L; C when C > N -> L; _ -> i2l(<<$0, L/binary>>, N) end. -spec encode_pid(pid()) -> binary(). encode_pid(Pid) -> list_to_binary(erlang:pid_to_list(Pid)). -spec decode_pid(binary(), binary()) -> pid(). decode_pid(PidBin, NodeBin) -> PidStr = binary_to_list(PidBin), Pid = erlang:list_to_pid(PidStr), case erlang:binary_to_atom(NodeBin, latin1) of Node when Node == node() -> Pid; Node -> try set_node_id(PidStr, NodeBin) catch _:badarg -> erlang:error({bad_node, Node}) end end. -spec compile_exprs(module(), [string()]) -> ok | {error, any()}. compile_exprs(Mod, Exprs) -> try Forms = lists:map( fun(Expr) -> {ok, Tokens, _} = erl_scan:string(lists:flatten(Expr)), {ok, Form} = erl_parse:parse_form(Tokens), Form end, Exprs), {ok, Code} = case compile:forms(Forms, []) of {ok, Mod, Bin} -> {ok, Bin}; {ok, Mod, Bin, _Warnings} -> {ok, Bin}; Error -> Error end, {module, Mod} = code:load_binary(Mod, "nofile", Code), ok catch _:{badmatch, {error, ErrInfo, _ErrLocation}} -> {error, ErrInfo}; _:{badmatch, {error, _} = Err} -> Err; _:{badmatch, error} -> {error, compile_failed} end. -spec join_atoms([atom()], binary()) -> binary(). join_atoms(Atoms, Sep) -> str:join([io_lib:format("~p", [A]) || A <- lists:sort(Atoms)], Sep). %% @doc Checks if the file is readable and converts its name to binary. %% Fails with `badarg' otherwise. The function is intended for usage %% in configuration validators only. -spec try_read_file(file:filename_all()) -> binary(). try_read_file(Path) -> case file:open(Path, [read]) of {ok, Fd} -> file:close(Fd), iolist_to_binary(Path); {error, Why} -> ?ERROR_MSG("Failed to read ~ts: ~ts", [Path, file:format_error(Why)]), erlang:error(badarg) end. -spec css_dir() -> file:filename(). css_dir() -> get_dir("css"). -spec img_dir() -> file:filename(). img_dir() -> get_dir("img"). -spec js_dir() -> file:filename(). js_dir() -> get_dir("js"). -spec msgs_dir() -> file:filename(). msgs_dir() -> get_dir("msgs"). -spec sql_dir() -> file:filename(). sql_dir() -> get_dir("sql"). -spec lua_dir() -> file:filename(). lua_dir() -> get_dir("lua"). -spec read_css(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_css(File) -> read_file(filename:join(css_dir(), File)). -spec read_img(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_img(File) -> read_file(filename:join(img_dir(), File)). -spec read_js(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_js(File) -> read_file(filename:join(js_dir(), File)). -spec read_lua(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_lua(File) -> read_file(filename:join(lua_dir(), File)). -spec get_descr(binary(), binary()) -> binary(). get_descr(Lang, Text) -> Desc = translate:translate(Lang, Text), Copyright = ejabberd_config:get_copyright(), <>. -spec intersection(list(), list()) -> list(). intersection(L1, L2) -> lists:filter( fun(E) -> lists:member(E, L2) end, L1). -spec format_val(any()) -> iodata(). format_val({yaml, S}) when is_integer(S); is_binary(S); is_atom(S) -> format_val(S); format_val({yaml, YAML}) -> S = try fast_yaml:encode(YAML) catch _:_ -> YAML end, format_val(S); format_val(I) when is_integer(I) -> integer_to_list(I); format_val(B) when is_atom(B) -> erlang:atom_to_binary(B, utf8); format_val(Term) -> S = try iolist_to_binary(Term) catch _:_ -> list_to_binary(io_lib:format("~p", [Term])) end, case binary:match(S, <<"\n">>) of nomatch -> S; _ -> [io_lib:nl(), S] end. -spec cancel_timer(reference() | undefined) -> ok. cancel_timer(TRef) when is_reference(TRef) -> case erlang:cancel_timer(TRef) of false -> receive {timeout, TRef, _} -> ok after 0 -> ok end; _ -> ok end; cancel_timer(_) -> ok. -spec best_match(atom() | binary() | string(), [atom() | binary() | string()]) -> string(). best_match(Pattern, []) -> Pattern; best_match(Pattern, Opts) -> String = to_string(Pattern), {Ds, _} = lists:mapfoldl( fun(Opt, Cache) -> SOpt = to_string(Opt), {Distance, Cache1} = ld(String, SOpt, Cache), {{Distance, SOpt}, Cache1} end, #{}, Opts), element(2, lists:min(Ds)). -spec pmap(fun((T1) -> T2), [T1]) -> [T2]. pmap(Fun, [_,_|_] = List) -> case erlang:system_info(logical_processors) of 1 -> lists:map(Fun, List); _ -> Self = self(), lists:map( fun({Pid, Ref}) -> receive {Pid, Ret} -> receive {'DOWN', Ref, _, _, _} -> Ret end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end end, [spawn_monitor( fun() -> Self ! {self(), Fun(X)} end) || X <- List]) end; pmap(Fun, List) -> lists:map(Fun, List). -spec peach(fun((T) -> any()), [T]) -> ok. peach(Fun, [_,_|_] = List) -> case erlang:system_info(logical_processors) of 1 -> lists:foreach(Fun, List); _ -> Self = self(), lists:foreach( fun({Pid, Ref}) -> receive Pid -> receive {'DOWN', Ref, _, _, _} -> ok end; {'DOWN', Ref, _, _, Reason} -> exit(Reason) end end, [spawn_monitor( fun() -> Fun(X), Self ! self() end) || X <- List]) end; peach(Fun, List) -> lists:foreach(Fun, List). -ifdef(HAVE_ERL_ERROR). format_exception(Level, Class, Reason, Stacktrace) -> erl_error:format_exception( Level, Class, Reason, Stacktrace, fun(_M, _F, _A) -> false end, fun(Term, I) -> io_lib:print(Term, I, 80, -1) end). -else. format_exception(Level, Class, Reason, Stacktrace) -> lib:format_exception( Level, Class, Reason, Stacktrace, fun(_M, _F, _A) -> false end, fun(Term, I) -> io_lib:print(Term, I, 80, -1) end). -endif. -spec get_my_ipv4_address() -> inet:ip4_address(). get_my_ipv4_address() -> {ok, MyHostName} = inet:gethostname(), case inet:getaddr(MyHostName, inet) of {ok, Addr} -> Addr; {error, _} -> {127, 0, 0, 1} end. -spec get_my_ipv6_address() -> inet:ip6_address(). get_my_ipv6_address() -> {ok, MyHostName} = inet:gethostname(), case inet:getaddr(MyHostName, inet6) of {ok, Addr} -> Addr; {error, _} -> {0, 0, 0, 0, 0, 0, 0, 1} end. -spec parse_ip_mask(binary()) -> {ok, {inet:ip4_address(), 0..32}} | {ok, {inet:ip6_address(), 0..128}} | error. parse_ip_mask(S) -> case econf:validate(econf:ip_mask(), S) of {ok, _} = Ret -> Ret; _ -> error end. -spec match_ip_mask(inet:ip_address(), inet:ip_address(), 0..128) -> boolean(). match_ip_mask({_, _, _, _} = IP, {_, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (32 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({_, _, _, _, _, _, _, _} = IP, {_, _, _, _, _, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (128 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({_, _, _, _} = IP, {0, 0, 0, 0, 0, 16#FFFF, _, _} = Net, Mask) -> IPInt = ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}) + ip_to_integer(IP), NetInt = ip_to_integer(Net), M = bnot (1 bsl (128 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask({0, 0, 0, 0, 0, 16#FFFF, _, _} = IP, {_, _, _, _} = Net, Mask) -> IPInt = ip_to_integer(IP) - ip_to_integer({0, 0, 0, 0, 0, 16#FFFF, 0, 0}), NetInt = ip_to_integer(Net), M = bnot (1 bsl (32 - Mask) - 1), IPInt band M =:= NetInt band M; match_ip_mask(_, _, _) -> false. -spec format_hosts_list([binary(), ...]) -> iolist(). format_hosts_list([Host]) -> Host; format_hosts_list([H1, H2]) -> [H1, " and ", H2]; format_hosts_list([H1, H2, H3]) -> [H1, ", ", H2, " and ", H3]; format_hosts_list([H1, H2|Hs]) -> io_lib:format("~ts, ~ts and ~B more hosts", [H1, H2, length(Hs)]). -spec format_cycle([atom(), ...]) -> iolist(). format_cycle([M1]) -> atom_to_list(M1); format_cycle([M1, M2]) -> [atom_to_list(M1), " and ", atom_to_list(M2)]; format_cycle([M|Ms]) -> atom_to_list(M) ++ ", " ++ format_cycle(Ms). -spec delete_dir(file:filename_all()) -> ok | {error, file:posix()}. delete_dir(Dir) -> try {ok, Entries} = file:list_dir(Dir), lists:foreach(fun(Path) -> case filelib:is_dir(Path) of true -> ok = delete_dir(Path); false -> ok = file:delete(Path) end end, [filename:join(Dir, Entry) || Entry <- Entries]), ok = file:del_dir(Dir) catch _:{badmatch, {error, Error}} -> {error, Error} end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec url_encode(binary(), binary()) -> binary(). url_encode(<>, Acc) when (H >= $a andalso H =< $z) orelse (H >= $A andalso H =< $Z) orelse (H >= $0 andalso H =< $9) orelse H == $_ orelse H == $. orelse H == $- orelse H == $/ orelse H == $: -> url_encode(T, <>); url_encode(<>, Acc) -> case integer_to_list(H, 16) of [X, Y] -> url_encode(T, <>); [X] -> url_encode(T, <>) end; url_encode(<<>>, Acc) -> Acc. -spec set_node_id(string(), binary()) -> pid(). set_node_id(PidStr, NodeBin) -> ExtPidStr = erlang:pid_to_list( binary_to_term( <<131,103,100,(size(NodeBin)):16,NodeBin/binary,0:72>>)), [H|_] = string:tokens(ExtPidStr, "."), [_|T] = string:tokens(PidStr, "."), erlang:list_to_pid(string:join([H|T], ".")). -spec read_file(file:filename()) -> {ok, binary()} | {error, file:posix()}. read_file(Path) -> case file:read_file(Path) of {ok, Data} -> {ok, Data}; {error, Why} = Err -> ?ERROR_MSG("Failed to read file ~ts: ~ts", [Path, file:format_error(Why)]), Err end. -spec get_dir(string()) -> file:filename(). get_dir(Type) -> Env = "EJABBERD_" ++ string:to_upper(Type) ++ "_PATH", case os:getenv(Env) of false -> case code:priv_dir(ejabberd) of {error, _} -> filename:join(["priv", Type]); Path -> filename:join([Path, Type]) end; Path -> Path end. %% Generates erlang:timestamp() that is guaranteed to unique -spec unique_timestamp() -> erlang:timestamp(). unique_timestamp() -> {MS, S, _} = erlang:timestamp(), {MS, S, erlang:unique_integer([positive, monotonic]) rem 1000000}. %% Levenshtein distance -spec ld(string(), string(), distance_cache()) -> {non_neg_integer(), distance_cache()}. ld([] = S, T, Cache) -> {length(T), maps:put({S, T}, length(T), Cache)}; ld(S, [] = T, Cache) -> {length(S), maps:put({S, T}, length(S), Cache)}; ld([X|S], [X|T], Cache) -> ld(S, T, Cache); ld([_|ST] = S, [_|TT] = T, Cache) -> try {maps:get({S, T}, Cache), Cache} catch _:{badkey, _} -> {L1, C1} = ld(S, TT, Cache), {L2, C2} = ld(ST, T, C1), {L3, C3} = ld(ST, TT, C2), L = 1 + lists:min([L1, L2, L3]), {L, maps:put({S, T}, L, C3)} end. -spec ip_to_integer(inet:ip_address()) -> non_neg_integer(). ip_to_integer({IP1, IP2, IP3, IP4}) -> IP1 bsl 8 bor IP2 bsl 8 bor IP3 bsl 8 bor IP4; ip_to_integer({IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}) -> IP1 bsl 16 bor IP2 bsl 16 bor IP3 bsl 16 bor IP4 bsl 16 bor IP5 bsl 16 bor IP6 bsl 16 bor IP7 bsl 16 bor IP8. -spec to_string(atom() | binary() | string()) -> string(). to_string(A) when is_atom(A) -> atom_to_list(A); to_string(B) when is_binary(B) -> binary_to_list(B); to_string(S) -> S. ejabberd-21.12/src/mod_privilege.erl0000644000232200023220000004265514154362354017766 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_privilege.erl %%% Author : Anna Mukharram %%% Purpose : XEP-0356: Privileged Entity %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_privilege). -author('amuhar3@gmail.com'). -protocol({xep, 0356, '0.2.1'}). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([component_connected/1, component_disconnected/2, roster_access/2, process_message/1, process_presence_out/1, process_presence_in/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -type roster_permission() :: both | get | set. -type presence_permission() :: managed_entity | roster. -type message_permission() :: outgoing. -type roster_permissions() :: [{roster_permission(), acl:acl()}]. -type presence_permissions() :: [{presence_permission(), acl:acl()}]. -type message_permissions() :: [{message_permission(), acl:acl()}]. -type access() :: [{roster, roster_permissions()} | {presence, presence_permissions()} | {message, message_permissions()}]. -type permissions() :: #{binary() => access()}. -record(state, {server_host = <<"">> :: binary()}). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> ok. mod_opt_type(roster) -> econf:options( #{both => econf:acl(), get => econf:acl(), set => econf:acl()}); mod_opt_type(message) -> econf:options( #{outgoing => econf:acl()}); mod_opt_type(presence) -> econf:options( #{managed_entity => econf:acl(), roster => econf:acl()}). mod_options(_) -> [{roster, [{both, none}, {get, none}, {set, none}]}, {presence, [{managed_entity, none}, {roster, none}]}, {message, [{outgoing,none}]}]. mod_doc() -> #{desc => [?T("This module is an implementation of " "https://xmpp.org/extensions/xep-0356.html" "[XEP-0356: Privileged Entity]. This extension " "allows components to have privileged access to " "other entity data (send messages on behalf of the " "server or on behalf of a user, get/set user roster, " "access presence information, etc.). This may be used " "to write powerful external components, for example " "implementing an external " "https://xmpp.org/extensions/xep-0163.html[PEP] or " "https://xmpp.org/extensions/xep-0313.html[MAM] service."), "", ?T("By default a component does not have any privileged access. " "It is worth noting that the permissions grant access to " "the component to a specific data type for all users of " "the virtual host on which 'mod_privilege' is loaded."), "", ?T("Make sure you have a listener configured to connect your " "component. Check the section about listening ports for more " "information."), "", ?T("WARNING: Security issue: Privileged access gives components " "access to sensitive data, so permission should be granted " "carefully, only if you trust a component."), "", ?T("NOTE: This module is complementary to _`mod_delegation`_, " "but can also be used separately.")], opts => [{roster, #{value => ?T("Options"), desc => ?T("This option defines roster permissions. " "By default no permissions are given. " "The 'Options' are:")}, [{both, #{value => ?T("AccessName"), desc => ?T("Sets read/write access to a user's roster. " "The default value is 'none'.")}}, {get, #{value => ?T("AccessName"), desc => ?T("Sets read access to a user's roster. " "The default value is 'none'.")}}, {set, #{value => ?T("AccessName"), desc => ?T("Sets write access to a user's roster. " "The default value is 'none'.")}}]}, {message, #{value => ?T("Options"), desc => ?T("This option defines permissions for messages. " "By default no permissions are given. " "The 'Options' are:")}, [{outgoing, #{value => ?T("AccessName"), desc => ?T("The option defines an access rule for sending " "outgoing messages by the component. " "The default value is 'none'.")}}]}, {presence, #{value => ?T("Options"), desc => ?T("This option defines permissions for presences. " "By default no permissions are given. " "The 'Options' are:")}, [{managed_entity, #{value => ?T("AccessName"), desc => ?T("An access rule that gives permissions to " "the component to receive server presences. " "The default value is 'none'.")}}, {roster, #{value => ?T("AccessName"), desc => ?T("An access rule that gives permissions to " "the component to receive the presence of both " "the users and the contacts in their roster. " "The default value is 'none'.")}}]}], example => ["modules:", " ...", " mod_privilege:", " roster:", " get: all", " presence:", " managed_entity: all", " message:", " outgoing: all", " ..."]}. depends(_, _) -> []. -spec component_connected(binary()) -> ok. component_connected(Host) -> lists:foreach( fun(ServerHost) -> Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), gen_server:cast(Proc, {component_connected, Host}) end, ejabberd_option:hosts()). -spec component_disconnected(binary(), binary()) -> ok. component_disconnected(Host, _Reason) -> lists:foreach( fun(ServerHost) -> Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), gen_server:cast(Proc, {component_disconnected, Host}) end, ejabberd_option:hosts()). -spec process_message(stanza()) -> stop | ok. process_message(#message{from = #jid{luser = <<"">>, lresource = <<"">>} = From, to = #jid{lresource = <<"">>} = To, lang = Lang, type = T} = Msg) when T /= error -> Host = From#jid.lserver, ServerHost = To#jid.lserver, Permissions = get_permissions(ServerHost), case maps:find(Host, Permissions) of {ok, Access} -> case proplists:get_value(message, Access, none) of outgoing -> forward_message(Msg); _ -> Txt = ?T("Insufficient privilege"), Err = xmpp:err_forbidden(Txt, Lang), ejabberd_router:route_error(Msg, Err) end, stop; error -> %% Component is disconnected ok end; process_message(_Stanza) -> ok. -spec roster_access(boolean(), iq()) -> boolean(). roster_access(true, _) -> true; roster_access(false, #iq{from = From, to = To, type = Type}) -> Host = From#jid.lserver, ServerHost = To#jid.lserver, Permissions = get_permissions(ServerHost), case maps:find(Host, Permissions) of {ok, Access} -> Permission = proplists:get_value(roster, Access, none), (Permission == both) orelse (Permission == get andalso Type == get) orelse (Permission == set andalso Type == set); error -> %% Component is disconnected false end. -spec process_presence_out({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. process_presence_out({#presence{ from = #jid{luser = LUser, lserver = LServer} = From, to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>}, type = Type} = Pres, C2SState}) when Type == available; Type == unavailable -> %% Self-presence processing Permissions = get_permissions(LServer), lists:foreach( fun({Host, Access}) -> Permission = proplists:get_value(presence, Access, none), if Permission == roster; Permission == managed_entity -> To = jid:make(Host), ejabberd_router:route( xmpp:set_from_to(Pres, From, To)); true -> ok end end, maps:to_list(Permissions)), {Pres, C2SState}; process_presence_out(Acc) -> Acc. -spec process_presence_in({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. process_presence_in({#presence{ from = #jid{luser = U, lserver = S} = From, to = #jid{luser = LUser, lserver = LServer}, type = Type} = Pres, C2SState}) when {U, S} /= {LUser, LServer} andalso (Type == available orelse Type == unavailable) -> Permissions = get_permissions(LServer), lists:foreach( fun({Host, Access}) -> case proplists:get_value(presence, Access, none) of roster -> Permission = proplists:get_value(roster, Access, none), if Permission == both; Permission == get -> To = jid:make(Host), ejabberd_router:route( xmpp:set_from_to(Pres, From, To)); true -> ok end; _ -> ok end end, maps:to_list(Permissions)), {Pres, C2SState}; process_presence_in(Acc) -> Acc. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host|_]) -> process_flag(trap_exit, true), catch ets:new(?MODULE, [named_table, public, {heir, erlang:group_leader(), none}]), ejabberd_hooks:add(component_connected, ?MODULE, component_connected, 50), ejabberd_hooks:add(component_disconnected, ?MODULE, component_disconnected, 50), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, process_message, 50), ejabberd_hooks:add(roster_remote_access, Host, ?MODULE, roster_access, 50), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, process_presence_out, 50), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, process_presence_in, 50), {ok, #state{server_host = Host}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({component_connected, Host}, State) -> ServerHost = State#state.server_host, From = jid:make(ServerHost), To = jid:make(Host), RosterPerm = get_roster_permission(ServerHost, Host), PresencePerm = get_presence_permission(ServerHost, Host), MessagePerm = get_message_permission(ServerHost, Host), if RosterPerm /= none; PresencePerm /= none; MessagePerm /= none -> Priv = #privilege{perms = [#privilege_perm{access = message, type = MessagePerm}, #privilege_perm{access = roster, type = RosterPerm}, #privilege_perm{access = presence, type = PresencePerm}]}, ?INFO_MSG("Granting permissions to external " "component '~ts': roster = ~ts, presence = ~ts, " "message = ~ts", [Host, RosterPerm, PresencePerm, MessagePerm]), Msg = #message{from = From, to = To, sub_els = [Priv]}, ejabberd_router:route(Msg), Permissions = maps:put(Host, [{roster, RosterPerm}, {presence, PresencePerm}, {message, MessagePerm}], get_permissions(ServerHost)), ets:insert(?MODULE, {ServerHost, Permissions}), {noreply, State}; true -> ?INFO_MSG("Granting no permissions to external component '~ts'", [Host]), {noreply, State} end; handle_cast({component_disconnected, Host}, State) -> ServerHost = State#state.server_host, Permissions = maps:remove(Host, get_permissions(ServerHost)), case maps:size(Permissions) of 0 -> ets:delete(?MODULE, ServerHost); _ -> ets:insert(?MODULE, {ServerHost, Permissions}) end, {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> Host = State#state.server_host, case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_hooks:delete(component_connected, ?MODULE, component_connected, 50), ejabberd_hooks:delete(component_disconnected, ?MODULE, component_disconnected, 50); true -> ok end, ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, process_message, 50), ejabberd_hooks:delete(roster_remote_access, Host, ?MODULE, roster_access, 50), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, process_presence_out, 50), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, process_presence_in, 50), ets:delete(?MODULE, Host). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_permissions(binary()) -> permissions(). get_permissions(ServerHost) -> try ets:lookup_element(?MODULE, ServerHost, 2) catch _:badarg -> #{} end. -spec forward_message(message()) -> ok. forward_message(#message{to = To} = Msg) -> ServerHost = To#jid.lserver, Lang = xmpp:get_lang(Msg), CodecOpts = ejabberd_config:codec_options(), try xmpp:try_subtag(Msg, #privilege{}) of #privilege{forwarded = #forwarded{sub_els = [SubEl]}} -> try xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of #message{} = NewMsg -> case NewMsg#message.from of #jid{lresource = <<"">>, lserver = ServerHost} -> ejabberd_router:route(NewMsg); _ -> Lang = xmpp:get_lang(Msg), Txt = ?T("Invalid 'from' attribute in forwarded message"), Err = xmpp:err_forbidden(Txt, Lang), ejabberd_router:route_error(Msg, Err) end; _ -> Txt = ?T("Message not found in forwarded payload"), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) end; _ -> Txt = ?T("No element found"), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err) end. -spec get_roster_permission(binary(), binary()) -> roster_permission() | none. get_roster_permission(ServerHost, Host) -> Perms = mod_privilege_opt:roster(ServerHost), case match_rule(ServerHost, Host, Perms, both) of allow -> both; deny -> Get = match_rule(ServerHost, Host, Perms, get), Set = match_rule(ServerHost, Host, Perms, set), if Get == allow, Set == allow -> both; Get == allow -> get; Set == allow -> set; true -> none end end. -spec get_message_permission(binary(), binary()) -> message_permission() | none. get_message_permission(ServerHost, Host) -> Perms = mod_privilege_opt:message(ServerHost), case match_rule(ServerHost, Host, Perms, outgoing) of allow -> outgoing; deny -> none end. -spec get_presence_permission(binary(), binary()) -> presence_permission() | none. get_presence_permission(ServerHost, Host) -> Perms = mod_privilege_opt:presence(ServerHost), case match_rule(ServerHost, Host, Perms, roster) of allow -> roster; deny -> case match_rule(ServerHost, Host, Perms, managed_entity) of allow -> managed_entity; deny -> none end end. -spec match_rule(binary(), binary(), roster_permissions(), roster_permission()) -> allow | deny; (binary(), binary(), presence_permissions(), presence_permission()) -> allow | deny; (binary(), binary(), message_permissions(), message_permission()) -> allow | deny. match_rule(ServerHost, Host, Perms, Type) -> Access = proplists:get_value(Type, Perms, none), acl:match_rule(ServerHost, Access, jid:make(Host)). ejabberd-21.12/src/mod_adhoc.erl0000644000232200023220000002455514154362354017055 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_adhoc.erl %%% Author : Magnus Henoch %%% Purpose : Handle incoming ad-doc requests (XEP-0050) %%% Created : 15 Nov 2005 by Magnus Henoch %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_adhoc). -author('henoch@dtek.chalmers.se'). -protocol({xep, 50, '1.2'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq/1, process_sm_iq/1, get_local_commands/5, get_local_identity/5, get_local_features/5, get_sm_commands/5, get_sm_identity/5, get_sm_features/5, ping_item/4, ping_command/4, mod_opt_type/1, depends/2, mod_options/1, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_COMMANDS, ?MODULE, process_sm_iq), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, get_local_identity, 99), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 99), ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_commands, 99), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 99), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 99), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_commands, 99), ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, ping_item, 100), ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, ping_command, 100). stop(Host) -> ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, ping_command, 100), ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, ping_item, 100), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_commands, 99), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 99), ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 99), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, get_local_commands, 99), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 99), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, get_local_identity, 99), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_COMMANDS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS). reload(_Host, _NewOpts, _OldOpts) -> ok. %------------------------------------------------------------------------- -spec get_local_commands(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_local_commands(Acc, _From, #jid{server = Server, lserver = LServer} = _To, <<"">>, Lang) -> Display = mod_adhoc_opt:report_commands_node(LServer), case Display of false -> Acc; _ -> Items = case Acc of {result, I} -> I; _ -> [] end, Nodes = [#disco_item{jid = jid:make(Server), node = ?NS_COMMANDS, name = translate:translate(Lang, ?T("Commands"))}], {result, Items ++ Nodes} end; get_local_commands(_Acc, From, #jid{lserver = LServer} = To, ?NS_COMMANDS, Lang) -> ejabberd_hooks:run_fold(adhoc_local_items, LServer, {result, []}, [From, To, Lang]); get_local_commands(_Acc, _From, _To, <<"ping">>, _Lang) -> {result, []}; get_local_commands(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec get_sm_commands(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_sm_commands(Acc, _From, #jid{lserver = LServer} = To, <<"">>, Lang) -> Display = mod_adhoc_opt:report_commands_node(LServer), case Display of false -> Acc; _ -> Items = case Acc of {result, I} -> I; _ -> [] end, Nodes = [#disco_item{jid = To, node = ?NS_COMMANDS, name = translate:translate(Lang, ?T("Commands"))}], {result, Items ++ Nodes} end; get_sm_commands(_Acc, From, #jid{lserver = LServer} = To, ?NS_COMMANDS, Lang) -> ejabberd_hooks:run_fold(adhoc_sm_items, LServer, {result, []}, [From, To, Lang]); get_sm_commands(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. %% On disco info request to the ad-hoc node, return automation/command-list. get_local_identity(Acc, _From, _To, ?NS_COMMANDS, Lang) -> [#identity{category = <<"automation">>, type = <<"command-list">>, name = translate:translate(Lang, ?T("Commands"))} | Acc]; get_local_identity(Acc, _From, _To, <<"ping">>, Lang) -> [#identity{category = <<"automation">>, type = <<"command-node">>, name = translate:translate(Lang, ?T("Ping"))} | Acc]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. %% On disco info request to the ad-hoc node, return automation/command-list. get_sm_identity(Acc, _From, _To, ?NS_COMMANDS, Lang) -> [#identity{category = <<"automation">>, type = <<"command-list">>, name = translate:translate(Lang, ?T("Commands"))} | Acc]; get_sm_identity(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec get_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_local_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; _ -> [] end, {result, Feats ++ [?NS_COMMANDS]}; get_local_features(_Acc, _From, _To, ?NS_COMMANDS, _Lang) -> {result, []}; get_local_features(_Acc, _From, _To, <<"ping">>, _Lang) -> {result, [?NS_COMMANDS]}; get_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec get_sm_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; _ -> [] end, {result, Feats ++ [?NS_COMMANDS]}; get_sm_features(_Acc, _From, _To, ?NS_COMMANDS, _Lang) -> {result, []}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. %------------------------------------------------------------------------- -spec process_local_iq(iq()) -> iq() | ignore. process_local_iq(IQ) -> process_adhoc_request(IQ, local). -spec process_sm_iq(iq()) -> iq() | ignore. process_sm_iq(IQ) -> process_adhoc_request(IQ, sm). -spec process_adhoc_request(iq(), sm | local) -> iq() | ignore. process_adhoc_request(#iq{from = From, to = To, type = set, lang = Lang, sub_els = [#adhoc_command{} = SubEl]} = IQ, Type) -> Host = To#jid.lserver, Res = case Type of local -> ejabberd_hooks:run_fold(adhoc_local_commands, Host, empty, [From, To, fix_lang(Lang, SubEl)]); sm -> ejabberd_hooks:run_fold(adhoc_sm_commands, Host, empty, [From, To, fix_lang(Lang, SubEl)]) end, case Res of ignore -> ignore; empty -> Txt = ?T("No hook has processed this command"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, Error} -> xmpp:make_error(IQ, Error); Command -> xmpp:make_iq_result(IQ, Command) end; process_adhoc_request(#iq{} = IQ, _Hooks) -> xmpp:make_error(IQ, xmpp:err_bad_request()). -spec ping_item(mod_disco:items_acc(), jid(), jid(), binary()) -> {result, [disco_item()]}. ping_item(Acc, _From, #jid{server = Server} = _To, Lang) -> Items = case Acc of {result, I} -> I; _ -> [] end, Nodes = [#disco_item{jid = jid:make(Server), node = <<"ping">>, name = translate:translate(Lang, ?T("Ping"))}], {result, Items ++ Nodes}. -spec ping_command(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. ping_command(_Acc, _From, _To, #adhoc_command{lang = Lang, node = <<"ping">>, action = Action} = Request) -> if Action == execute -> xmpp_util:make_adhoc_response( Request, #adhoc_command{ status = completed, notes = [#adhoc_note{ type = info, data = translate:translate(Lang, ?T("Pong"))}]}); true -> Txt = ?T("Incorrect value of 'action' attribute"), {error, xmpp:err_bad_request(Txt, Lang)} end; ping_command(Acc, _From, _To, _Request) -> Acc. -spec fix_lang(binary(), adhoc_command()) -> adhoc_command(). fix_lang(Lang, #adhoc_command{lang = <<>>} = Cmd) -> Cmd#adhoc_command{lang = Lang}; fix_lang(_, Cmd) -> Cmd. depends(_Host, _Opts) -> []. mod_opt_type(report_commands_node) -> econf:bool(). mod_options(_Host) -> [{report_commands_node, false}]. mod_doc() -> #{desc => ?T("This module implements https://xmpp.org/extensions/xep-0050.html" "[XEP-0050: Ad-Hoc Commands]. It's an auxiliary module and is " "only needed by some of the other modules."), opts => [{report_commands_node, #{value => "true | false", desc => ?T("Provide the Commands item in the Service Discovery. " "Default value: 'false'.")}}]}. ejabberd-21.12/src/mod_private_opt.erl0000644000232200023220000000256114154362354020324 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_private_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_private, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_private, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_private, use_cache). ejabberd-21.12/src/ejabberd_sql_sup.erl0000644000232200023220000001651014154362354020434 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_sql_sup.erl %%% Author : Alexey Shchepin %%% Purpose : SQL connections supervisor %%% Created : 22 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sql_sup). -author('alexey@process-one.net'). -export([start/1, stop/1, stop/0]). -export([start_link/0, start_link/1]). -export([init/1, reload/1, config_reloaded/0, is_started/1]). -include("logger.hrl"). start(Host) -> case is_started(Host) of true -> ok; false -> App = case ejabberd_option:sql_type(Host) of mysql -> p1_mysql; pgsql -> p1_pgsql; sqlite -> sqlite3; _ -> odbc end, ejabberd:start_app(App), Spec = #{id => gen_mod:get_module_proc(Host, ?MODULE), start => {ejabberd_sql_sup, start_link, [Host]}, restart => transient, shutdown => infinity, type => supervisor, modules => [?MODULE]}, case supervisor:start_child(ejabberd_db_sup, Spec) of {ok, _} -> ok; {error, {already_started, Pid}} -> %% Wait for the supervisor to fully start _ = supervisor:count_children(Pid), ok; {error, Why} = Err -> ?ERROR_MSG("Failed to start ~ts: ~p", [?MODULE, Why]), Err end end. stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), case supervisor:terminate_child(ejabberd_db_sup, Proc) of ok -> supervisor:delete_child(ejabberd_db_sup, Proc); Err -> Err end. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). start_link(Host) -> supervisor:start_link({local, gen_mod:get_module_proc(Host, ?MODULE)}, ?MODULE, [Host]). stop() -> ejabberd_hooks:delete(host_up, ?MODULE, start, 20), ejabberd_hooks:delete(host_down, ?MODULE, stop, 90), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 20). init([]) -> file:delete(ejabberd_sql:odbcinst_config()), ejabberd_hooks:add(host_up, ?MODULE, start, 20), ejabberd_hooks:add(host_down, ?MODULE, stop, 90), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 20), ignore; init([Host]) -> Type = ejabberd_option:sql_type(Host), PoolSize = get_pool_size(Type, Host), case Type of sqlite -> check_sqlite_db(Host); mssql -> ejabberd_sql:init_mssql(Host); _ -> ok end, {ok, {{one_for_one, PoolSize * 10, 1}, child_specs(Host, PoolSize)}}. -spec config_reloaded() -> ok. config_reloaded() -> lists:foreach(fun reload/1, ejabberd_option:hosts()). -spec reload(binary()) -> ok. reload(Host) -> case is_started(Host) of true -> Sup = gen_mod:get_module_proc(Host, ?MODULE), Type = ejabberd_option:sql_type(Host), PoolSize = get_pool_size(Type, Host), lists:foreach( fun(Spec) -> supervisor:start_child(Sup, Spec) end, child_specs(Host, PoolSize)), lists:foreach( fun({Id, _, _, _}) when Id > PoolSize -> case supervisor:terminate_child(Sup, Id) of ok -> supervisor:delete_child(Sup, Id); _ -> ok end; (_) -> ok end, supervisor:which_children(Sup)); false -> ok end. -spec is_started(binary()) -> boolean(). is_started(Host) -> whereis(gen_mod:get_module_proc(Host, ?MODULE)) /= undefined. -spec get_pool_size(atom(), binary()) -> pos_integer(). get_pool_size(SQLType, Host) -> PoolSize = ejabberd_option:sql_pool_size(Host), if PoolSize > 1 andalso SQLType == sqlite -> ?WARNING_MSG("It's not recommended to set sql_pool_size > 1 for " "sqlite, because it may cause race conditions", []); true -> ok end, PoolSize. -spec child_spec(binary(), pos_integer()) -> supervisor:child_spec(). child_spec(Host, I) -> #{id => I, start => {ejabberd_sql, start_link, [Host, I]}, restart => transient, shutdown => 2000, type => worker, modules => [?MODULE]}. -spec child_specs(binary(), pos_integer()) -> [supervisor:child_spec()]. child_specs(Host, PoolSize) -> [child_spec(Host, I) || I <- lists:seq(1, PoolSize)]. check_sqlite_db(Host) -> DB = ejabberd_sql:sqlite_db(Host), File = ejabberd_sql:sqlite_file(Host), Ret = case filelib:ensure_dir(File) of ok -> case sqlite3:open(DB, [{file, File}]) of {ok, _Ref} -> ok; {error, {already_started, _Ref}} -> ok; {error, R} -> {error, R} end; Err -> Err end, case Ret of ok -> sqlite3:sql_exec(DB, "pragma foreign_keys = on"), case sqlite3:list_tables(DB) of [] -> create_sqlite_tables(DB), sqlite3:close(DB), ok; [_H | _] -> ok end; {error, Reason} -> ?WARNING_MSG("Failed open sqlite database, reason ~p", [Reason]) end. create_sqlite_tables(DB) -> SqlDir = misc:sql_dir(), Filename = case ejabberd_sql:use_new_schema() of true -> "lite.new.sql"; false -> "lite.sql" end, File = filename:join(SqlDir, Filename), case file:open(File, [read, binary]) of {ok, Fd} -> Qs = read_lines(Fd, File, []), ok = sqlite3:sql_exec(DB, "begin"), [ok = sqlite3:sql_exec(DB, Q) || Q <- Qs], ok = sqlite3:sql_exec(DB, "commit"); {error, Reason} -> ?WARNING_MSG("Failed to read SQLite schema file: ~ts", [file:format_error(Reason)]) end. read_lines(Fd, File, Acc) -> case file:read_line(Fd) of {ok, Line} -> NewAcc = case str:strip(str:strip(Line, both, $\r), both, $\n) of <<"--", _/binary>> -> Acc; <<>> -> Acc; _ -> [Line|Acc] end, read_lines(Fd, File, NewAcc); eof -> QueryList = str:tokens(list_to_binary(lists:reverse(Acc)), <<";">>), lists:flatmap( fun(Query) -> case str:strip(str:strip(Query, both, $\r), both, $\n) of <<>> -> []; Q -> [<>] end end, QueryList); {error, _} = Err -> ?ERROR_MSG("Failed read from lite.sql, reason: ~p", [Err]), [] end. ejabberd-21.12/src/ejabberd_mnesia.erl0000644000232200023220000003340714154362354020226 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mnesia_mnesia.erl %%% Author : Christophe Romain %%% Purpose : Handle configurable mnesia schema %%% Created : 17 Nov 2016 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% This module should be used everywhere ejabberd creates a mnesia table %%% to make the schema customizable without code change %%% Just apply this change in ejabberd modules %%% s/ejabberd_mnesia:create(?MODULE, /ejabberd_mnesia:create(?MODULE, / -module(ejabberd_mnesia). -author('christophe.romain@process-one.net'). -behaviour(gen_server). -export([start/0, create/3, update/2, transform/2, transform/3, dump_schema/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(STORAGE_TYPES, [disc_copies, disc_only_copies, ram_copies]). -define(NEED_RESET, [local_content, type]). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {tables = #{} :: tables(), schema = [] :: [{atom(), custom_schema()}]}). -type tables() :: #{atom() => {[{atom(), term()}], term()}}. -type custom_schema() :: [{ram_copies | disc_copies | disc_only_copies, [node()]} | {local_content, boolean()} | {type, set | ordered_set | bag} | {attributes, [atom()]} | {index, [atom()]}]. start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec create(module(), atom(), list()) -> any(). create(Module, Name, TabDef) -> gen_server:call(?MODULE, {create, Module, Name, TabDef}, %% Huge timeout is need to have enough %% time to transform huge tables timer:minutes(30)). init([]) -> ejabberd_config:env_binary_to_list(mnesia, dir), MyNode = node(), DbNodes = mnesia:system_info(db_nodes), case lists:member(MyNode, DbNodes) of true -> case mnesia:system_info(extra_db_nodes) of [] -> mnesia:create_schema([node()]); _ -> ok end, ejabberd:start_app(mnesia, permanent), ?DEBUG("Waiting for Mnesia tables synchronization...", []), mnesia:wait_for_tables(mnesia:system_info(local_tables), infinity), Schema = read_schema_file(), {ok, #state{schema = Schema}}; false -> ?CRITICAL_MSG("Node name mismatch: I'm [~ts], " "the database is owned by ~p", [MyNode, DbNodes]), ?CRITICAL_MSG("Either set ERLANG_NODE in ejabberdctl.cfg " "or change node name in Mnesia", []), {stop, node_name_mismatch} end. handle_call({create, Module, Name, TabDef}, _From, State) -> case maps:get(Name, State#state.tables, undefined) of {TabDef, Result} -> {reply, Result, State}; _ -> Result = do_create(Module, Name, TabDef, State#state.schema), Tables = maps:put(Name, {TabDef, Result}, State#state.tables), {reply, Result, State#state{tables = Tables}} end; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. do_create(Module, Name, TabDef, TabDefs) -> code:ensure_loaded(Module), Schema = schema(Name, TabDef, TabDefs), {attributes, Attrs} = lists:keyfind(attributes, 1, Schema), case catch mnesia:table_info(Name, attributes) of {'EXIT', _} -> create(Name, TabDef); Attrs -> case need_reset(Name, Schema) of true -> reset(Name, Schema); false -> case update(Name, Attrs, Schema) of {atomic, ok} -> transform(Module, Name, Attrs, Attrs); Err -> Err end end; OldAttrs -> transform(Module, Name, OldAttrs, Attrs) end. reset(Name, TabDef) -> ?INFO_MSG("Deleting Mnesia table '~ts'", [Name]), mnesia_op(delete_table, [Name]), create(Name, TabDef). update(Name, TabDef) -> {attributes, Attrs} = lists:keyfind(attributes, 1, TabDef), update(Name, Attrs, TabDef). update(Name, Attrs, TabDef) -> case change_table_copy_type(Name, TabDef) of {atomic, ok} -> CurrIndexes = [lists:nth(N-1, Attrs) || N <- mnesia:table_info(Name, index)], NewIndexes = proplists:get_value(index, TabDef, []), case delete_indexes(Name, CurrIndexes -- NewIndexes) of {atomic, ok} -> add_indexes(Name, NewIndexes -- CurrIndexes); Err -> Err end; Err -> Err end. change_table_copy_type(Name, TabDef) -> CurrType = mnesia:table_info(Name, storage_type), NewType = case lists:filter(fun is_storage_type_option/1, TabDef) of [{Type, _}|_] -> Type; [] -> CurrType end, if NewType /= CurrType -> ?INFO_MSG("Changing Mnesia table '~ts' from ~ts to ~ts", [Name, CurrType, NewType]), mnesia_op(change_table_copy_type, [Name, node(), NewType]); true -> {atomic, ok} end. delete_indexes(Name, [Index|Indexes]) -> ?INFO_MSG("Deleting index '~ts' from Mnesia table '~ts'", [Index, Name]), case mnesia_op(del_table_index, [Name, Index]) of {atomic, ok} -> delete_indexes(Name, Indexes); Err -> Err end; delete_indexes(_Name, []) -> {atomic, ok}. add_indexes(Name, [Index|Indexes]) -> ?INFO_MSG("Adding index '~ts' to Mnesia table '~ts'", [Index, Name]), case mnesia_op(add_table_index, [Name, Index]) of {atomic, ok} -> add_indexes(Name, Indexes); Err -> Err end; add_indexes(_Name, []) -> {atomic, ok}. % % utilities % schema(Name, Default, Schema) -> case lists:keyfind(Name, 1, Schema) of {_, Custom} -> TabDefs = merge(Custom, Default), ?DEBUG("Using custom schema for table '~ts': ~p", [Name, TabDefs]), TabDefs; false -> Default end. -spec read_schema_file() -> [{atom(), custom_schema()}]. read_schema_file() -> File = schema_path(), case fast_yaml:decode_from_file(File, [plain_as_atom]) of {ok, Y} -> case econf:validate(validator(), lists:flatten(Y)) of {ok, []} -> ?WARNING_MSG("Mnesia schema file ~ts is empty", [File]), []; {ok, Config} -> lists:map( fun({Tab, Opts}) -> {Tab, lists:map( fun({storage_type, T}) -> {T, [node()]}; (Other) -> Other end, Opts)} end, Config); {error, Reason, Ctx} -> ?ERROR_MSG("Failed to read Mnesia schema from ~ts: ~ts", [File, econf:format_error(Reason, Ctx)]), [] end; {error, enoent} -> ?DEBUG("No custom Mnesia schema file found at ~ts", [File]), []; {error, Reason} -> ?ERROR_MSG("Failed to read Mnesia schema file ~ts: ~ts", [File, fast_yaml:format_error(Reason)]) end. -spec validator() -> econf:validator(). validator() -> econf:map( econf:atom(), econf:options( #{storage_type => econf:enum([ram_copies, disc_copies, disc_only_copies]), local_content => econf:bool(), type => econf:enum([set, ordered_set, bag]), attributes => econf:list(econf:atom()), index => econf:list(econf:atom())}, [{return, orddict}, unique]), [unique]). create(Name, TabDef) -> Type = lists:foldl( fun({ram_copies, _}, _) -> " ram "; ({disc_copies, _}, _) -> " disc "; ({disc_only_copies, _}, _) -> " disc_only "; (_, Acc) -> Acc end, " ", TabDef), ?INFO_MSG("Creating Mnesia~tstable '~ts'", [Type, Name]), case mnesia_op(create_table, [Name, TabDef]) of {atomic, ok} -> add_table_copy(Name); Err -> Err end. %% The table MUST exist, otherwise the function would fail add_table_copy(Name) -> Type = mnesia:table_info(Name, storage_type), Nodes = mnesia:table_info(Name, Type), case lists:member(node(), Nodes) of true -> {atomic, ok}; false -> mnesia_op(add_table_copy, [Name, node(), Type]) end. merge(Custom, Default) -> NewDefault = case lists:any(fun is_storage_type_option/1, Custom) of true -> lists:filter( fun(O) -> not is_storage_type_option(O) end, Default); false -> Default end, lists:ukeymerge(1, Custom, lists:ukeysort(1, NewDefault)). need_reset(Table, TabDef) -> ValuesF = [mnesia:table_info(Table, Key) || Key <- ?NEED_RESET], ValuesT = [proplists:get_value(Key, TabDef) || Key <- ?NEED_RESET], lists:foldl( fun({Val, Val}, Acc) -> Acc; ({_, undefined}, Acc) -> Acc; ({_, _}, _) -> true end, false, lists:zip(ValuesF, ValuesT)). transform(Module, Name) -> try mnesia:table_info(Name, attributes) of Attrs -> transform(Module, Name, Attrs, Attrs) catch _:{aborted, _} = Err -> Err end. transform(Module, Name, NewAttrs) -> try mnesia:table_info(Name, attributes) of OldAttrs -> transform(Module, Name, OldAttrs, NewAttrs) catch _:{aborted, _} = Err -> Err end. transform(Module, Name, Attrs, Attrs) -> case need_transform(Module, Name) of true -> ?INFO_MSG("Transforming table '~ts', this may take a while", [Name]), transform_table(Module, Name); false -> {atomic, ok} end; transform(Module, Name, OldAttrs, NewAttrs) -> Fun = case erlang:function_exported(Module, transform, 1) of true -> transform_fun(Module, Name); false -> fun(Old) -> do_transform(OldAttrs, NewAttrs, Old) end end, mnesia_op(transform_table, [Name, Fun, NewAttrs]). -spec need_transform(module(), atom()) -> boolean(). need_transform(Module, Name) -> case erlang:function_exported(Module, need_transform, 1) of true -> do_need_transform(Module, Name, mnesia:dirty_first(Name)); false -> false end. do_need_transform(_Module, _Name, '$end_of_table') -> false; do_need_transform(Module, Name, Key) -> Objs = mnesia:dirty_read(Name, Key), case lists:foldl( fun(_, true) -> true; (Obj, _) -> Module:need_transform(Obj) end, undefined, Objs) of true -> true; false -> false; _ -> do_need_transform(Module, Name, mnesia:dirty_next(Name, Key)) end. do_transform(OldAttrs, Attrs, Old) -> [Name|OldValues] = tuple_to_list(Old), Before = lists:zip(OldAttrs, OldValues), After = lists:foldl( fun(Attr, Acc) -> case lists:keyfind(Attr, 1, Before) of false -> [{Attr, undefined}|Acc]; Value -> [Value|Acc] end end, [], lists:reverse(Attrs)), {Attrs, NewRecord} = lists:unzip(After), list_to_tuple([Name|NewRecord]). transform_fun(Module, Name) -> fun(Obj) -> try Module:transform(Obj) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to transform Mnesia table ~ts:~n" "** Record: ~p~n" "** ~ts", [Name, Obj, misc:format_exception(2, Class, Reason, StackTrace)]), erlang:raise(Class, Reason, StackTrace) end end. transform_table(Module, Name) -> Type = mnesia:table_info(Name, type), Attrs = mnesia:table_info(Name, attributes), TmpTab = list_to_atom(atom_to_list(Name) ++ "_backup"), StorageType = if Type == ordered_set -> disc_copies; true -> disc_only_copies end, mnesia:create_table(TmpTab, [{StorageType, [node()]}, {type, Type}, {local_content, true}, {record_name, Name}, {attributes, Attrs}]), mnesia:clear_table(TmpTab), Fun = transform_fun(Module, Name), Res = mnesia_op( transaction, [fun() -> do_transform_table(Name, Fun, TmpTab, mnesia:first(Name)) end]), mnesia:delete_table(TmpTab), Res. do_transform_table(Name, _Fun, TmpTab, '$end_of_table') -> mnesia:foldl( fun(Obj, _) -> mnesia:write(Name, Obj, write) end, ok, TmpTab); do_transform_table(Name, Fun, TmpTab, Key) -> Next = mnesia:next(Name, Key), Objs = mnesia:read(Name, Key), lists:foreach( fun(Obj) -> mnesia:write(TmpTab, Fun(Obj), write), mnesia:delete_object(Obj) end, Objs), do_transform_table(Name, Fun, TmpTab, Next). mnesia_op(Fun, Args) -> case apply(mnesia, Fun, Args) of {atomic, ok} -> {atomic, ok}; Other -> ?ERROR_MSG("Failure on mnesia ~ts ~p: ~p", [Fun, Args, Other]), Other end. schema_path() -> Dir = case os:getenv("EJABBERD_MNESIA_SCHEMA") of false -> mnesia:system_info(directory); Path -> Path end, filename:join(Dir, "ejabberd.schema"). is_storage_type_option({O, _}) -> O == ram_copies orelse O == disc_copies orelse O == disc_only_copies. dump_schema() -> File = schema_path(), Schema = lists:flatmap( fun(schema) -> []; (Tab) -> [{Tab, [{storage_type, mnesia:table_info(Tab, storage_type)}, {local_content, mnesia:table_info(Tab, local_content)}]}] end, mnesia:system_info(tables)), case file:write_file(File, [fast_yaml:encode(Schema), io_lib:nl()]) of ok -> io:format("Mnesia schema is written to ~ts~n", [File]); {error, Reason} -> io:format("Failed to write Mnesia schema to ~ts: ~ts", [File, file:format_error(Reason)]) end. ejabberd-21.12/src/mqtt_codec.erl0000644000232200023220000015756314154362354017270 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mqtt_codec). %% API -export([new/1, new/2, renew/1, decode/2, encode/2]). -export([pp/1, pp/2, format_error/1, format_reason_code/1]). -export([error_reason_code/1, is_error_code/1]). %% Validators -export([topic/1, topic_filter/1, qos/1, utf8/1]). -export([decode_varint/1]). -include("mqtt.hrl"). -define(MAX_UINT16, 65535). -define(MAX_UINT32, 4294967295). -define(MAX_VARINT, 268435456). -record(codec_state, {version :: undefined | mqtt_version(), type :: undefined | non_neg_integer(), flags :: undefined | non_neg_integer(), size :: undefined | non_neg_integer(), max_size :: pos_integer() | infinity, buf = <<>> :: binary()}). -type error_reason() :: bad_varint | {payload_too_big, integer()} | {bad_packet_type, char()} | {bad_packet, atom()} | {unexpected_packet, atom()} | {bad_reason_code, atom(), char()} | {bad_properties, atom()} | {bad_property, atom(), atom()} | {duplicated_property, atom(), atom()} | bad_will_topic_or_message | bad_connect_username_or_password | bad_publish_id_or_payload | {bad_topic_filters, atom()} | {bad_qos, char()} | bad_topic | bad_topic_filter | bad_utf8_string | {unsupported_protocol_name, binary(), binary()} | {unsupported_protocol_version, char(), iodata()} | {{bad_flag, atom()}, char(), term()} | {{bad_flags, atom()}, char(), char()}. -opaque state() :: #codec_state{}. -export_type([state/0, error_reason/0]). %%%=================================================================== %%% API %%%=================================================================== -spec new(pos_integer() | infinity) -> state(). new(MaxSize) -> new(MaxSize, undefined). -spec new(pos_integer() | infinity, undefined | mqtt_version()) -> state(). new(MaxSize, Version) -> #codec_state{max_size = MaxSize, version = Version}. -spec renew(state()) -> state(). renew(#codec_state{version = Version, max_size = MaxSize}) -> #codec_state{version = Version, max_size = MaxSize}. -spec decode(state(), binary()) -> {ok, mqtt_packet(), state()} | {more, state()} | {error, error_reason()}. decode(#codec_state{size = undefined, buf = Buf} = State, Data) -> Buf1 = <>, case Buf1 of <> -> try case decode_varint(Data1) of {Len, _} when Len >= State#codec_state.max_size -> err({payload_too_big, State#codec_state.max_size}); {Len, Data2} when size(Data2) >= Len -> <> = Data2, Version = State#codec_state.version, Pkt = decode_pkt(Version, Type, Flags, Payload), State1 = case Pkt of #connect{proto_level = V} -> State#codec_state{version = V}; _ -> State end, {ok, Pkt, State1#codec_state{buf = Data3}}; {Len, Data2} -> {more, State#codec_state{type = Type, flags = Flags, size = Len, buf = Data2}}; more -> {more, State#codec_state{buf = Buf1}} end catch _:{?MODULE, Why} -> {error, Why} end; <<>> -> {more, State} end; decode(#codec_state{size = Len, buf = Buf, version = Version, type = Type, flags = Flags} = State, Data) -> Buf1 = <>, if size(Buf1) >= Len -> <> = Buf1, try Pkt = decode_pkt(Version, Type, Flags, Payload), State1 = case Pkt of #connect{proto_level = V} -> State#codec_state{version = V}; _ -> State end, {ok, Pkt, State1#codec_state{type = undefined, flags = undefined, size = undefined, buf = Data1}} catch _:{?MODULE, Why} -> {error, Why} end; true -> {more, State#codec_state{buf = Buf1}} end. -spec encode(mqtt_version(), mqtt_packet()) -> binary(). encode(Version, Pkt) -> case Pkt of #connect{proto_level = Version} -> encode_connect(Pkt); #connack{} -> encode_connack(Version, Pkt); #publish{} -> encode_publish(Version, Pkt); #puback{} -> encode_puback(Version, Pkt); #pubrec{} -> encode_pubrec(Version, Pkt); #pubrel{} -> encode_pubrel(Version, Pkt); #pubcomp{} -> encode_pubcomp(Version, Pkt); #subscribe{} -> encode_subscribe(Version, Pkt); #suback{} -> encode_suback(Version, Pkt); #unsubscribe{} -> encode_unsubscribe(Version, Pkt); #unsuback{} -> encode_unsuback(Version, Pkt); #pingreq{} -> encode_pingreq(); #pingresp{} -> encode_pingresp(); #disconnect{} -> encode_disconnect(Version, Pkt); #auth{} -> encode_auth(Pkt) end. -spec pp(any()) -> iolist(). pp(Term) -> io_lib_pretty:print(Term, fun pp/2). -spec format_error(error_reason()) -> string(). format_error({payload_too_big, Max}) -> format("Payload exceeds ~B bytes", [Max]); format_error(bad_varint) -> "Variable Integer is out of boundaries"; format_error({bad_packet_type, Type}) -> format("Unexpected packet type: ~B", [Type]); format_error({bad_packet, Name}) -> format("Malformed ~ts packet", [string:to_upper(atom_to_list(Name))]); format_error({unexpected_packet, Name}) -> format("Unexpected ~ts packet", [string:to_upper(atom_to_list(Name))]); format_error({bad_reason_code, Name, Code}) -> format("Unexpected reason code in ~ts code: ~B", [string:to_upper(atom_to_list(Name)), Code]); format_error({bad_properties, Name}) -> format("Malformed properties of ~ts packet", [string:to_upper(atom_to_list(Name))]); format_error({bad_property, Pkt, Prop}) -> format("Malformed property ~ts of ~ts packet", [Prop, string:to_upper(atom_to_list(Pkt))]); format_error({duplicated_property, Pkt, Prop}) -> format("Property ~ts is included more than once into ~ts packet", [Prop, string:to_upper(atom_to_list(Pkt))]); format_error(bad_will_topic_or_message) -> "Malformed Will Topic or Will Message"; format_error(bad_connect_username_or_password) -> "Malformed username or password of CONNECT packet"; format_error(bad_publish_id_or_payload) -> "Malformed id or payload of PUBLISH packet"; format_error({bad_topic_filters, Name}) -> format("Malformed topic filters of ~ts packet", [string:to_upper(atom_to_list(Name))]); format_error({bad_qos, Q}) -> format_got_expected("Malformed QoS value", Q, "0, 1 or 2"); format_error(bad_topic) -> "Malformed topic"; format_error(bad_topic_filter) -> "Malformed topic filter"; format_error(bad_utf8_string) -> "Malformed UTF-8 string"; format_error({unsupported_protocol_name, Got, Expected}) -> format_got_expected("Unsupported protocol name", Got, Expected); format_error({unsupported_protocol_version, Got, Expected}) -> format_got_expected("Unsupported protocol version", Got, Expected); format_error({{bad_flag, Name}, Got, Expected}) -> Txt = "Unexpected " ++ atom_to_list(Name) ++ " flag", format_got_expected(Txt, Got, Expected); format_error({{bad_flags, Name}, Got, Expected}) -> Txt = "Unexpected " ++ string:to_upper(atom_to_list(Name)) ++ " flags", format_got_expected(Txt, Got, Expected); format_error(Reason) -> format("Unexpected error: ~w", [Reason]). -spec error_reason_code(error_reason()) -> reason_code(). error_reason_code({unsupported_protocol_name, _, _}) -> 'unsupported-protocol-version'; error_reason_code({unsupported_protocol_version, _, _}) -> 'unsupported-protocol-version'; error_reason_code({payload_too_big, _}) -> 'packet-too-large'; error_reason_code({unexpected_packet, _}) -> 'protocol-error'; error_reason_code(_) -> 'malformed-packet'. -spec format_reason_code(reason_code()) -> string(). format_reason_code('success') -> "Success"; format_reason_code('normal-disconnection') -> "Normal disconnection"; format_reason_code('granted-qos-0') -> "Granted QoS 0"; format_reason_code('granted-qos-1') -> "Granted QoS 1"; format_reason_code('granted-qos-2') -> "Granted QoS 2"; format_reason_code('no-matching-subscribers') -> "No matching subscribers"; format_reason_code('no-subscription-existed') -> "No subscription existed"; format_reason_code('continue-authentication') -> "Continue authentication"; format_reason_code('re-authenticate') -> "Re-authenticate"; format_reason_code('unspecified-error') -> "Unspecified error"; format_reason_code('malformed-packet') -> "Malformed Packet"; format_reason_code('protocol-error') -> "Protocol Error"; format_reason_code('bad-user-name-or-password') -> "Bad User Name or Password"; format_reason_code('not-authorized') -> "Not authorized"; format_reason_code('server-unavailable') -> "Server unavailable"; format_reason_code('server-busy') -> "Server busy"; format_reason_code('banned') -> "Banned"; format_reason_code('server-shutting-down') -> "Server shutting down"; format_reason_code('bad-authentication-method') -> "Bad authentication method"; format_reason_code('keep-alive-timeout') -> "Keep Alive timeout"; format_reason_code('session-taken-over') -> "Session taken over"; format_reason_code('topic-filter-invalid') -> "Topic Filter invalid"; format_reason_code('topic-name-invalid') -> "Topic Name invalid"; format_reason_code('packet-identifier-in-use') -> "Packet Identifier in use"; format_reason_code('receive-maximum-exceeded') -> "Receive Maximum exceeded"; format_reason_code('topic-alias-invalid') -> "Topic Alias invalid"; format_reason_code('packet-too-large') -> "Packet too large"; format_reason_code('message-rate-too-high') -> "Message rate too high"; format_reason_code('quota-exceeded') -> "Quota exceeded"; format_reason_code('administrative-action') -> "Administrative action"; format_reason_code('payload-format-invalid') -> "Payload format invalid"; format_reason_code('retain-not-supported') -> "Retain not supported"; format_reason_code('qos-not-supported') -> "QoS not supported"; format_reason_code('use-another-server') -> "Use another server"; format_reason_code('server-moved') -> "Server moved"; format_reason_code('connection-rate-exceeded') -> "Connection rate exceeded"; format_reason_code('maximum-connect-time') -> "Maximum connect time"; format_reason_code('unsupported-protocol-version') -> "Unsupported Protocol Version"; format_reason_code('client-identifier-not-valid') -> "Client Identifier not valid"; format_reason_code('packet-identifier-not-found') -> "Packet Identifier not found"; format_reason_code('disconnect-with-will-message') -> "Disconnect with Will Message"; format_reason_code('implementation-specific-error') -> "Implementation specific error"; format_reason_code('shared-subscriptions-not-supported') -> "Shared Subscriptions not supported"; format_reason_code('subscription-identifiers-not-supported') -> "Subscription Identifiers not supported"; format_reason_code('wildcard-subscriptions-not-supported') -> "Wildcard Subscriptions not supported"; format_reason_code(Code) -> format("Unexpected error: ~w", [Code]). -spec is_error_code(char() | reason_code()) -> boolean(). is_error_code('success') -> false; is_error_code('normal-disconnection') -> false; is_error_code('granted-qos-0') -> false; is_error_code('granted-qos-1') -> false; is_error_code('granted-qos-2') -> false; is_error_code('disconnect-with-will-message') -> false; is_error_code('no-matching-subscribers') -> false; is_error_code('no-subscription-existed') -> false; is_error_code('continue-authentication') -> false; is_error_code('re-authenticate') -> false; is_error_code(Code) when is_integer(Code) -> Code >= 128; is_error_code(_) -> true. %%%=================================================================== %%% Decoder %%%=================================================================== -spec decode_varint(binary()) -> {non_neg_integer(), binary()} | more. decode_varint(Data) -> decode_varint(Data, 0, 1). -spec decode_varint(binary(), non_neg_integer(), pos_integer()) -> {non_neg_integer(), binary()} | more. decode_varint(<>, Val, Mult) -> NewVal = Val + (C band 127) * Mult, NewMult = Mult*128, if NewMult > ?MAX_VARINT -> err(bad_varint); (C band 128) == 0 -> {NewVal, Data}; true -> decode_varint(Data, NewVal, NewMult) end; decode_varint(_, _, _) -> more. -spec decode_pkt(mqtt_version() | undefined, non_neg_integer(), non_neg_integer(), binary()) -> mqtt_packet(). decode_pkt(undefined, 1, Flags, Data) -> decode_connect(Flags, Data); decode_pkt(Version, Type, Flags, Data) when Version /= undefined, Type>1 -> case Type of 2 -> decode_connack(Version, Flags, Data); 3 -> decode_publish(Version, Flags, Data); 4 -> decode_puback(Version, Flags, Data); 5 -> decode_pubrec(Version, Flags, Data); 6 -> decode_pubrel(Version, Flags, Data); 7 -> decode_pubcomp(Version, Flags, Data); 8 -> decode_subscribe(Version, Flags, Data); 9 -> decode_suback(Version, Flags, Data); 10 -> decode_unsubscribe(Version, Flags, Data); 11 -> decode_unsuback(Version, Flags, Data); 12 -> decode_pingreq(Flags, Data); 13 -> decode_pingresp(Flags, Data); 14 -> decode_disconnect(Version, Flags, Data); 15 when Version == ?MQTT_VERSION_5 -> decode_auth(Flags, Data); _ -> err({bad_packet_type, Type}) end; decode_pkt(_, Type, _, _) -> err({unexpected_packet, decode_packet_type(Type)}). -spec decode_connect(non_neg_integer(), binary()) -> connect(). decode_connect(Flags, <>) -> assert(Proto, <<"MQTT">>, unsupported_protocol_name), if ProtoLevel == ?MQTT_VERSION_4; ProtoLevel == ?MQTT_VERSION_5 -> decode_connect(ProtoLevel, Flags, Data); true -> err({unsupported_protocol_version, ProtoLevel, "4 or 5"}) end; decode_connect(_, _) -> err({bad_packet, connect}). -spec decode_connect(mqtt_version(), non_neg_integer(), binary()) -> connect(). decode_connect(Version, Flags, <>) -> assert(Flags, 0, {bad_flags, connect}), assert(Reserved, 0, {bad_flag, reserved}), {Props, Data1} = case Version of ?MQTT_VERSION_5 -> decode_props(connect, Data); ?MQTT_VERSION_4 -> {#{}, Data} end, case Data1 of <> -> {Will, WillProps, Data3} = decode_will(Version, WillFlag, WillRetain, WillQoS, Data2), {Username, Password} = decode_user_pass(UserFlag, PassFlag, Data3), #connect{proto_level = Version, will = Will, will_properties = WillProps, properties = Props, clean_start = dec_bool(CleanStart), keep_alive = KeepAlive, client_id = utf8(ClientID), username = utf8(Username), password = Password}; _ -> err({bad_packet, connect}) end; decode_connect(_, _, _) -> err({bad_packet, connect}). -spec decode_connack(mqtt_version(), non_neg_integer(), binary()) -> connack(). decode_connack(Version, Flags, <<0:7, SessionPresent:1, Data/binary>>) -> assert(Flags, 0, {bad_flags, connack}), {Code, PropMap} = decode_code_with_props(Version, connack, Data), #connack{session_present = dec_bool(SessionPresent), code = Code, properties = PropMap}; decode_connack(_, _, _) -> err({bad_packet, connack}). -spec decode_publish(mqtt_version(), non_neg_integer(), binary()) -> publish(). decode_publish(Version, Flags, <>) -> Retain = Flags band 1, QoS = qos((Flags bsr 1) band 3), DUP = Flags band 8, {ID, Props, Payload} = decode_id_props_payload(Version, QoS, Data), #publish{dup = dec_bool(DUP), qos = QoS, retain = dec_bool(Retain), topic = topic(Topic, Props), id = ID, properties = Props, payload = Payload}; decode_publish(_, _, _) -> err({bad_packet, publish}). -spec decode_puback(mqtt_version(), non_neg_integer(), binary()) -> puback(). decode_puback(Version, Flags, <>) when ID>0 -> assert(Flags, 0, {bad_flags, puback}), {Code, PropMap} = decode_code_with_props(Version, puback, Data), #puback{id = ID, code = Code, properties = PropMap}; decode_puback(_, _, _) -> err({bad_packet, puback}). -spec decode_pubrec(mqtt_version(), non_neg_integer(), binary()) -> pubrec(). decode_pubrec(Version, Flags, <>) when ID>0 -> assert(Flags, 0, {bad_flags, pubrec}), {Code, PropMap} = decode_code_with_props(Version, pubrec, Data), #pubrec{id = ID, code = Code, properties = PropMap}; decode_pubrec(_, _, _) -> err({bad_packet, pubrec}). -spec decode_pubrel(mqtt_version(), non_neg_integer(), binary()) -> pubrel(). decode_pubrel(Version, Flags, <>) when ID>0 -> assert(Flags, 2, {bad_flags, pubrel}), {Code, PropMap} = decode_code_with_props(Version, pubrel, Data), #pubrel{id = ID, code = Code, properties = PropMap}; decode_pubrel(_, _, _) -> err({bad_packet, pubrel}). -spec decode_pubcomp(mqtt_version(), non_neg_integer(), binary()) -> pubcomp(). decode_pubcomp(Version, Flags, <>) when ID>0 -> assert(Flags, 0, {bad_flags, pubcomp}), {Code, PropMap} = decode_code_with_props(Version, pubcomp, Data), #pubcomp{id = ID, code = Code, properties = PropMap}; decode_pubcomp(_, _, _) -> err({bad_packet, pubcomp}). -spec decode_subscribe(mqtt_version(), non_neg_integer(), binary()) -> subscribe(). decode_subscribe(Version, Flags, <>) when ID>0 -> assert(Flags, 2, {bad_flags, subscribe}), case Version of ?MQTT_VERSION_4 -> Filters = decode_subscribe_filters(Data), #subscribe{id = ID, filters = Filters}; ?MQTT_VERSION_5 -> {Props, Payload} = decode_props(subscribe, Data), Filters = decode_subscribe_filters(Payload), #subscribe{id = ID, filters = Filters, properties = Props} end; decode_subscribe(_, _, _) -> err({bad_packet, subscribe}). -spec decode_suback(mqtt_version(), non_neg_integer(), binary()) -> suback(). decode_suback(Version, Flags, <>) when ID>0 -> assert(Flags, 0, {bad_flags, suback}), case Version of ?MQTT_VERSION_4 -> #suback{id = ID, codes = decode_suback_codes(Data)}; ?MQTT_VERSION_5 -> {PropMap, Tail} = decode_props(suback, Data), #suback{id = ID, codes = decode_suback_codes(Tail), properties = PropMap} end; decode_suback(_, _, _) -> err({bad_packet, suback}). -spec decode_unsubscribe(mqtt_version(), non_neg_integer(), binary()) -> unsubscribe(). decode_unsubscribe(Version, Flags, <>) when ID>0 -> assert(Flags, 2, {bad_flags, unsubscribe}), case Version of ?MQTT_VERSION_4 -> Filters = decode_unsubscribe_filters(Data), #unsubscribe{id = ID, filters = Filters}; ?MQTT_VERSION_5 -> {Props, Payload} = decode_props(unsubscribe, Data), Filters = decode_unsubscribe_filters(Payload), #unsubscribe{id = ID, filters = Filters, properties = Props} end; decode_unsubscribe(_, _, _) -> err({bad_packet, unsubscribe}). -spec decode_unsuback(mqtt_version(), non_neg_integer(), binary()) -> unsuback(). decode_unsuback(Version, Flags, <>) when ID>0 -> assert(Flags, 0, {bad_flags, unsuback}), case Version of ?MQTT_VERSION_4 -> #unsuback{id = ID}; ?MQTT_VERSION_5 -> {PropMap, Tail} = decode_props(unsuback, Data), #unsuback{id = ID, codes = decode_unsuback_codes(Tail), properties = PropMap} end; decode_unsuback(_, _, _) -> err({bad_packet, unsuback}). -spec decode_pingreq(non_neg_integer(), binary()) -> pingreq(). decode_pingreq(Flags, <<>>) -> assert(Flags, 0, {bad_flags, pingreq}), #pingreq{}; decode_pingreq(_, _) -> err({bad_packet, pingreq}). -spec decode_pingresp(non_neg_integer(), binary()) -> pingresp(). decode_pingresp(Flags, <<>>) -> assert(Flags, 0, {bad_flags, pingresp}), #pingresp{}; decode_pingresp(_, _) -> err({bad_packet, pingresp}). -spec decode_disconnect(mqtt_version(), non_neg_integer(), binary()) -> disconnect(). decode_disconnect(Version, Flags, Payload) -> assert(Flags, 0, {bad_flags, disconnect}), {Code, PropMap} = decode_code_with_props(Version, disconnect, Payload), #disconnect{code = Code, properties = PropMap}. -spec decode_auth(non_neg_integer(), binary()) -> auth(). decode_auth(Flags, Payload) -> assert(Flags, 0, {bad_flags, auth}), {Code, PropMap} = decode_code_with_props(?MQTT_VERSION_5, auth, Payload), #auth{code = Code, properties = PropMap}. -spec decode_packet_type(char()) -> atom(). decode_packet_type(1) -> connect; decode_packet_type(2) -> connack; decode_packet_type(3) -> publish; decode_packet_type(4) -> puback; decode_packet_type(5) -> pubrec; decode_packet_type(6) -> pubrel; decode_packet_type(7) -> pubcomp; decode_packet_type(8) -> subscribe; decode_packet_type(9) -> suback; decode_packet_type(10) -> unsubscribe; decode_packet_type(11) -> unsuback; decode_packet_type(12) -> pingreq; decode_packet_type(13) -> pingresp; decode_packet_type(14) -> disconnect; decode_packet_type(15) -> auth; decode_packet_type(T) -> err({bad_packet_type, T}). -spec decode_will(mqtt_version(), 0|1, 0|1, qos(), binary()) -> {undefined | publish(), properties(), binary()}. decode_will(_, 0, WillRetain, WillQoS, Data) -> assert(WillRetain, 0, {bad_flag, will_retain}), assert(WillQoS, 0, {bad_flag, will_qos}), {undefined, #{}, Data}; decode_will(Version, 1, WillRetain, WillQoS, Data) -> {Props, Data1} = case Version of ?MQTT_VERSION_5 -> decode_props(connect, Data); ?MQTT_VERSION_4 -> {#{}, Data} end, case Data1 of <> -> {#publish{retain = dec_bool(WillRetain), qos = qos(WillQoS), topic = topic(Topic), payload = Message}, Props, Data2}; _ -> err(bad_will_topic_or_message) end. -spec decode_user_pass(non_neg_integer(), non_neg_integer(), binary()) -> {binary(), binary()}. decode_user_pass(1, 0, <>) -> {utf8(User), <<>>}; decode_user_pass(1, 1, <>) -> {utf8(User), Pass}; decode_user_pass(0, Flag, <<>>) -> assert(Flag, 0, {bad_flag, password}), {<<>>, <<>>}; decode_user_pass(_, _, _) -> err(bad_connect_username_or_password). -spec decode_id_props_payload(mqtt_version(), non_neg_integer(), binary()) -> {undefined | non_neg_integer(), properties(), binary()}. decode_id_props_payload(Version, 0, Data) -> case Version of ?MQTT_VERSION_4 -> {undefined, #{}, Data}; ?MQTT_VERSION_5 -> {Props, Payload} = decode_props(publish, Data), {undefined, Props, Payload} end; decode_id_props_payload(Version, _, <>) when ID>0 -> case Version of ?MQTT_VERSION_4 -> {ID, #{}, Data}; ?MQTT_VERSION_5 -> {Props, Payload} = decode_props(publish, Data), {ID, Props, Payload} end; decode_id_props_payload(_, _, _) -> err(bad_publish_id_or_payload). -spec decode_subscribe_filters(binary()) -> [{binary(), sub_opts()}]. decode_subscribe_filters(<>) -> assert(Reserved, 0, {bad_flag, reserved}), case RH of 3 -> err({{bad_flag, retain_handling}, RH, "0, 1 or 2"}); _ -> ok end, Opts = #sub_opts{qos = qos(QoS), no_local = dec_bool(NL), retain_as_published = dec_bool(RAP), retain_handling = RH}, [{topic_filter(Filter), Opts}|decode_subscribe_filters(Tail)]; decode_subscribe_filters(<<>>) -> []; decode_subscribe_filters(_) -> err({bad_topic_filters, subscribe}). -spec decode_unsubscribe_filters(binary()) -> [binary()]. decode_unsubscribe_filters(<>) -> [topic_filter(Filter)|decode_unsubscribe_filters(Tail)]; decode_unsubscribe_filters(<<>>) -> []; decode_unsubscribe_filters(_) -> err({bad_topic_filters, unsubscribe}). -spec decode_suback_codes(binary()) -> [reason_code()]. decode_suback_codes(<>) -> [decode_suback_code(Code)|decode_suback_codes(Data)]; decode_suback_codes(<<>>) -> []. -spec decode_unsuback_codes(binary()) -> [reason_code()]. decode_unsuback_codes(<>) -> [decode_unsuback_code(Code)|decode_unsuback_codes(Data)]; decode_unsuback_codes(<<>>) -> []. -spec decode_utf8_pair(binary()) -> {utf8_pair(), binary()}. decode_utf8_pair(<>) -> {{utf8(Name), utf8(Val)}, Tail}; decode_utf8_pair(_) -> err(bad_utf8_pair). -spec decode_props(atom(), binary()) -> {properties(), binary()}. decode_props(Pkt, Data) -> try {Len, Data1} = decode_varint(Data), <> = Data1, {decode_props(Pkt, PData, #{}), Tail} catch _:{badmatch, _} -> err({bad_properties, Pkt}) end. -spec decode_props(atom(), binary(), properties()) -> properties(). decode_props(_, <<>>, Props) -> Props; decode_props(Pkt, Data, Props) -> {Type, Payload} = decode_varint(Data), {Name, Val, Tail} = decode_prop(Pkt, Type, Payload), Props1 = maps:update_with( Name, fun(Vals) when is_list(Val) -> Vals ++ Val; (_) -> err({duplicated_property, Pkt, Name}) end, Val, Props), decode_props(Pkt, Tail, Props1). -spec decode_prop(atom(), char(), binary()) -> {property(), term(), binary()}. decode_prop(_, 18, <>) -> {assigned_client_identifier, utf8(Data), Bin}; decode_prop(_, 22, <>) -> {authentication_data, Data, Bin}; decode_prop(_, 21, <>) -> {authentication_method, utf8(Data), Bin}; decode_prop(_, 3, <>) -> {content_type, utf8(Data), Bin}; decode_prop(_, 9, <>) -> {correlation_data, Data, Bin}; decode_prop(_, 39, <>) when Size>0 -> {maximum_packet_size, Size, Bin}; decode_prop(Pkt, 36, <>) -> {maximum_qos, case QoS of 0 -> 0; 1 -> 1; _ -> err({bad_property, Pkt, maximum_qos}) end, Bin}; decode_prop(_, 2, <>) -> {message_expiry_interval, I, Bin}; decode_prop(Pkt, 1, <>) -> {payload_format_indicator, case I of 0 -> binary; 1 -> utf8; _ -> err({bad_property, Pkt, payload_format_indicator}) end, Bin}; decode_prop(_, 31, <>) -> {reason_string, utf8(Data), Bin}; decode_prop(_, 33, <>) when Max>0 -> {receive_maximum, Max, Bin}; decode_prop(Pkt, 23, Data) -> decode_bool_prop(Pkt, request_problem_information, Data); decode_prop(Pkt, 25, Data) -> decode_bool_prop(Pkt, request_response_information, Data); decode_prop(_, 26, <>) -> {response_information, utf8(Data), Bin}; decode_prop(_, 8, <>) -> {response_topic, topic(Data), Bin}; decode_prop(Pkt, 37, Data) -> decode_bool_prop(Pkt, retain_available, Data); decode_prop(_, 19, <>) -> {server_keep_alive, Secs, Bin}; decode_prop(_, 28, <>) -> {server_reference, utf8(Data), Bin}; decode_prop(_, 17, <>) -> {session_expiry_interval, I, Bin}; decode_prop(Pkt, 42, Data) -> decode_bool_prop(Pkt, shared_subscription_available, Data); decode_prop(Pkt, 11, Data) when Pkt == publish; Pkt == subscribe -> case decode_varint(Data) of {ID, Bin} when Pkt == publish -> {subscription_identifier, [ID], Bin}; {ID, Bin} when Pkt == subscribe -> {subscription_identifier, ID, Bin}; _ -> err({bad_property, publish, subscription_identifier}) end; decode_prop(Pkt, 41, Data) -> decode_bool_prop(Pkt, subscription_identifiers_available, Data); decode_prop(_, 35, <>) when Alias>0 -> {topic_alias, Alias, Bin}; decode_prop(_, 34, <>) -> {topic_alias_maximum, Max, Bin}; decode_prop(_, 38, Data) -> {Pair, Bin} = decode_utf8_pair(Data), {user_property, [Pair], Bin}; decode_prop(Pkt, 40, Data) -> decode_bool_prop(Pkt, wildcard_subscription_available, Data); decode_prop(_, 24, <>) -> {will_delay_interval, I, Bin}; decode_prop(Pkt, _, _) -> err({bad_properties, Pkt}). decode_bool_prop(Pkt, Name, <>) -> case Val of 0 -> {Name, false, Bin}; 1 -> {Name, true, Bin}; _ -> err({bad_property, Pkt, Name}) end; decode_bool_prop(Pkt, Name, _) -> err({bad_property, Pkt, Name}). -spec decode_code_with_props(mqtt_version(), atom(), binary()) -> {reason_code(), properties()}. decode_code_with_props(_, connack, <>) -> {decode_connack_code(Code), case Props of <<>> -> #{}; _ -> {PropMap, <<>>} = decode_props(connack, Props), PropMap end}; decode_code_with_props(_, Pkt, <<>>) -> {decode_reason_code(Pkt, 0), #{}}; decode_code_with_props(?MQTT_VERSION_5, Pkt, <>) -> {decode_reason_code(Pkt, Code), #{}}; decode_code_with_props(?MQTT_VERSION_5, Pkt, <>) -> {PropMap, <<>>} = decode_props(Pkt, Props), {decode_reason_code(Pkt, Code), PropMap}; decode_code_with_props(_, Pkt, _) -> err({bad_packet, Pkt}). -spec decode_pubcomp_code(char()) -> reason_code(). decode_pubcomp_code(0) -> 'success'; decode_pubcomp_code(146) -> 'packet-identifier-not-found'; decode_pubcomp_code(Code) -> err({bad_reason_code, pubcomp, Code}). -spec decode_pubrec_code(char()) -> reason_code(). decode_pubrec_code(0) -> 'success'; decode_pubrec_code(16) -> 'no-matching-subscribers'; decode_pubrec_code(128) -> 'unspecified-error'; decode_pubrec_code(131) -> 'implementation-specific-error'; decode_pubrec_code(135) -> 'not-authorized'; decode_pubrec_code(144) -> 'topic-name-invalid'; decode_pubrec_code(145) -> 'packet-identifier-in-use'; decode_pubrec_code(151) -> 'quota-exceeded'; decode_pubrec_code(153) -> 'payload-format-invalid'; decode_pubrec_code(Code) -> err({bad_reason_code, pubrec, Code}). -spec decode_disconnect_code(char()) -> reason_code(). decode_disconnect_code(0) -> 'normal-disconnection'; decode_disconnect_code(4) -> 'disconnect-with-will-message'; decode_disconnect_code(128) -> 'unspecified-error'; decode_disconnect_code(129) -> 'malformed-packet'; decode_disconnect_code(130) -> 'protocol-error'; decode_disconnect_code(131) -> 'implementation-specific-error'; decode_disconnect_code(135) -> 'not-authorized'; decode_disconnect_code(137) -> 'server-busy'; decode_disconnect_code(139) -> 'server-shutting-down'; decode_disconnect_code(140) -> 'bad-authentication-method'; decode_disconnect_code(141) -> 'keep-alive-timeout'; decode_disconnect_code(142) -> 'session-taken-over'; decode_disconnect_code(143) -> 'topic-filter-invalid'; decode_disconnect_code(144) -> 'topic-name-invalid'; decode_disconnect_code(147) -> 'receive-maximum-exceeded'; decode_disconnect_code(148) -> 'topic-alias-invalid'; decode_disconnect_code(149) -> 'packet-too-large'; decode_disconnect_code(150) -> 'message-rate-too-high'; decode_disconnect_code(151) -> 'quota-exceeded'; decode_disconnect_code(152) -> 'administrative-action'; decode_disconnect_code(153) -> 'payload-format-invalid'; decode_disconnect_code(154) -> 'retain-not-supported'; decode_disconnect_code(155) -> 'qos-not-supported'; decode_disconnect_code(156) -> 'use-another-server'; decode_disconnect_code(157) -> 'server-moved'; decode_disconnect_code(158) -> 'shared-subscriptions-not-supported'; decode_disconnect_code(159) -> 'connection-rate-exceeded'; decode_disconnect_code(160) -> 'maximum-connect-time'; decode_disconnect_code(161) -> 'subscription-identifiers-not-supported'; decode_disconnect_code(162) -> 'wildcard-subscriptions-not-supported'; decode_disconnect_code(Code) -> err({bad_reason_code, disconnect, Code}). -spec decode_auth_code(char()) -> reason_code(). decode_auth_code(0) -> 'success'; decode_auth_code(24) -> 'continue-authentication'; decode_auth_code(25) -> 're-authenticate'; decode_auth_code(Code) -> err({bad_reason_code, auth, Code}). -spec decode_suback_code(char()) -> 0..2 | reason_code(). decode_suback_code(0) -> 0; decode_suback_code(1) -> 1; decode_suback_code(2) -> 2; decode_suback_code(128) -> 'unspecified-error'; decode_suback_code(131) -> 'implementation-specific-error'; decode_suback_code(135) -> 'not-authorized'; decode_suback_code(143) -> 'topic-filter-invalid'; decode_suback_code(145) -> 'packet-identifier-in-use'; decode_suback_code(151) -> 'quota-exceeded'; decode_suback_code(158) -> 'shared-subscriptions-not-supported'; decode_suback_code(161) -> 'subscription-identifiers-not-supported'; decode_suback_code(162) -> 'wildcard-subscriptions-not-supported'; decode_suback_code(Code) -> err({bad_reason_code, suback, Code}). -spec decode_unsuback_code(char()) -> reason_code(). decode_unsuback_code(0) -> 'success'; decode_unsuback_code(17) -> 'no-subscription-existed'; decode_unsuback_code(128) -> 'unspecified-error'; decode_unsuback_code(131) -> 'implementation-specific-error'; decode_unsuback_code(135) -> 'not-authorized'; decode_unsuback_code(143) -> 'topic-filter-invalid'; decode_unsuback_code(145) -> 'packet-identifier-in-use'; decode_unsuback_code(Code) -> err({bad_reason_code, unsuback, Code}). -spec decode_puback_code(char()) -> reason_code(). decode_puback_code(0) -> 'success'; decode_puback_code(16) -> 'no-matching-subscribers'; decode_puback_code(128) -> 'unspecified-error'; decode_puback_code(131) -> 'implementation-specific-error'; decode_puback_code(135) -> 'not-authorized'; decode_puback_code(144) -> 'topic-name-invalid'; decode_puback_code(145) -> 'packet-identifier-in-use'; decode_puback_code(151) -> 'quota-exceeded'; decode_puback_code(153) -> 'payload-format-invalid'; decode_puback_code(Code) -> err({bad_reason_code, puback, Code}). -spec decode_pubrel_code(char()) -> reason_code(). decode_pubrel_code(0) -> 'success'; decode_pubrel_code(146) -> 'packet-identifier-not-found'; decode_pubrel_code(Code) -> err({bad_reason_code, pubrel, Code}). -spec decode_connack_code(char()) -> reason_code(). decode_connack_code(0) -> 'success'; decode_connack_code(1) -> 'unsupported-protocol-version'; decode_connack_code(2) -> 'client-identifier-not-valid'; decode_connack_code(3) -> 'server-unavailable'; decode_connack_code(4) -> 'bad-user-name-or-password'; decode_connack_code(5) -> 'not-authorized'; decode_connack_code(128) -> 'unspecified-error'; decode_connack_code(129) -> 'malformed-packet'; decode_connack_code(130) -> 'protocol-error'; decode_connack_code(131) -> 'implementation-specific-error'; decode_connack_code(132) -> 'unsupported-protocol-version'; decode_connack_code(133) -> 'client-identifier-not-valid'; decode_connack_code(134) -> 'bad-user-name-or-password'; decode_connack_code(135) -> 'not-authorized'; decode_connack_code(136) -> 'server-unavailable'; decode_connack_code(137) -> 'server-busy'; decode_connack_code(138) -> 'banned'; decode_connack_code(140) -> 'bad-authentication-method'; decode_connack_code(144) -> 'topic-name-invalid'; decode_connack_code(149) -> 'packet-too-large'; decode_connack_code(151) -> 'quota-exceeded'; decode_connack_code(153) -> 'payload-format-invalid'; decode_connack_code(154) -> 'retain-not-supported'; decode_connack_code(155) -> 'qos-not-supported'; decode_connack_code(156) -> 'use-another-server'; decode_connack_code(157) -> 'server-moved'; decode_connack_code(159) -> 'connection-rate-exceeded'; decode_connack_code(Code) -> err({bad_reason_code, connack, Code}). -spec decode_reason_code(atom(), char()) -> reason_code(). decode_reason_code(pubcomp, Code) -> decode_pubcomp_code(Code); decode_reason_code(pubrec, Code) -> decode_pubrec_code(Code); decode_reason_code(disconnect, Code) -> decode_disconnect_code(Code); decode_reason_code(auth, Code) -> decode_auth_code(Code); decode_reason_code(puback, Code) -> decode_puback_code(Code); decode_reason_code(pubrel, Code) -> decode_pubrel_code(Code); decode_reason_code(connack, Code) -> decode_connack_code(Code). %%%=================================================================== %%% Encoder %%%=================================================================== encode_connect(#connect{proto_level = Version, properties = Props, will = Will, will_properties = WillProps, clean_start = CleanStart, keep_alive = KeepAlive, client_id = ClientID, username = Username, password = Password}) -> UserFlag = Username /= <<>>, PassFlag = UserFlag andalso Password /= <<>>, WillFlag = is_record(Will, publish), WillRetain = WillFlag andalso Will#publish.retain, WillQoS = if WillFlag -> Will#publish.qos; true -> 0 end, Header = <<4:16, "MQTT", Version, (enc_bool(UserFlag)):1, (enc_bool(PassFlag)):1, (enc_bool(WillRetain)):1, WillQoS:2, (enc_bool(WillFlag)):1, (enc_bool(CleanStart)):1, 0:1, KeepAlive:16>>, EncClientID = <<(size(ClientID)):16, ClientID/binary>>, EncWill = encode_will(Will), EncUserPass = encode_user_pass(Username, Password), Payload = case Version of ?MQTT_VERSION_5 -> [Header, encode_props(Props), EncClientID, if WillFlag -> encode_props(WillProps); true -> <<>> end, EncWill, EncUserPass]; _ -> [Header, EncClientID, EncWill, EncUserPass] end, <<1:4, 0:4, (encode_with_len(Payload))/binary>>. encode_connack(Version, #connack{session_present = SP, code = Code, properties = Props}) -> Payload = [enc_bool(SP), encode_connack_code(Version, Code), encode_props(Version, Props)], <<2:4, 0:4, (encode_with_len(Payload))/binary>>. encode_publish(Version, #publish{qos = QoS, retain = Retain, dup = Dup, topic = Topic, id = ID, payload = Payload, properties = Props}) -> Data1 = <<(size(Topic)):16, Topic/binary>>, Data2 = case QoS of 0 -> <<>>; _ when ID>0 -> <> end, Data3 = encode_props(Version, Props), Data4 = encode_with_len([Data1, Data2, Data3, Payload]), <<3:4, (enc_bool(Dup)):1, QoS:2, (enc_bool(Retain)):1, Data4/binary>>. encode_puback(Version, #puback{id = ID, code = Code, properties = Props}) when ID>0 -> Data = encode_code_with_props(Version, Code, Props), <<4:4, 0:4, (encode_with_len([<>|Data]))/binary>>. encode_pubrec(Version, #pubrec{id = ID, code = Code, properties = Props}) when ID>0 -> Data = encode_code_with_props(Version, Code, Props), <<5:4, 0:4, (encode_with_len([<>|Data]))/binary>>. encode_pubrel(Version, #pubrel{id = ID, code = Code, properties = Props}) when ID>0 -> Data = encode_code_with_props(Version, Code, Props), <<6:4, 2:4, (encode_with_len([<>|Data]))/binary>>. encode_pubcomp(Version, #pubcomp{id = ID, code = Code, properties = Props}) when ID>0 -> Data = encode_code_with_props(Version, Code, Props), <<7:4, 0:4, (encode_with_len([<>|Data]))/binary>>. encode_subscribe(Version, #subscribe{id = ID, filters = [_|_] = Filters, properties = Props}) when ID>0 -> EncFilters = [<<(size(Filter)):16, Filter/binary, (encode_subscription_options(SubOpts))>> || {Filter, SubOpts} <- Filters], Payload = [<>, encode_props(Version, Props), EncFilters], <<8:4, 2:4, (encode_with_len(Payload))/binary>>. encode_suback(Version, #suback{id = ID, codes = Codes, properties = Props}) when ID>0 -> Payload = [<>, encode_props(Version, Props) |[encode_reason_code(Code) || Code <- Codes]], <<9:4, 0:4, (encode_with_len(Payload))/binary>>. encode_unsubscribe(Version, #unsubscribe{id = ID, filters = [_|_] = Filters, properties = Props}) when ID>0 -> EncFilters = [<<(size(Filter)):16, Filter/binary>> || Filter <- Filters], Payload = [<>, encode_props(Version, Props), EncFilters], <<10:4, 2:4, (encode_with_len(Payload))/binary>>. encode_unsuback(Version, #unsuback{id = ID, codes = Codes, properties = Props}) when ID>0 -> EncCodes = case Version of ?MQTT_VERSION_5 -> [encode_reason_code(Code) || Code <- Codes]; ?MQTT_VERSION_4 -> [] end, Payload = [<>, encode_props(Version, Props)|EncCodes], <<11:4, 0:4, (encode_with_len(Payload))/binary>>. encode_pingreq() -> <<12:4, 0:4, 0>>. encode_pingresp() -> <<13:4, 0:4, 0>>. encode_disconnect(Version, #disconnect{code = Code, properties = Props}) -> Data = encode_code_with_props(Version, Code, Props), <<14:4, 0:4, (encode_with_len(Data))/binary>>. encode_auth(#auth{code = Code, properties = Props}) -> Data = encode_code_with_props(?MQTT_VERSION_5, Code, Props), <<15:4, 0:4, (encode_with_len(Data))/binary>>. -spec encode_with_len(iodata()) -> binary(). encode_with_len(IOData) -> Data = iolist_to_binary(IOData), Len = encode_varint(size(Data)), <>. -spec encode_varint(non_neg_integer()) -> binary(). encode_varint(X) when X < 128 -> <<0:1, X:7>>; encode_varint(X) when X < ?MAX_VARINT -> <<1:1, (X rem 128):7, (encode_varint(X div 128))/binary>>. -spec encode_props(mqtt_version(), properties()) -> binary(). encode_props(?MQTT_VERSION_5, Props) -> encode_props(Props); encode_props(?MQTT_VERSION_4, _) -> <<>>. -spec encode_props(properties()) -> binary(). encode_props(Props) -> encode_with_len( maps:fold( fun(Name, Val, Acc) -> [encode_prop(Name, Val)|Acc] end, [], Props)). -spec encode_prop(property(), term()) -> iodata(). encode_prop(assigned_client_identifier, <<>>) -> <<>>; encode_prop(assigned_client_identifier, ID) -> <<18, (size(ID)):16, ID/binary>>; encode_prop(authentication_data, <<>>) -> <<>>; encode_prop(authentication_data, Data) -> <<22, (size(Data)):16, Data/binary>>; encode_prop(authentication_method, <<>>) -> <<>>; encode_prop(authentication_method, M) -> <<21, (size(M)):16, M/binary>>; encode_prop(content_type, <<>>) -> <<>>; encode_prop(content_type, T) -> <<3, (size(T)):16, T/binary>>; encode_prop(correlation_data, <<>>) -> <<>>; encode_prop(correlation_data, Data) -> <<9, (size(Data)):16, Data/binary>>; encode_prop(maximum_packet_size, Size) when Size>0, Size= <<39, Size:32>>; encode_prop(maximum_qos, QoS) when QoS>=0, QoS<2 -> <<36, QoS>>; encode_prop(message_expiry_interval, I) when I>=0, I= <<2, I:32>>; encode_prop(payload_format_indicator, binary) -> <<>>; encode_prop(payload_format_indicator, utf8) -> <<1, 1>>; encode_prop(reason_string, <<>>) -> <<>>; encode_prop(reason_string, S) -> <<31, (size(S)):16, S/binary>>; encode_prop(receive_maximum, Max) when Max>0, Max= <<33, Max:16>>; encode_prop(request_problem_information, true) -> <<>>; encode_prop(request_problem_information, false) -> <<23, 0>>; encode_prop(request_response_information, false) -> <<>>; encode_prop(request_response_information, true) -> <<25, 1>>; encode_prop(response_information, <<>>) -> <<>>; encode_prop(response_information, S) -> <<26, (size(S)):16, S/binary>>; encode_prop(response_topic, <<>>) -> <<>>; encode_prop(response_topic, T) -> <<8, (size(T)):16, T/binary>>; encode_prop(retain_available, true) -> <<>>; encode_prop(retain_available, false) -> <<37, 0>>; encode_prop(server_keep_alive, Secs) when Secs>=0, Secs= <<19, Secs:16>>; encode_prop(server_reference, <<>>) -> <<>>; encode_prop(server_reference, S) -> <<28, (size(S)):16, S/binary>>; encode_prop(session_expiry_interval, I) when I>=0, I= <<17, I:32>>; encode_prop(shared_subscription_available, true) -> <<>>; encode_prop(shared_subscription_available, false) -> <<42, 0>>; encode_prop(subscription_identifier, [_|_] = IDs) -> [encode_prop(subscription_identifier, ID) || ID <- IDs]; encode_prop(subscription_identifier, ID) when ID>0, ID <<11, (encode_varint(ID))/binary>>; encode_prop(subscription_identifiers_available, true) -> <<>>; encode_prop(subscription_identifiers_available, false) -> <<41, 0>>; encode_prop(topic_alias, Alias) when Alias>0, Alias= <<35, Alias:16>>; encode_prop(topic_alias_maximum, 0) -> <<>>; encode_prop(topic_alias_maximum, Max) when Max>0, Max= <<34, Max:16>>; encode_prop(user_property, Pairs) -> [<<38, (encode_utf8_pair(Pair))/binary>> || Pair <- Pairs]; encode_prop(wildcard_subscription_available, true) -> <<>>; encode_prop(wildcard_subscription_available, false) -> <<40, 0>>; encode_prop(will_delay_interval, 0) -> <<>>; encode_prop(will_delay_interval, I) when I>0, I= <<24, I:32>>. -spec encode_user_pass(binary(), binary()) -> binary(). encode_user_pass(User, Pass) when User /= <<>> andalso Pass /= <<>> -> <<(size(User)):16, User/binary, (size(Pass)):16, Pass/binary>>; encode_user_pass(User, _) when User /= <<>> -> <<(size(User)):16, User/binary>>; encode_user_pass(_, _) -> <<>>. -spec encode_will(undefined | publish()) -> binary(). encode_will(#publish{topic = Topic, payload = Payload}) -> <<(size(Topic)):16, Topic/binary, (size(Payload)):16, Payload/binary>>; encode_will(undefined) -> <<>>. encode_subscription_options(#sub_opts{qos = QoS, no_local = NL, retain_as_published = RAP, retain_handling = RH}) when QoS>=0, RH>=0, QoS<3, RH<3 -> (RH bsl 4) bor (enc_bool(RAP) bsl 3) bor (enc_bool(NL) bsl 2) bor QoS. -spec encode_code_with_props(mqtt_version(), reason_code(), properties()) -> [binary()]. encode_code_with_props(Version, Code, Props) -> if Version == ?MQTT_VERSION_4 orelse (Code == success andalso Props == #{}) -> []; Props == #{} -> [encode_reason_code(Code)]; true -> [encode_reason_code(Code), encode_props(Props)] end. -spec encode_utf8_pair({binary(), binary()}) -> binary(). encode_utf8_pair({Key, Val}) -> <<(size(Key)):16, Key/binary, (size(Val)):16, Val/binary>>. -spec encode_connack_code(mqtt_version(), atom()) -> char(). encode_connack_code(?MQTT_VERSION_5, Reason) -> encode_reason_code(Reason); encode_connack_code(_, success) -> 0; encode_connack_code(_, 'unsupported-protocol-version') -> 1; encode_connack_code(_, 'client-identifier-not-valid') -> 2; encode_connack_code(_, 'server-unavailable') -> 3; encode_connack_code(_, 'bad-user-name-or-password') -> 4; encode_connack_code(_, 'not-authorized') -> 5; encode_connack_code(_, _) -> 128. -spec encode_reason_code(char() | reason_code()) -> char(). encode_reason_code('success') -> 0; encode_reason_code('normal-disconnection') -> 0; encode_reason_code('granted-qos-0') -> 0; encode_reason_code('granted-qos-1') -> 1; encode_reason_code('granted-qos-2') -> 2; encode_reason_code('disconnect-with-will-message') -> 4; encode_reason_code('no-matching-subscribers') -> 16; encode_reason_code('no-subscription-existed') -> 17; encode_reason_code('continue-authentication') -> 24; encode_reason_code('re-authenticate') -> 25; encode_reason_code('unspecified-error') -> 128; encode_reason_code('malformed-packet') -> 129; encode_reason_code('protocol-error') -> 130; encode_reason_code('implementation-specific-error') -> 131; encode_reason_code('unsupported-protocol-version') -> 132; encode_reason_code('client-identifier-not-valid') -> 133; encode_reason_code('bad-user-name-or-password') -> 134; encode_reason_code('not-authorized') -> 135; encode_reason_code('server-unavailable') -> 136; encode_reason_code('server-busy') -> 137; encode_reason_code('banned') -> 138; encode_reason_code('server-shutting-down') -> 139; encode_reason_code('bad-authentication-method') -> 140; encode_reason_code('keep-alive-timeout') -> 141; encode_reason_code('session-taken-over') -> 142; encode_reason_code('topic-filter-invalid') -> 143; encode_reason_code('topic-name-invalid') -> 144; encode_reason_code('packet-identifier-in-use') -> 145; encode_reason_code('packet-identifier-not-found') -> 146; encode_reason_code('receive-maximum-exceeded') -> 147; encode_reason_code('topic-alias-invalid') -> 148; encode_reason_code('packet-too-large') -> 149; encode_reason_code('message-rate-too-high') -> 150; encode_reason_code('quota-exceeded') -> 151; encode_reason_code('administrative-action') -> 152; encode_reason_code('payload-format-invalid') -> 153; encode_reason_code('retain-not-supported') -> 154; encode_reason_code('qos-not-supported') -> 155; encode_reason_code('use-another-server') -> 156; encode_reason_code('server-moved') -> 157; encode_reason_code('shared-subscriptions-not-supported') -> 158; encode_reason_code('connection-rate-exceeded') -> 159; encode_reason_code('maximum-connect-time') -> 160; encode_reason_code('subscription-identifiers-not-supported') -> 161; encode_reason_code('wildcard-subscriptions-not-supported') -> 162; encode_reason_code(Code) when is_integer(Code) -> Code. %%%=================================================================== %%% Formatters %%%=================================================================== -spec pp(atom(), non_neg_integer()) -> [atom()] | no. pp(codec_state, 6) -> record_info(fields, codec_state); pp(connect, 9) -> record_info(fields, connect); pp(connack, 3) -> record_info(fields, connack); pp(publish, 8) -> record_info(fields, publish); pp(puback, 3) -> record_info(fields, puback); pp(pubrec, 3) -> record_info(fields, pubrec); pp(pubrel, 4) -> record_info(fields, pubrel); pp(pubcomp, 3) -> record_info(fields, pubcomp); pp(subscribe, 4) -> record_info(fields, subscribe); pp(suback, 3) -> record_info(fields, suback); pp(unsubscribe, 3) -> record_info(fields, unsubscribe); pp(unsuback, 1) -> record_info(fields, unsuback); pp(pingreq, 1) -> record_info(fields, pingreq); pp(pingresp, 0) -> record_info(fields, pingresp); pp(disconnect, 2) -> record_info(fields, disconnect); pp(sub_opts, 4) -> record_info(fields, sub_opts); pp(_, _) -> no. -spec format(io:format(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). format_got_expected(Txt, Got, Expected) -> FmtGot = term_format(Got), FmtExp = term_format(Expected), format("~ts: " ++ FmtGot ++ " (expected: " ++ FmtExp ++ ")", [Txt, Got, Expected]). term_format(I) when is_integer(I) -> "~B"; term_format(B) when is_binary(B) -> term_format(binary_to_list(B)); term_format(A) when is_atom(A) -> term_format(atom_to_list(A)); term_format(T) -> case io_lib:printable_latin1_list(T) of true -> "~ts"; false -> "~w" end. %%%=================================================================== %%% Validators %%%=================================================================== -spec assert(T, any(), any()) -> T. assert(Got, Got, _) -> Got; assert(Got, Expected, Reason) -> err({Reason, Got, Expected}). -spec qos(qos()) -> qos(). qos(QoS) when is_integer(QoS), QoS>=0, QoS<3 -> QoS; qos(QoS) -> err({bad_qos, QoS}). -spec topic(binary()) -> binary(). topic(Topic) -> topic(Topic, #{}). -spec topic(binary(), properties()) -> binary(). topic(<<>>, Props) -> case maps:is_key(topic_alias, Props) of true -> <<>>; false -> err(bad_topic) end; topic(Bin, _) when is_binary(Bin) -> ok = check_topic(Bin), ok = check_utf8(Bin), Bin; topic(_, _) -> err(bad_topic). -spec topic_filter(binary()) -> binary(). topic_filter(<<>>) -> err(bad_topic_filter); topic_filter(Bin) when is_binary(Bin) -> ok = check_topic_filter(Bin, $/), ok = check_utf8(Bin), Bin; topic_filter(_) -> err(bad_topic_filter). -spec utf8(binary()) -> binary(). utf8(Bin) -> ok = check_utf8(Bin), ok = check_zero(Bin), Bin. -spec check_topic(binary()) -> ok. check_topic(<>) when H == $#; H == $+; H == 0 -> err(bad_topic); check_topic(<<_, T/binary>>) -> check_topic(T); check_topic(<<>>) -> ok. -spec check_topic_filter(binary(), char()) -> ok. check_topic_filter(<<>>, _) -> ok; check_topic_filter(_, $#) -> err(bad_topic_filter); check_topic_filter(<<$#, _/binary>>, C) when C /= $/ -> err(bad_topic_filter); check_topic_filter(<<$+, _/binary>>, C) when C /= $/ -> err(bad_topic_filter); check_topic_filter(<>, $+) when C /= $/ -> err(bad_topic_filter); check_topic_filter(<<0, _/binary>>, _) -> err(bad_topic_filter); check_topic_filter(<>, _) -> check_topic_filter(T, H). -spec check_utf8(binary()) -> ok. check_utf8(Bin) -> case unicode:characters_to_binary(Bin, utf8) of UTF8Str when is_binary(UTF8Str) -> ok; _ -> err(bad_utf8_string) end. -spec check_zero(binary()) -> ok. check_zero(<<0, _/binary>>) -> err(bad_utf8_string); check_zero(<<_, T/binary>>) -> check_zero(T); check_zero(<<>>) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec dec_bool(non_neg_integer()) -> boolean(). dec_bool(0) -> false; dec_bool(_) -> true. -spec enc_bool(boolean()) -> 0..1. enc_bool(true) -> 1; enc_bool(false) -> 0. -spec err(any()) -> no_return(). err(Reason) -> erlang:error({?MODULE, Reason}). ejabberd-21.12/src/ejabberd_listener.erl0000644000232200023220000005314514154362354020600 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_listener.erl %%% Author : Alexey Shchepin %%% Purpose : Manage socket listener %%% Created : 16 Nov 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_listener). -behaviour(supervisor). -author('alexey@process-one.net'). -author('ekhramtsov@process-one.net'). -export([start_link/0, init/1, stop/0, start/3, init/3, start_listeners/0, start_listener/3, stop_listeners/0, add_listener/3, delete_listener/2, config_reloaded/0]). -export([listen_options/0, listen_opt_type/1, validator/0]). -export([tls_listeners/0]). -include("logger.hrl"). -type transport() :: tcp | udp. -type endpoint() :: {inet:port_number(), inet:ip_address(), transport()}. -type list_opts() :: [{atom(), term()}]. -type opts() :: #{atom() => term()}. -type listener() :: {endpoint(), module(), opts()}. -type sockmod() :: gen_tcp. -type socket() :: inet:socket(). -type state() :: term(). -export_type([listener/0]). -callback start(sockmod(), socket(), state()) -> {ok, pid()} | {error, any()} | ignore. -callback start_link(sockmod(), socket(), state()) -> {ok, pid()} | {error, any()} | ignore. -callback accept(pid()) -> any(). -callback listen_opt_type(atom()) -> econf:validator(). -callback listen_options() -> [{atom(), term()} | atom()]. -callback tcp_init(socket(), list_opts()) -> state(). -callback udp_init(socket(), list_opts()) -> state(). -optional_callbacks([listen_opt_type/1, tcp_init/2, udp_init/2]). -define(TCP_SEND_TIMEOUT, 15000). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init(_) -> _ = ets:new(?MODULE, [named_table, public]), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), Listeners = ejabberd_option:listen(), {ok, {{one_for_one, 10, 1}, listeners_childspec(Listeners)}}. stop() -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50), stop_listeners(), ejabberd_sup:stop_child(?MODULE). -spec listeners_childspec([listener()]) -> [supervisor:child_spec()]. listeners_childspec(Listeners) -> lists:map( fun({EndPoint, Module, Opts}) -> ets:insert(?MODULE, {EndPoint, Module, Opts}), {EndPoint, {?MODULE, start, [EndPoint, Module, Opts]}, transient, brutal_kill, worker, [?MODULE]} end, Listeners). -spec start_listeners() -> ok. start_listeners() -> Listeners = ejabberd_option:listen(), lists:foreach( fun(Spec) -> supervisor:start_child(?MODULE, Spec) end, listeners_childspec(Listeners)). -spec start(endpoint(), module(), opts()) -> term(). start(EndPoint, Module, Opts) -> proc_lib:start_link(?MODULE, init, [EndPoint, Module, Opts]). -spec init(endpoint(), module(), opts()) -> ok. init({_, _, Transport} = EndPoint, Module, AllOpts) -> {ModuleOpts, SockOpts} = split_opts(Transport, AllOpts), init(EndPoint, Module, ModuleOpts, SockOpts). -spec init(endpoint(), module(), opts(), [gen_tcp:option()]) -> ok. init({Port, _, udp} = EndPoint, Module, Opts, SockOpts) -> {Port2, ExtraOpts} = case Port of <<"unix:", Path/binary>> -> SO = lists:keydelete(ip, 1, SockOpts), file:delete(Path), {0, [{ip, {local, Path}} | SO]}; _ -> {Port, SockOpts} end, ExtraOpts2 = lists:keydelete(send_timeout, 1, ExtraOpts), case gen_udp:open(Port2, [binary, {active, false}, {reuseaddr, true} | ExtraOpts2]) of {ok, Socket} -> case inet:sockname(Socket) of {ok, {Addr, Port1}} -> proc_lib:init_ack({ok, self()}), case application:ensure_started(ejabberd) of ok -> ?INFO_MSG("Start accepting ~ts connections at ~ts for ~p", [format_transport(udp, Opts), format_endpoint({Port1, Addr, udp}), Module]), Opts1 = opts_to_list(Module, Opts), case erlang:function_exported(Module, udp_init, 2) of false -> udp_recv(Socket, Module, Opts1); true -> State = Module:udp_init(Socket, Opts1), udp_recv(Socket, Module, State) end; {error, _} -> ok end; {error, Reason} = Err -> report_socket_error(Reason, EndPoint, Module), proc_lib:init_ack(Err) end; {error, Reason} = Err -> report_socket_error(Reason, EndPoint, Module), proc_lib:init_ack(Err) end; init({Port, _, tcp} = EndPoint, Module, Opts, SockOpts) -> case listen_tcp(Port, SockOpts) of {ok, ListenSocket} -> case inet:sockname(ListenSocket) of {ok, {Addr, Port1}} -> proc_lib:init_ack({ok, self()}), case application:ensure_started(ejabberd) of ok -> Sup = start_module_sup(Module, Opts), Interval = maps:get(accept_interval, Opts), Proxy = maps:get(use_proxy_protocol, Opts), ?INFO_MSG("Start accepting ~ts connections at ~ts for ~p", [format_transport(tcp, Opts), format_endpoint({Port1, Addr, tcp}), Module]), Opts1 = opts_to_list(Module, Opts), case erlang:function_exported(Module, tcp_init, 2) of false -> accept(ListenSocket, Module, Opts1, Sup, Interval, Proxy); true -> State = Module:tcp_init(ListenSocket, Opts1), accept(ListenSocket, Module, State, Sup, Interval, Proxy) end; {error, _} -> ok end; {error, Reason} = Err -> report_socket_error(Reason, EndPoint, Module), proc_lib:init_ack(Err) end; {error, Reason} = Err -> report_socket_error(Reason, EndPoint, Module), proc_lib:init_ack(Err) end. -spec listen_tcp(inet:port_number(), [gen_tcp:option()]) -> {ok, inet:socket()} | {error, system_limit | inet:posix()}. listen_tcp(Port, SockOpts) -> {Port2, ExtraOpts} = case Port of <<"unix:", Path/binary>> -> SO = lists:keydelete(ip, 1, SockOpts), file:delete(Path), {0, [{ip, {local, Path}} | SO]}; _ -> {Port, SockOpts} end, Res = gen_tcp:listen(Port2, [binary, {packet, 0}, {active, false}, {reuseaddr, true}, {nodelay, true}, {send_timeout_close, true}, {keepalive, true} | ExtraOpts]), case Res of {ok, ListenSocket} -> {ok, ListenSocket}; {error, _} = Err -> Err end. -spec split_opts(transport(), opts()) -> {opts(), [gen_tcp:option()]}. split_opts(Transport, Opts) -> maps:fold( fun(Opt, Val, {ModOpts, SockOpts}) -> case OptVal = {Opt, Val} of {ip, _} -> {ModOpts, [OptVal|SockOpts]}; {backlog, _} when Transport == tcp -> {ModOpts, [OptVal|SockOpts]}; {backlog, _} -> {ModOpts, SockOpts}; _ -> {ModOpts#{Opt => Val}, SockOpts} end end, {#{}, []}, Opts). -spec accept(inet:socket(), module(), state(), atom(), non_neg_integer(), boolean()) -> no_return(). accept(ListenSocket, Module, State, Sup, Interval, Proxy) -> Arity = case erlang:function_exported(Module, start, 3) of true -> 3; false -> 2 end, accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity). -spec accept(inet:socket(), module(), state(), atom(), non_neg_integer(), boolean(), 2|3) -> no_return(). accept(ListenSocket, Module, State, Sup, Interval, Proxy, Arity) -> NewInterval = apply_rate_limit(Interval), case gen_tcp:accept(ListenSocket) of {ok, Socket} when Proxy -> case proxy_protocol:decode(gen_tcp, Socket, 10000) of {error, Err} -> ?ERROR_MSG("(~w) Proxy protocol parsing failed: ~ts", [ListenSocket, format_error(Err)]), gen_tcp:close(Socket); {undefined, undefined} -> gen_tcp:close(Socket); {{Addr, Port}, {PAddr, PPort}} = SP -> %% THIS IS WRONG State2 = [{sock_peer_name, SP} | State], Receiver = case start_connection(Module, Arity, Socket, State2, Sup) of {ok, RecvPid} -> RecvPid; _ -> gen_tcp:close(Socket), none end, ?INFO_MSG("(~p) Accepted proxied connection ~ts -> ~ts", [Receiver, ejabberd_config:may_hide_data( format_endpoint({PPort, PAddr, tcp})), format_endpoint({Port, Addr, tcp})]) end, accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity); {ok, Socket} -> case {inet:sockname(Socket), inet:peername(Socket)} of {{ok, {Addr, Port}}, {ok, {PAddr, PPort}}} -> Receiver = case start_connection(Module, Arity, Socket, State, Sup) of {ok, RecvPid} -> RecvPid; _ -> gen_tcp:close(Socket), none end, ?INFO_MSG("(~p) Accepted connection ~ts -> ~ts", [Receiver, ejabberd_config:may_hide_data( format_endpoint({PPort, PAddr, tcp})), format_endpoint({Port, Addr, tcp})]); _ -> gen_tcp:close(Socket) end, accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity); {error, Reason} -> ?ERROR_MSG("(~w) Failed TCP accept: ~ts", [ListenSocket, format_error(Reason)]), accept(ListenSocket, Module, State, Sup, NewInterval, Proxy, Arity) end. -spec udp_recv(inet:socket(), module(), state()) -> no_return(). udp_recv(Socket, Module, State) -> case gen_udp:recv(Socket, 0) of {ok, {Addr, Port, Packet}} -> case catch Module:udp_recv(Socket, Addr, Port, Packet, State) of {'EXIT', Reason} -> ?ERROR_MSG("Failed to process UDP packet:~n" "** Source: {~p, ~p}~n" "** Reason: ~p~n** Packet: ~p", [Addr, Port, Reason, Packet]), udp_recv(Socket, Module, State); NewState -> udp_recv(Socket, Module, NewState) end; {error, Reason} -> ?ERROR_MSG("Unexpected UDP error: ~ts", [format_error(Reason)]), throw({error, Reason}) end. -spec start_connection(module(), 2|3, inet:socket(), state(), atom()) -> {ok, pid()} | {error, any()} | ignore. start_connection(Module, Arity, Socket, State, Sup) -> Res = case Sup of undefined when Arity == 3 -> Module:start(gen_tcp, Socket, State); undefined -> Module:start({gen_tcp, Socket}, State); _ when Arity == 3 -> supervisor:start_child(Sup, [gen_tcp, Socket, State]); _ -> supervisor:start_child(Sup, [{gen_tcp, Socket}, State]) end, case Res of {ok, Pid} -> case gen_tcp:controlling_process(Socket, Pid) of ok -> Module:accept(Pid), {ok, Pid}; Err -> case Sup of undefined -> exit(Pid, kill); _ -> supervisor:terminate_child(Sup, Pid) end, Err end; Err -> Err end. -spec start_listener(endpoint(), module(), opts()) -> {ok, pid()} | {error, any()}. start_listener(EndPoint, Module, Opts) -> %% It is only required to start the supervisor in some cases. %% But it doesn't hurt to attempt to start it for any listener. %% So, it's normal (and harmless) that in most cases this %% call returns: {error, {already_started, pid()}} case start_listener_sup(EndPoint, Module, Opts) of {ok, _Pid} = R -> R; {error, {{'EXIT', {undef, [{M, _F, _A}|_]}}, _} = Error} -> ?ERROR_MSG("Error starting the ejabberd listener: ~p.~n" "It could not be loaded or is not an ejabberd listener.~n" "Error: ~p~n", [Module, Error]), {error, {module_not_available, M}}; {error, {already_started, Pid}} -> {ok, Pid}; {error, Error} -> {error, Error} end. -spec start_module_sup(module(), opts()) -> atom(). start_module_sup(Module, Opts) -> case maps:get(supervisor, Opts) of true -> Proc = list_to_atom(atom_to_list(Module) ++ "_sup"), ChildSpec = {Proc, {ejabberd_tmp_sup, start_link, [Proc, Module]}, permanent, infinity, supervisor, [ejabberd_tmp_sup]}, case supervisor:start_child(ejabberd_sup, ChildSpec) of {ok, _} -> Proc; {error, {already_started, _}} -> Proc; _ -> undefined end; false -> undefined end. -spec start_listener_sup(endpoint(), module(), opts()) -> {ok, pid()} | {error, any()}. start_listener_sup(EndPoint, Module, Opts) -> ChildSpec = {EndPoint, {?MODULE, start, [EndPoint, Module, Opts]}, transient, brutal_kill, worker, [?MODULE]}, supervisor:start_child(?MODULE, ChildSpec). -spec stop_listeners() -> ok. stop_listeners() -> Ports = ejabberd_option:listen(), lists:foreach( fun({PortIpNetp, Module, _Opts}) -> delete_listener(PortIpNetp, Module) end, Ports). -spec stop_listener(endpoint(), module(), opts()) -> ok | {error, any()}. stop_listener({_, _, Transport} = EndPoint, Module, Opts) -> case supervisor:terminate_child(?MODULE, EndPoint) of ok -> ?INFO_MSG("Stop accepting ~ts connections at ~ts for ~p", [format_transport(Transport, Opts), format_endpoint(EndPoint), Module]), ets:delete(?MODULE, EndPoint), supervisor:delete_child(?MODULE, EndPoint); Err -> Err end. -spec add_listener(endpoint(), module(), opts()) -> ok | {error, any()}. add_listener(EndPoint, Module, Opts) -> Opts1 = apply_defaults(Module, Opts), case start_listener(EndPoint, Module, Opts1) of {ok, _Pid} -> ok; {error, {already_started, _Pid}} -> {error, {already_started, EndPoint}}; {error, Error} -> {error, Error} end. -spec delete_listener(endpoint(), module()) -> ok | {error, any()}. delete_listener(EndPoint, Module) -> try ets:lookup_element(?MODULE, EndPoint, 3) of Opts -> stop_listener(EndPoint, Module, Opts) catch _:badarg -> ok end. -spec tls_listeners() -> [module()]. tls_listeners() -> lists:usort( lists:filtermap( fun({_, Module, #{tls := true}}) -> {true, Module}; ({_, Module, #{starttls := true}}) -> {true, Module}; (_) -> false end, ets:tab2list(?MODULE))). -spec config_reloaded() -> ok. config_reloaded() -> New = ejabberd_option:listen(), Old = ets:tab2list(?MODULE), lists:foreach( fun({EndPoint, Module, Opts}) -> case lists:keyfind(EndPoint, 1, New) of false -> stop_listener(EndPoint, Module, Opts); _ -> ok end end, Old), lists:foreach( fun({EndPoint, Module, Opts}) -> case lists:keyfind(EndPoint, 1, Old) of {_, Module, Opts} -> ok; {_, OldModule, OldOpts} -> _ = stop_listener(EndPoint, OldModule, OldOpts), ets:insert(?MODULE, {EndPoint, Module, Opts}), start_listener(EndPoint, Module, Opts); false -> ets:insert(?MODULE, {EndPoint, Module, Opts}), start_listener(EndPoint, Module, Opts) end end, New). -spec report_socket_error(inet:posix(), endpoint(), module()) -> ok. report_socket_error(Reason, EndPoint, Module) -> ?ERROR_MSG("Failed to open socket at ~ts for ~ts: ~ts", [format_endpoint(EndPoint), Module, format_error(Reason)]). -spec format_error(inet:posix() | atom()) -> string(). format_error(Reason) -> case inet:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); ReasonStr -> ReasonStr end. -spec format_endpoint(endpoint()) -> string(). format_endpoint({Port, IP, _Transport}) -> case Port of Unix when is_binary(Unix) -> <<"unix:", Unix/binary>>; _ -> IPStr = case tuple_size(IP) of 4 -> inet:ntoa(IP); 8 -> "[" ++ inet:ntoa(IP) ++ "]" end, IPStr ++ ":" ++ integer_to_list(Port) end. -spec format_transport(transport(), opts()) -> string(). format_transport(Transport, Opts) -> case maps:get(tls, Opts, false) of true when Transport == tcp -> "TLS"; true when Transport == udp -> "DTLS"; false when Transport == tcp -> "TCP"; false when Transport == udp -> "UDP" end. -spec apply_rate_limit(non_neg_integer()) -> non_neg_integer(). apply_rate_limit(Interval) -> NewInterval = receive {rate_limit, AcceptInterval} -> AcceptInterval after 0 -> Interval end, case NewInterval of 0 -> ok; Ms when is_integer(Ms) -> timer:sleep(Ms); {linear, I1, T1, T2, I2} -> {MSec, Sec, _USec} = os:timestamp(), TS = MSec * 1000000 + Sec, I = if TS =< T1 -> I1; TS >= T1 + T2 -> I2; true -> round((I2 - I1) * (TS - T1) / T2 + I1) end, timer:sleep(I) end, NewInterval. -spec validator() -> econf:validator(). validator() -> econf:and_then( econf:list( econf:and_then( econf:options( #{module => listen_opt_type(module), transport => listen_opt_type(transport), '_' => econf:any()}, [{required, [module]}]), fun(Opts) -> M = proplists:get_value(module, Opts), T = proplists:get_value(transport, Opts, tcp), (validator(M, T))(Opts) end)), fun prepare_opts/1). -spec validator(module(), transport()) -> econf:validator(). validator(M, T) -> Options = listen_options() ++ M:listen_options(), Required = lists:usort([Opt || Opt <- Options, is_atom(Opt)]), Disallowed = if T == udp -> [backlog, use_proxy_protocol, accept_interval]; true -> [] end, Validator = maps:from_list( lists:map( fun(Opt) -> try {Opt, M:listen_opt_type(Opt)} catch _:_ when M /= ?MODULE -> {Opt, listen_opt_type(Opt)} end end, proplists:get_keys(Options))), econf:options( Validator, [{required, Required}, {disallowed, Disallowed}, {return, map}, unique]). -spec prepare_opts([opts()]) -> [listener()]. prepare_opts(Listeners) -> check_overlapping_listeners( lists:map( fun(Opts1) -> {Opts2, Opts3} = partition( fun({port, _}) -> true; ({transport, _}) -> true; ({module, _}) -> true; (_) -> false end, Opts1), Mod = maps:get(module, Opts2), Port = maps:get(port, Opts2), Transport = maps:get(transport, Opts2, tcp), IP = maps:get(ip, Opts3, {0,0,0,0}), Opts4 = apply_defaults(Mod, Opts3), {{Port, IP, Transport}, Mod, Opts4} end, Listeners)). -spec check_overlapping_listeners([listener()]) -> [listener()]. check_overlapping_listeners(Listeners) -> _ = lists:foldl( fun({{Port, IP, Transport} = Key, _, _}, Acc) -> case lists:member(Key, Acc) of true -> econf:fail({listener_dup, {IP, Port}}); false -> ZeroIP = case size(IP) of 8 -> {0,0,0,0,0,0,0,0}; 4 -> {0,0,0,0} end, Key1 = {Port, ZeroIP, Transport}, case lists:member(Key1, Acc) of true -> econf:fail({listener_conflict, {IP, Port}, {ZeroIP, Port}}); false -> [Key|Acc] end end end, [], Listeners), Listeners. -spec apply_defaults(module(), opts()) -> opts(). apply_defaults(Mod, Opts) -> lists:foldl( fun({Opt, Default}, M) -> case maps:is_key(Opt, M) of true -> M; false -> M#{Opt => Default} end; (_, M) -> M end, Opts, Mod:listen_options() ++ listen_options()). %% Convert options to list with removing defaults -spec opts_to_list(module(), opts()) -> list_opts(). opts_to_list(Mod, Opts) -> Defaults = Mod:listen_options() ++ listen_options(), maps:fold( fun(Opt, Val, Acc) -> case proplists:get_value(Opt, Defaults) of Val -> Acc; _ -> [{Opt, Val}|Acc] end end, [], Opts). -spec partition(fun(({atom(), term()}) -> boolean()), opts()) -> {opts(), opts()}. partition(Fun, Opts) -> maps:fold( fun(Opt, Val, {True, False}) -> case Fun({Opt, Val}) of true -> {True#{Opt => Val}, False}; false -> {True, False#{Opt => Val}} end end, {#{}, #{}}, Opts). -spec listen_opt_type(atom()) -> econf:validator(). listen_opt_type(port) -> econf:either( econf:int(0, 65535), econf:binary("^unix:.*")); listen_opt_type(module) -> econf:beam([[{start, 3}, {start, 2}], [{start_link, 3}, {start_link, 2}], {accept, 1}, {listen_options, 0}]); listen_opt_type(ip) -> econf:ip(); listen_opt_type(transport) -> econf:enum([tcp, udp]); listen_opt_type(accept_interval) -> econf:non_neg_int(); listen_opt_type(backlog) -> econf:non_neg_int(); listen_opt_type(supervisor) -> econf:bool(); listen_opt_type(ciphers) -> econf:binary(); listen_opt_type(dhfile) -> econf:file(); listen_opt_type(cafile) -> econf:pem(); listen_opt_type(certfile) -> econf:pem(); listen_opt_type(protocol_options) -> econf:and_then( econf:list(econf:binary()), fun(Options) -> str:join(Options, <<"|">>) end); listen_opt_type(tls_compression) -> econf:bool(); listen_opt_type(tls) -> econf:bool(); listen_opt_type(max_stanza_size) -> econf:pos_int(infinity); listen_opt_type(max_fsm_queue) -> econf:pos_int(); listen_opt_type(send_timeout) -> econf:timeout(second, infinity); listen_opt_type(shaper) -> econf:shaper(); listen_opt_type(access) -> econf:acl(); listen_opt_type(use_proxy_protocol) -> econf:bool(). listen_options() -> [module, port, {transport, tcp}, {ip, {0,0,0,0}}, {accept_interval, 0}, {send_timeout, 15000}, {backlog, 5}, {use_proxy_protocol, false}, {supervisor, true}]. ejabberd-21.12/src/eldap_filter.erl0000644000232200023220000001612614154362354017565 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File: eldap_filter.erl %%% Purpose: Converts String Representation of %%% LDAP Search Filter (RFC 2254) %%% to eldap's representation of filter %%% Author: Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(eldap_filter). %% TODO: remove this when new regexp module will be used -export([parse/1, parse/2, do_sub/2]). %%==================================================================== %% API %%==================================================================== %%%------------------------------------------------------------------- %%% Arity: parse/1 %%% Function: parse(RFC2254_Filter) -> {ok, EldapFilter} | %%% {error, bad_filter} %%% %%% RFC2254_Filter = string(). %%% %%% Description: Converts String Representation of LDAP Search Filter (RFC 2254) %%% to eldap's representation of filter. %%% %%% Example: %%% > eldap_filter:parse("(&(!(uid<=100))(mail=*))"). %%% %%% {ok,{'and',[{'not',{lessOrEqual,{'AttributeValueAssertion',"uid","100"}}}, %%% {present,"mail"}]}} %%%------------------------------------------------------------------- -spec parse(binary()) -> {error, any()} | {ok, eldap:filter()}. parse(L) -> parse(L, []). %%%------------------------------------------------------------------- %%% Arity: parse/2 %%% Function: parse(RFC2254_Filter, [SubstValue |...]) -> %%% {ok, EldapFilter} | %%% {error, bad_filter} | %%% {error, bad_regexp} | %%% {error, max_substitute_recursion} %%% %%% SubstValue = {RegExp, Value} | {RegExp, Value, N}, %%% RFC2254_Filter = RegExp = Value = string(), %%% N = integer(). %%% %%% Description: The same as parse/1, but substitutes N or all occurrences %%% of RegExp with Value *after* parsing. %%% %%% Example: %%% > eldap_filter:parse( %%% "(|(mail=%u@%d)(jid=%u@%d))", %%% [{"%u", "xramtsov"},{"%d","gmail.com"}]). %%% %%% {ok,{'or',[{equalityMatch,{'AttributeValueAssertion', %%% "mail", %%% "xramtsov@gmail.com"}}, %%% {equalityMatch,{'AttributeValueAssertion', %%% "jid", %%% "xramtsov@gmail.com"}}]}} %%%------------------------------------------------------------------- -spec parse(binary(), [{binary(), binary()} | {binary(), binary(), pos_integer()}]) -> {error, any()} | {ok, eldap:filter()}. parse(L, SList) -> case catch eldap_filter_yecc:parse(scan(binary_to_list(L), SList)) of {'EXIT', _} = Err -> {error, Err}; {error, {_, _, Msg}} -> {error, Msg}; {ok, Result} -> {ok, Result}; {regexp, Err} -> {error, Err} end. %%==================================================================== %% Internal functions %%==================================================================== -define(do_scan(L), scan(Rest, <<>>, [{L, 1} | check(Buf, S) ++ Result], L, S)). scan(L, SList) -> scan(L, <<"">>, [], undefined, SList). scan("=*)" ++ Rest, Buf, Result, '(', S) -> scan(Rest, <<>>, [{')', 1}, {'=*', 1} | check(Buf, S) ++ Result], ')', S); scan(":dn" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':dn'); scan(":=" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':='); scan(":=" ++ Rest, Buf, Result, ':dn', S) -> ?do_scan(':='); scan(":=" ++ Rest, Buf, Result, ':', S) -> ?do_scan(':='); scan("~=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('~='); scan(">=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('>='); scan("<=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('<='); scan("=" ++ Rest, Buf, Result, '(', S) -> ?do_scan('='); scan(":" ++ Rest, Buf, Result, '(', S) -> ?do_scan(':'); scan(":" ++ Rest, Buf, Result, ':dn', S) -> ?do_scan(':'); scan("&" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('&'); scan("|" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('|'); scan("!" ++ Rest, Buf, Result, '(', S) when Buf==<<"">> -> ?do_scan('!'); scan("*" ++ Rest, Buf, Result, '*', S) -> ?do_scan('*'); scan("*" ++ Rest, Buf, Result, '=', S) -> ?do_scan('*'); scan("(" ++ Rest, Buf, Result, _, S) -> ?do_scan('('); scan(")" ++ Rest, Buf, Result, _, S) -> ?do_scan(')'); scan([Letter | Rest], Buf, Result, PreviosAtom, S) -> scan(Rest, <>, Result, PreviosAtom, S); scan([], Buf, Result, _, S) -> lists:reverse(check(Buf, S) ++ Result). check(<<>>, _) -> []; check(Buf, S) -> [{str, 1, binary_to_list(do_sub(Buf, S))}]. -define(MAX_RECURSION, 100). -spec do_sub(binary(), [{binary(), binary()} | {binary(), binary(), pos_integer()}]) -> binary(). do_sub(S, []) -> S; do_sub(<<>>, _) -> <<>>; do_sub(S, [{RegExp, New} | T]) -> Result = do_sub(S, {RegExp, replace_amps(New)}, 1), do_sub(Result, T); do_sub(S, [{RegExp, New, Times} | T]) -> Result = do_sub(S, {RegExp, replace_amps(New), Times}, 1), do_sub(Result, T). do_sub(S, {RegExp, New}, Iter) -> case ejabberd_regexp:run(S, RegExp) of match -> case ejabberd_regexp:replace(S, RegExp, New) of NewS when Iter =< ?MAX_RECURSION -> do_sub(NewS, {RegExp, New}, Iter+1); _NewS when Iter > ?MAX_RECURSION -> erlang:error(max_substitute_recursion) end; nomatch -> S; _ -> erlang:error(bad_regexp) end; do_sub(S, {_, _, N}, _) when N<1 -> S; do_sub(S, {RegExp, New, Times}, Iter) -> case ejabberd_regexp:run(S, RegExp) of match -> case ejabberd_regexp:replace(S, RegExp, New) of NewS when Iter < Times -> do_sub(NewS, {RegExp, New, Times}, Iter+1); NewS -> NewS end; nomatch -> S; _ -> erlang:error(bad_regexp) end. replace_amps(Bin) -> list_to_binary( lists:flatmap( fun($&) -> "\\&"; ($\\) -> "\\\\"; (Chr) -> [Chr] end, binary_to_list(Bin))). ejabberd-21.12/src/mod_announce_mnesia.erl0000644000232200023220000001021214154362354021122 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_announce_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_announce_mnesia). -behaviour(mod_announce). %% API -export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, get_motd/1, is_motd_user/2, set_motd_user/2, import/3]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_announce.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, motd, [{disc_only_copies, [node()]}, {attributes, record_info(fields, motd)}]), ejabberd_mnesia:create(?MODULE, motd_users, [{disc_only_copies, [node()]}, {attributes, record_info(fields, motd_users)}]). set_motd_users(_LServer, USRs) -> F = fun() -> lists:foreach( fun({U, S, _R}) -> mnesia:write(#motd_users{us = {U, S}}) end, USRs) end, transaction(F). set_motd(LServer, Packet) -> F = fun() -> mnesia:write(#motd{server = LServer, packet = Packet}) end, transaction(F). delete_motd(LServer) -> F = fun() -> mnesia:delete({motd, LServer}), mnesia:write_lock_table(motd_users), Users = mnesia:select( motd_users, [{#motd_users{us = '$1', _ = '_'}, [{'==', {element, 2, '$1'}, LServer}], ['$1']}]), lists:foreach(fun(US) -> mnesia:delete({motd_users, US}) end, Users) end, transaction(F). get_motd(LServer) -> case mnesia:dirty_read({motd, LServer}) of [#motd{packet = Packet}] -> {ok, Packet}; [] -> error end. is_motd_user(LUser, LServer) -> case mnesia:dirty_read({motd_users, {LUser, LServer}}) of [#motd_users{}] -> {ok, true}; _ -> {ok, false} end. set_motd_user(LUser, LServer) -> F = fun() -> mnesia:write(#motd_users{us = {LUser, LServer}}) end, transaction(F). need_transform({motd, S, _}) when is_list(S) -> ?INFO_MSG("Mnesia table 'motd' will be converted to binary", []), true; need_transform({motd_users, {U, S}, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'motd_users' will be converted to binary", []), true; need_transform(_) -> false. transform(#motd{server = S, packet = P} = R) -> NewS = iolist_to_binary(S), NewP = fxml:to_xmlel(P), R#motd{server = NewS, packet = NewP}; transform(#motd_users{us = {U, S}} = R) -> NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, R#motd_users{us = NewUS}. import(LServer, <<"motd">>, [<<>>, XML, _TimeStamp]) -> El = fxml_stream:parse_element(XML), mnesia:dirty_write(#motd{server = LServer, packet = El}); import(LServer, <<"motd">>, [LUser, <<>>, _TimeStamp]) -> mnesia:dirty_write(#motd_users{us = {LUser, LServer}}). %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of {atomic, Res} -> Res; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. ejabberd-21.12/src/mod_disco.erl0000644000232200023220000004437314154362354017100 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_disco.erl %%% Author : Alexey Shchepin %%% Purpose : Service Discovery (XEP-0030) support %%% Created : 1 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_disco). -author('alexey@process-one.net'). -protocol({xep, 30, '2.4'}). -protocol({xep, 157, '1.0'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq_items/1, process_local_iq_info/1, get_local_identity/5, get_local_features/5, get_local_services/5, process_sm_iq_items/1, process_sm_iq_info/1, get_sm_identity/5, get_sm_features/5, get_sm_items/5, get_info/5, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include("mod_roster.hrl"). -type features_acc() :: {error, stanza_error()} | {result, [binary()]} | empty. -type items_acc() :: {error, stanza_error()} | {result, [disco_item()]} | empty. -export_type([features_acc/0, items_acc/0]). start(Host, Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_local_iq_items), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_local_iq_info), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, ?MODULE, process_sm_iq_items), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, ?MODULE, process_sm_iq_info), catch ets:new(disco_extra_domains, [named_table, ordered_set, public, {heir, erlang:group_leader(), none}]), ExtraDomains = mod_disco_opt:extra_domains(Opts), lists:foreach(fun (Domain) -> register_extra_domain(Host, Domain) end, ExtraDomains), ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_services, 100), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 100), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, get_local_identity, 100), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_items, 100), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 100), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 100), ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 100), ok. stop(Host) -> ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 100), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 100), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 100), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, get_local_identity, 100), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 100), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, get_local_services, 100), ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 100), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), catch ets:match_delete(disco_extra_domains, {{'_', Host}}), ok. reload(Host, NewOpts, OldOpts) -> NewDomains = mod_disco_opt:extra_domains(NewOpts), OldDomains = mod_disco_opt:extra_domains(OldOpts), lists:foreach( fun(Domain) -> register_extra_domain(Host, Domain) end, NewDomains -- OldDomains), lists:foreach( fun(Domain) -> unregister_extra_domain(Host, Domain) end, OldDomains -- NewDomains). -spec register_extra_domain(binary(), binary()) -> true. register_extra_domain(Host, Domain) -> ets:insert(disco_extra_domains, {{Domain, Host}}). -spec unregister_extra_domain(binary(), binary()) -> true. unregister_extra_domain(Host, Domain) -> ets:delete_object(disco_extra_domains, {{Domain, Host}}). -spec process_local_iq_items(iq()) -> iq(). process_local_iq_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq_items(#iq{type = get, lang = Lang, from = From, to = To, sub_els = [#disco_items{node = Node}]} = IQ) -> Host = To#jid.lserver, case ejabberd_hooks:run_fold(disco_local_items, Host, empty, [From, To, Node, Lang]) of {result, Items} -> xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec process_local_iq_info(iq()) -> iq(). process_local_iq_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq_info(#iq{type = get, lang = Lang, from = From, to = To, sub_els = [#disco_info{node = Node}]} = IQ) -> Host = To#jid.lserver, Identity = ejabberd_hooks:run_fold(disco_local_identity, Host, [], [From, To, Node, Lang]), Info = ejabberd_hooks:run_fold(disco_info, Host, [], [Host, ?MODULE, Node, Lang]), case ejabberd_hooks:run_fold(disco_local_features, Host, empty, [From, To, Node, Lang]) of {result, Features} -> xmpp:make_iq_result(IQ, #disco_info{node = Node, identities = Identity, xdata = Info, features = Features}); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_local_identity(Acc, _From, To, <<"">>, _Lang) -> Host = To#jid.lserver, Name = mod_disco_opt:name(Host), Acc ++ [#identity{category = <<"server">>, type = <<"im">>, name = Name}]; get_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. -spec get_local_features(features_acc(), jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. get_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_features(Acc, _From, To, <<"">>, _Lang) -> Feats = case Acc of {result, Features} -> Features; empty -> [] end, {result, lists:usort( lists:flatten( [?NS_FEATURE_IQ, ?NS_FEATURE_PRESENCE, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, Feats, ejabberd_local:get_features(To#jid.lserver)]))}; get_local_features(Acc, _From, _To, _Node, Lang) -> case Acc of {result, _Features} -> Acc; empty -> Txt = ?T("No features available"), {error, xmpp:err_item_not_found(Txt, Lang)} end. -spec get_local_services(items_acc(), jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [disco_item()]}. get_local_services({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_services(Acc, _From, To, <<"">>, _Lang) -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Host = To#jid.lserver, {result, lists:usort( lists:map( fun(Domain) -> #disco_item{jid = jid:make(Domain)} end, get_vh_services(Host) ++ ets:select(disco_extra_domains, ets:fun2ms( fun({{D, H}}) when H == Host -> D end)))) ++ Items}; get_local_services({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; get_local_services(empty, _From, _To, _Node, Lang) -> {error, xmpp:err_item_not_found(?T("No services available"), Lang)}. -spec get_vh_services(binary()) -> [binary()]. get_vh_services(Host) -> Hosts = lists:sort(fun (H1, H2) -> byte_size(H1) >= byte_size(H2) end, ejabberd_option:hosts()), lists:filter(fun (H) -> case lists:dropwhile(fun (VH) -> not str:suffix( <<".", VH/binary>>, H) end, Hosts) of [] -> false; [VH | _] -> VH == Host end end, ejabberd_router:get_all_routes()). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec process_sm_iq_items(iq()) -> iq(). process_sm_iq_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_sm_iq_items(#iq{type = get, lang = Lang, from = From, to = To, sub_els = [#disco_items{node = Node}]} = IQ) -> case mod_roster:is_subscribed(From, To) of true -> Host = To#jid.lserver, case ejabberd_hooks:run_fold(disco_sm_items, Host, empty, [From, To, Node, Lang]) of {result, Items} -> xmpp:make_iq_result( IQ, #disco_items{node = Node, items = Items}); {error, Error} -> xmpp:make_error(IQ, Error) end; false -> Txt = ?T("Not subscribed"), xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. -spec get_sm_items(items_acc(), jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [disco_item()]}. get_sm_items({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_items(Acc, From, #jid{user = User, server = Server} = To, <<"">>, _Lang) -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Items1 = case mod_roster:is_subscribed(From, To) of true -> get_user_resources(User, Server); _ -> [] end, {result, Items ++ Items1}; get_sm_items({result, _} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_items(empty, From, To, _Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> {error, xmpp:err_item_not_found()}; _ -> Txt = ?T("Query to another users is forbidden"), {error, xmpp:err_not_allowed(Txt, Lang)} end. -spec process_sm_iq_info(iq()) -> iq(). process_sm_iq_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_sm_iq_info(#iq{type = get, lang = Lang, from = From, to = To, sub_els = [#disco_info{node = Node}]} = IQ) -> case mod_roster:is_subscribed(From, To) of true -> Host = To#jid.lserver, Identity = ejabberd_hooks:run_fold(disco_sm_identity, Host, [], [From, To, Node, Lang]), Info = ejabberd_hooks:run_fold(disco_info, Host, [], [From, To, Node, Lang]), case ejabberd_hooks:run_fold(disco_sm_features, Host, empty, [From, To, Node, Lang]) of {result, Features} -> xmpp:make_iq_result(IQ, #disco_info{node = Node, identities = Identity, xdata = Info, features = Features}); {error, Error} -> xmpp:make_error(IQ, Error) end; false -> Txt = ?T("Not subscribed"), xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. -spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, #jid{luser = LUser, lserver = LServer}, _Node, _Lang) -> Acc ++ case ejabberd_auth:user_exists(LUser, LServer) of true -> [#identity{category = <<"account">>, type = <<"registered">>}]; _ -> [] end. -spec get_sm_features(features_acc(), jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. get_sm_features(empty, From, To, Node, Lang) -> #jid{luser = LFrom, lserver = LSFrom} = From, #jid{luser = LTo, lserver = LSTo} = To, case {LFrom, LSFrom} of {LTo, LSTo} -> case Node of <<"">> -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS]}; _ -> {error, xmpp:err_item_not_found()} end; _ -> Txt = ?T("Query to another users is forbidden"), {error, xmpp:err_not_allowed(Txt, Lang)} end; get_sm_features({result, Features}, _From, _To, <<"">>, _Lang) -> {result, [?NS_DISCO_INFO, ?NS_DISCO_ITEMS|Features]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), [#disco_item{jid = jid:make(User, Server, Resource), name = User} || Resource <- lists:sort(Rs)]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Support for: XEP-0157 Contact Addresses for XMPP Services -spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. get_info(_A, Host, Mod, Node, _Lang) when is_atom(Mod), Node == <<"">> -> Module = case Mod of undefined -> ?MODULE; _ -> Mod end, [#xdata{type = result, fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, values = [?NS_SERVERINFO]} | get_fields(Host, Module)]}]; get_info(Acc, _, _, _Node, _) -> Acc. -spec get_fields(binary(), module()) -> [xdata_field()]. get_fields(Host, Module) -> Fields = mod_disco_opt:server_info(Host), Fields1 = lists:filter(fun ({Modules, _, _}) -> case Modules of all -> true; Modules -> lists:member(Module, Modules) end end, Fields), [#xdata_field{var = Var, type = 'list-multi', values = Values} || {_, Var, Values} <- Fields1]. -spec depends(binary(), gen_mod:opts()) -> []. depends(_Host, _Opts) -> []. mod_opt_type(extra_domains) -> econf:list(econf:binary()); mod_opt_type(name) -> econf:binary(); mod_opt_type(server_info) -> econf:list( econf:and_then( econf:options( #{name => econf:binary(), urls => econf:list(econf:binary()), modules => econf:either( all, econf:list(econf:beam()))}), fun(Opts) -> Mods = proplists:get_value(modules, Opts, all), Name = proplists:get_value(name, Opts, <<>>), URLs = proplists:get_value(urls, Opts, []), {Mods, Name, URLs} end)). -spec mod_options(binary()) -> [{server_info, [{all | [module()], binary(), [binary()]}]} | {atom(), any()}]. mod_options(_Host) -> [{extra_domains, []}, {server_info, []}, {name, ?T("ejabberd")}]. mod_doc() -> #{desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. With this module enabled, " "services on your server can be discovered by XMPP clients."), opts => [{extra_domains, #{value => "[Domain, ...]", desc => ?T("With this option, you can specify a list of extra " "domains that are added to the Service Discovery item list. " "The default value is an empty list.")}}, {name, #{value => ?T("Name"), desc => ?T("A name of the server in the Service Discovery. " "This will only be displayed by special XMPP clients. " "The default value is 'ejabberd'.")}}, {server_info, #{value => "[Info, ...]", example => ["server_info:", " -", " modules: all", " name: abuse-addresses", " urls: [\"mailto:abuse@shakespeare.lit\"]", " -", " modules: [mod_muc]", " name: \"Web chatroom logs\"", " urls: [\"http://www.example.org/muc-logs\"]", " -", " modules: [mod_disco]", " name: feedback-addresses", " urls:", " - http://shakespeare.lit/feedback.php", " - mailto:feedback@shakespeare.lit", " - xmpp:feedback@shakespeare.lit", " -", " modules:", " - mod_disco", " - mod_vcard", " name: admin-addresses", " urls:", " - mailto:xmpp@shakespeare.lit", " - xmpp:admins@shakespeare.lit"], desc => ?T("Specify additional information about the server, " "as described in https://xmpp.org/extensions/xep-0157.html" "[XEP-0157: Contact Addresses for XMPP Services]. Every 'Info' " "element in the list is constructed from the following options:")}, [{modules, #{value => "all | [Module, ...]", desc => ?T("The value can be the keyword 'all', in which case the " "information is reported in all the services, " "or a list of ejabberd modules, in which case the " "information is only specified for the services provided " "by those modules.")}}, {name, #{value => ?T("Name"), desc => ?T("The field 'var' name that will be defined. " "See XEP-0157 for some standardized names.")}}, {urls, #{value => "[URI, ...]", desc => ?T("A list of contact URIs, such as " "HTTP URLs, XMPP URIs and so on.")}}]}]}. ejabberd-21.12/src/mod_roster_sql.erl0000644000232200023220000003253514154362354020171 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_roster_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_roster_sql). -behaviour(mod_roster). %% API -export([init/2, read_roster_version/2, write_roster_version/4, get_roster/2, get_roster_item/3, roster_subscribe/4, read_subscription_and_groups/3, remove_user/2, update_roster/4, del_roster/3, transaction/2, process_rosteritems/5, import/3, export/1, raw_to_record/2]). -include("mod_roster.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/jid.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. read_roster_version(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(version)s from roster_version" " where username = %(LUser)s and %(LServer)H")) of {selected, [{Version}]} -> {ok, Version}; {selected, []} -> error; _ -> {error, db_failure} end. write_roster_version(LUser, LServer, InTransaction, Ver) -> if InTransaction -> set_roster_version(LUser, LServer, Ver); true -> transaction( LServer, fun () -> set_roster_version(LUser, LServer, Ver) end) end. get_roster(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s, " "@(ask)s, @(askmessage)s, @(server)s, @(subscribe)s, " "@(type)s from rosterusers " "where username=%(LUser)s and %(LServer)H")) of {selected, Items} when is_list(Items) -> JIDGroups = case get_roster_jid_groups(LServer, LUser) of {selected, JGrps} when is_list(JGrps) -> JGrps; _ -> [] end, GroupsDict = lists:foldl(fun({J, G}, Acc) -> Gs = maps:get(J, Acc, []), maps:put(J, [G | Gs], Acc) end, maps:new(), JIDGroups), {ok, lists:flatmap( fun(I) -> case raw_to_record(LServer, I) of %% Bad JID in database: error -> []; R -> SJID = jid:encode(R#roster.jid), Groups = maps:get(SJID, GroupsDict, []), [R#roster{groups = Groups}] end end, Items)}; _ -> error end. roster_subscribe(_LUser, _LServer, _LJID, Item) -> ItemVals = record_to_row(Item), roster_subscribe(ItemVals). transaction(LServer, F) -> ejabberd_sql:sql_transaction(LServer, F). get_roster_item(LUser, LServer, LJID) -> SJID = jid:encode(LJID), case get_roster_by_jid(LServer, LUser, SJID) of {selected, [I]} -> case raw_to_record(LServer, I) of error -> error; R -> Groups = case get_roster_groups(LServer, LUser, SJID) of {selected, JGrps} when is_list(JGrps) -> [JGrp || {JGrp} <- JGrps]; _ -> [] end, {ok, R#roster{groups = Groups}} end; {selected, []} -> error end. remove_user(LUser, LServer) -> transaction( LServer, fun () -> ejabberd_sql:sql_query_t( ?SQL("delete from rosterusers" " where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query_t( ?SQL("delete from rostergroups" " where username=%(LUser)s and %(LServer)H")) end), ok. update_roster(LUser, LServer, LJID, Item) -> SJID = jid:encode(LJID), ItemVals = record_to_row(Item), ItemGroups = Item#roster.groups, roster_subscribe(ItemVals), ejabberd_sql:sql_query_t( ?SQL("delete from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")), lists:foreach( fun(ItemGroup) -> ejabberd_sql:sql_query_t( ?SQL_INSERT( "rostergroups", ["username=%(LUser)s", "server_host=%(LServer)s", "jid=%(SJID)s", "grp=%(ItemGroup)s"])) end, ItemGroups). del_roster(LUser, LServer, LJID) -> SJID = jid:encode(LJID), ejabberd_sql:sql_query_t( ?SQL("delete from rosterusers" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")), ejabberd_sql:sql_query_t( ?SQL("delete from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). read_subscription_and_groups(LUser, LServer, LJID) -> SJID = jid:encode(LJID), case get_subscription(LServer, LUser, SJID) of {selected, [{SSubscription, SAsk}]} -> Subscription = decode_subscription(LUser, LServer, SSubscription), Ask = decode_ask(LUser, LServer, SAsk), Groups = case get_rostergroup_by_jid(LServer, LUser, SJID) of {selected, JGrps} when is_list(JGrps) -> [JGrp || {JGrp} <- JGrps]; _ -> [] end, {ok, {Subscription, Ask, Groups}}; _ -> error end. export(_Server) -> [{roster, fun(Host, #roster{usj = {_LUser, LServer, _LJID}} = R) when LServer == Host -> ItemVals = record_to_row(R), ItemGroups = R#roster.groups, update_roster_sql(ItemVals, ItemGroups); (_Host, _R) -> [] end}, {roster_version, fun(Host, #roster_version{us = {LUser, LServer}, version = Ver}) when LServer == Host -> [?SQL("delete from roster_version" " where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "roster_version", ["username=%(LUser)s", "server_host=%(LServer)s", "version=%(Ver)s"])]; (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== set_roster_version(LUser, LServer, Version) -> ?SQL_UPSERT_T( "roster_version", ["!username=%(LUser)s", "!server_host=%(LServer)s", "version=%(Version)s"]). get_roster_jid_groups(LServer, LUser) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(jid)s, @(grp)s from rostergroups where " "username=%(LUser)s and %(LServer)H")). get_roster_groups(LServer, LUser, SJID) -> ejabberd_sql:sql_query_t( ?SQL("select @(grp)s from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). roster_subscribe({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}) -> ?SQL_UPSERT_T( "rosterusers", ["!username=%(LUser)s", "!server_host=%(LServer)s", "!jid=%(SJID)s", "nick=%(Name)s", "subscription=%(SSubscription)s", "ask=%(SAsk)s", "askmessage=%(AskMessage)s", "server='N'", "subscribe=''", "type='item'"]). get_roster_by_jid(LServer, LUser, SJID) -> ejabberd_sql:sql_query_t( ?SQL("select @(username)s, @(jid)s, @(nick)s, @(subscription)s," " @(ask)s, @(askmessage)s, @(server)s, @(subscribe)s," " @(type)s from rosterusers" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). get_rostergroup_by_jid(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(grp)s from rostergroups" " where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). get_subscription(LServer, LUser, SJID) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(subscription)s, @(ask)s from rosterusers " "where username=%(LUser)s and %(LServer)H and jid=%(SJID)s")). update_roster_sql({LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}, ItemGroups) -> [?SQL("delete from rosterusers where" " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;"), ?SQL_INSERT( "rosterusers", ["username=%(LUser)s", "server_host=%(LServer)s", "jid=%(SJID)s", "nick=%(Name)s", "subscription=%(SSubscription)s", "ask=%(SAsk)s", "askmessage=%(AskMessage)s", "server='N'", "subscribe=''", "type='item'"]), ?SQL("delete from rostergroups where" " username=%(LUser)s and %(LServer)H and jid=%(SJID)s;")] ++ [?SQL_INSERT( "rostergroups", ["username=%(LUser)s", "server_host=%(LServer)s", "jid=%(SJID)s", "grp=%(ItemGroup)s"]) || ItemGroup <- ItemGroups]. raw_to_record(LServer, [User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType]) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType}); raw_to_record(LServer, {User, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType}) -> raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType}); raw_to_record(LServer, {User, LServer, SJID, Nick, SSubscription, SAsk, SAskMessage, _SServer, _SSubscribe, _SType}) -> try jid:decode(SJID) of JID -> LJID = jid:tolower(JID), Subscription = decode_subscription(User, LServer, SSubscription), Ask = decode_ask(User, LServer, SAsk), #roster{usj = {User, LServer, LJID}, us = {User, LServer}, jid = LJID, name = Nick, subscription = Subscription, ask = Ask, askmessage = SAskMessage} catch _:{bad_jid, _} -> ?ERROR_MSG("~ts", [format_row_error(User, LServer, {jid, SJID})]), error end. record_to_row( #roster{us = {LUser, LServer}, jid = JID, name = Name, subscription = Subscription, ask = Ask, askmessage = AskMessage}) -> SJID = jid:encode(jid:tolower(JID)), SSubscription = case Subscription of both -> <<"B">>; to -> <<"T">>; from -> <<"F">>; none -> <<"N">> end, SAsk = case Ask of subscribe -> <<"S">>; unsubscribe -> <<"U">>; both -> <<"B">>; out -> <<"O">>; in -> <<"I">>; none -> <<"N">> end, {LUser, LServer, SJID, Name, SSubscription, SAsk, AskMessage}. decode_subscription(User, Server, S) -> case S of <<"B">> -> both; <<"T">> -> to; <<"F">> -> from; <<"N">> -> none; <<"">> -> none; _ -> ?ERROR_MSG("~ts", [format_row_error(User, Server, {subscription, S})]), none end. decode_ask(User, Server, A) -> case A of <<"S">> -> subscribe; <<"U">> -> unsubscribe; <<"B">> -> both; <<"O">> -> out; <<"I">> -> in; <<"N">> -> none; <<"">> -> none; _ -> ?ERROR_MSG("~ts", [format_row_error(User, Server, {ask, A})]), none end. format_row_error(User, Server, Why) -> [case Why of {jid, JID} -> ["Malformed 'jid' field with value '", JID, "'"]; {subscription, Sub} -> ["Malformed 'subscription' field with value '", Sub, "'"]; {ask, Ask} -> ["Malformed 'ask' field with value '", Ask, "'"] end, " detected for ", User, "@", Server, " in table 'rosterusers'"]. process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> process_rosteritems_sql(ActionS, list_to_atom(SubsS), list_to_atom(AsksS), list_to_binary(UsersS), list_to_binary(ContactsS)). process_rosteritems_sql(ActionS, Subscription, Ask, SLocalJID, SJID) -> [LUser, LServer] = binary:split(SLocalJID, <<"@">>), SSubscription = case Subscription of any -> <<"_">>; both -> <<"B">>; to -> <<"T">>; from -> <<"F">>; none -> <<"N">> end, SAsk = case Ask of any -> <<"_">>; subscribe -> <<"S">>; unsubscribe -> <<"U">>; both -> <<"B">>; out -> <<"O">>; in -> <<"I">>; none -> <<"N">> end, {selected, List} = ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s, @(jid)s from rosterusers " "where username LIKE %(LUser)s" " and %(LServer)H" " and jid LIKE %(SJID)s" " and subscription LIKE %(SSubscription)s" " and ask LIKE %(SAsk)s")), case ActionS of "delete" -> [mod_roster:del_roster(User, LServer, jid:tolower(jid:decode(Contact))) || {User, Contact} <- List]; "list" -> ok end, List. ejabberd-21.12/src/mod_muc_mnesia.erl0000644000232200023220000003162414154362354020112 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_muc_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_mnesia). -behaviour(mod_muc). -behaviour(mod_muc_room). %% API -export([init/2, import/3, store_room/5, restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, get_online_rooms/3, count_online_rooms/2, rsm_supported/0, register_online_user/4, unregister_online_user/4, count_online_rooms_by_user/3, get_online_rooms_by_user/3, find_online_room_by_pid/2]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). %% gen_server callbacks -export([start_link/2, init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2, code_change/3]). -export([need_transform/1, transform/1]). -include("mod_muc.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== init(Host, Opts) -> Spec = {?MODULE, {?MODULE, start_link, [Host, Opts]}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. start_link(Host, Opts) -> Name = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Name}, ?MODULE, [Host, Opts], []). store_room(_LServer, Host, Name, Opts, _) -> F = fun () -> mnesia:write(#muc_room{name_host = {Name, Host}, opts = Opts}) end, mnesia:transaction(F). restore_room(_LServer, Host, Name) -> case catch mnesia:dirty_read(muc_room, {Name, Host}) of [#muc_room{opts = Opts}] -> Opts; _ -> error end. forget_room(_LServer, Host, Name) -> F = fun () -> mnesia:delete({muc_room, {Name, Host}}) end, mnesia:transaction(F). can_use_nick(_LServer, Host, JID, Nick) -> {LUser, LServer, _} = jid:tolower(JID), LUS = {LUser, LServer}, case catch mnesia:dirty_select(muc_registered, [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, [{'==', {element, 2, '$1'}, Host}], ['$_']}]) of {'EXIT', _Reason} -> true; [] -> true; [#muc_registered{us_host = {U, _Host}}] -> U == LUS end. get_rooms(_LServer, Host) -> mnesia:dirty_select(muc_room, [{#muc_room{name_host = {'_', Host}, _ = '_'}, [], ['$_']}]). get_nick(_LServer, Host, From) -> {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, case mnesia:dirty_read(muc_registered, {LUS, Host}) of [] -> error; [#muc_registered{nick = Nick}] -> Nick end. set_nick(_LServer, Host, From, Nick) -> {LUser, LServer, _} = jid:tolower(From), LUS = {LUser, LServer}, F = fun () -> case Nick of <<"">> -> mnesia:delete({muc_registered, {LUS, Host}}), ok; _ -> Allow = case mnesia:select( muc_registered, [{#muc_registered{us_host = '$1', nick = Nick, _ = '_'}, [{'==', {element, 2, '$1'}, Host}], ['$_']}]) of [] -> true; [#muc_registered{us_host = {U, _Host}}] -> U == LUS end, if Allow -> mnesia:write(#muc_registered{ us_host = {LUS, Host}, nick = Nick}), ok; true -> false end end end, mnesia:transaction(F). set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> {error, not_implemented}. set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> {error, not_implemented}. get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> {error, not_implemented}. get_affiliations(_ServerHost, _Room, _Host) -> {error, not_implemented}. search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> {error, not_implemented}. register_online_room(_ServerHost, Room, Host, Pid) -> F = fun() -> mnesia:write( #muc_online_room{name_host = {Room, Host}, pid = Pid}) end, mnesia:transaction(F). unregister_online_room(_ServerHost, Room, Host, Pid) -> F = fun () -> mnesia:delete_object( #muc_online_room{name_host = {Room, Host}, pid = Pid}) end, mnesia:transaction(F). find_online_room(_ServerHost, Room, Host) -> find_online_room(Room, Host). find_online_room(Room, Host) -> case mnesia:dirty_read(muc_online_room, {Room, Host}) of [] -> error; [#muc_online_room{pid = Pid}] -> {ok, Pid} end. find_online_room_by_pid(_ServerHost, Pid) -> Res = mnesia:dirty_select( muc_online_room, ets:fun2ms( fun(#muc_online_room{name_host = {Name, Host}, pid = PidS}) when PidS == Pid -> {Name, Host} end)), case Res of [{Name, Host}] -> {ok, Name, Host}; _ -> error end. count_online_rooms(_ServerHost, Host) -> ets:select_count( muc_online_room, ets:fun2ms( fun(#muc_online_room{name_host = {_, H}}) -> H == Host end)). get_online_rooms(_ServerHost, Host, #rsm_set{max = Max, 'after' = After, before = undefined}) when is_binary(After), After /= <<"">> -> lists:reverse(get_online_rooms(next, {After, Host}, Host, 0, Max, [])); get_online_rooms(_ServerHost, Host, #rsm_set{max = Max, 'after' = undefined, before = Before}) when is_binary(Before), Before /= <<"">> -> get_online_rooms(prev, {Before, Host}, Host, 0, Max, []); get_online_rooms(_ServerHost, Host, #rsm_set{max = Max, 'after' = undefined, before = <<"">>}) -> get_online_rooms(last, {<<"">>, Host}, Host, 0, Max, []); get_online_rooms(_ServerHost, Host, #rsm_set{max = Max}) -> lists:reverse(get_online_rooms(first, {<<"">>, Host}, Host, 0, Max, [])); get_online_rooms(_ServerHost, Host, undefined) -> mnesia:dirty_select( muc_online_room, ets:fun2ms( fun(#muc_online_room{name_host = {Name, H}, pid = Pid}) when H == Host -> {Name, Host, Pid} end)). -spec get_online_rooms(prev | next | last | first, {binary(), binary()}, binary(), non_neg_integer(), non_neg_integer() | undefined, [{binary(), binary(), pid()}]) -> [{binary(), binary(), pid()}]. get_online_rooms(_Action, _Key, _Host, Count, Max, Items) when Count >= Max -> Items; get_online_rooms(Action, Key, Host, Count, Max, Items) -> Call = fun() -> case Action of prev -> mnesia:dirty_prev(muc_online_room, Key); next -> mnesia:dirty_next(muc_online_room, Key); last -> mnesia:dirty_last(muc_online_room); first -> mnesia:dirty_first(muc_online_room) end end, NewAction = case Action of last -> prev; first -> next; _ -> Action end, try Call() of '$end_of_table' -> Items; {Room, Host} = NewKey -> case find_online_room(Room, Host) of {ok, Pid} -> get_online_rooms(NewAction, NewKey, Host, Count + 1, Max, [{Room, Host, Pid}|Items]); error -> get_online_rooms(NewAction, NewKey, Host, Count, Max, Items) end; NewKey -> get_online_rooms(NewAction, NewKey, Host, Count, Max, Items) catch _:{aborted, {badarg, _}} -> Items end. rsm_supported() -> true. register_online_user(_ServerHost, {U, S, R}, Room, Host) -> ets:insert(muc_online_users, #muc_online_users{us = {U, S}, resource = R, room = Room, host = Host}). unregister_online_user(_ServerHost, {U, S, R}, Room, Host) -> ets:delete_object(muc_online_users, #muc_online_users{us = {U, S}, resource = R, room = Room, host = Host}). count_online_rooms_by_user(ServerHost, U, S) -> MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), ets:select_count( muc_online_users, ets:fun2ms( fun(#muc_online_users{us = {U1, S1}, host = Host}) -> U == U1 andalso S == S1 andalso MucHost == Host end)). get_online_rooms_by_user(ServerHost, U, S) -> MucHost = hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)), ets:select( muc_online_users, ets:fun2ms( fun(#muc_online_users{us = {U1, S1}, room = Room, host = Host}) when U == U1 andalso S == S1 andalso MucHost == Host -> {Room, Host} end)). import(_LServer, <<"muc_room">>, [Name, RoomHost, SOpts, _TimeStamp]) -> Opts = mod_muc:opts_to_binary(ejabberd_sql:decode_term(SOpts)), mnesia:dirty_write( #muc_room{name_host = {Name, RoomHost}, opts = Opts}); import(_LServer, <<"muc_registered">>, [J, RoomHost, Nick, _TimeStamp]) -> #jid{user = U, server = S} = jid:decode(J), mnesia:dirty_write( #muc_registered{us_host = {{U, S}, RoomHost}, nick = Nick}). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([_Host, Opts]) -> MyHosts = mod_muc_opt:hosts(Opts), case gen_mod:db_mod(Opts, mod_muc) of ?MODULE -> ejabberd_mnesia:create(?MODULE, muc_room, [{disc_copies, [node()]}, {attributes, record_info(fields, muc_room)}]), ejabberd_mnesia:create(?MODULE, muc_registered, [{disc_copies, [node()]}, {attributes, record_info(fields, muc_registered)}, {index, [nick]}]); _ -> ok end, case gen_mod:ram_db_mod(Opts, mod_muc) of ?MODULE -> ejabberd_mnesia:create(?MODULE, muc_online_room, [{ram_copies, [node()]}, {type, ordered_set}, {attributes, record_info(fields, muc_online_room)}]), catch ets:new(muc_online_users, [bag, named_table, public, {keypos, 2}]), lists:foreach( fun(MyHost) -> clean_table_from_bad_node(node(), MyHost) end, MyHosts), mnesia:subscribe(system); _ -> ok end, {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> clean_table_from_bad_node(Node), {noreply, State}; handle_info({mnesia_system_event, {mnesia_up, _Node}}, State) -> {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== clean_table_from_bad_node(Node) -> F = fun() -> Es = mnesia:select( muc_online_room, [{#muc_online_room{pid = '$1', _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}]), lists:foreach(fun(E) -> mnesia:delete_object(E) end, Es) end, mnesia:async_dirty(F). clean_table_from_bad_node(Node, Host) -> F = fun() -> Es = mnesia:select( muc_online_room, [{#muc_online_room{pid = '$1', name_host = {'_', Host}, _ = '_'}, [{'==', {node, '$1'}, Node}], ['$_']}]), lists:foreach(fun(E) -> mnesia:delete_object(E) end, Es) end, mnesia:async_dirty(F). need_transform({muc_room, {N, H}, _}) when is_list(N) orelse is_list(H) -> ?INFO_MSG("Mnesia table 'muc_room' will be converted to binary", []), true; need_transform({muc_registered, {{U, S}, H}, Nick}) when is_list(U) orelse is_list(S) orelse is_list(H) orelse is_list(Nick) -> ?INFO_MSG("Mnesia table 'muc_registered' will be converted to binary", []), true; need_transform(_) -> false. transform(#muc_room{name_host = {N, H}, opts = Opts} = R) -> R#muc_room{name_host = {iolist_to_binary(N), iolist_to_binary(H)}, opts = mod_muc:opts_to_binary(Opts)}; transform(#muc_registered{us_host = {{U, S}, H}, nick = Nick} = R) -> R#muc_registered{us_host = {{iolist_to_binary(U), iolist_to_binary(S)}, iolist_to_binary(H)}, nick = iolist_to_binary(Nick)}. ejabberd-21.12/src/mod_pubsub_sql.erl0000644000232200023220000000250514154362354020145 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 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(mod_pubsub_sql). %% API -export([init/3]). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _ServerHost, _Opts) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_last_sql.erl0000644000232200023220000000570114154362354017611 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_last_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_last_sql). -behaviour(mod_last). %% API -export([init/2, get_last/2, store_last_info/4, remove_user/2, import/2, export/1]). -include("mod_last.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. get_last(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(seconds)d, @(state)s from last" " where username=%(LUser)s and %(LServer)H")) of {selected, []} -> error; {selected, [{TimeStamp, Status}]} -> {ok, {TimeStamp, Status}}; _Reason -> {error, db_failure} end. store_last_info(LUser, LServer, TimeStamp, Status) -> TS = integer_to_binary(TimeStamp), case ?SQL_UPSERT(LServer, "last", ["!username=%(LUser)s", "!server_host=%(LServer)s", "seconds=%(TS)s", "state=%(Status)s"]) of ok -> ok; _Err -> {error, db_failure} end. remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from last where username=%(LUser)s and %(LServer)H")). export(_Server) -> [{last_activity, fun(Host, #last_activity{us = {LUser, LServer}, timestamp = TimeStamp, status = Status}) when LServer == Host -> TS = integer_to_binary(TimeStamp), [?SQL("delete from last where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT("last", ["username=%(LUser)s", "server_host=%(LServer)s", "seconds=%(TS)s", "state=%(Status)s"])]; (_Host, _R) -> [] end}]. import(_LServer, _LA) -> pass. ejabberd-21.12/src/ejabberd_sm_redis.erl0000644000232200023220000001745014154362354020557 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_sm_redis.erl %%% Author : Evgeny Khramtsov %%% Created : 11 Mar 2015 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sm_redis). -ifndef(GEN_SERVER). -define(GEN_SERVER, p1_server). -endif. -behaviour(?GEN_SERVER). -behaviour(ejabberd_sm). -export([init/0, set_session/1, delete_session/1, get_sessions/0, get_sessions/1, get_sessions/2, cache_nodes/1, clean_table/1, clean_table/0]). %% gen_server callbacks -export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("ejabberd_sm.hrl"). -include("logger.hrl"). -define(SM_KEY, <<"ejabberd:sm">>). -define(MIN_REDIS_VERSION, <<"3.2.0">>). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). -spec cache_nodes(binary()) -> [node()]. cache_nodes(_LServer) -> [node()]. -spec set_session(#session{}) -> ok | {error, ejabberd_redis:error_reason()}. set_session(Session) -> T = term_to_binary(Session), USKey = us_to_key(Session#session.us), SIDKey = sid_to_key(Session#session.sid), ServKey = server_to_key(element(2, Session#session.us)), USSIDKey = us_sid_to_key(Session#session.us, Session#session.sid), NodeHostKey = node_host_to_key(node(), element(2, Session#session.us)), case ejabberd_redis:multi( fun() -> ejabberd_redis:hset(USKey, SIDKey, T), ejabberd_redis:hset(ServKey, USSIDKey, T), ejabberd_redis:hset(NodeHostKey, <>, USSIDKey), ejabberd_redis:publish( ?SM_KEY, term_to_binary({delete, Session#session.us})) end) of {ok, _} -> ok; Err -> Err end. -spec delete_session(#session{}) -> ok | {error, ejabberd_redis:error_reason()}. delete_session(#session{sid = SID} = Session) -> USKey = us_to_key(Session#session.us), SIDKey = sid_to_key(SID), ServKey = server_to_key(element(2, Session#session.us)), USSIDKey = us_sid_to_key(Session#session.us, SID), NodeHostKey = node_host_to_key(node(), element(2, Session#session.us)), case ejabberd_redis:multi( fun() -> ejabberd_redis:hdel(USKey, [SIDKey]), ejabberd_redis:hdel(ServKey, [USSIDKey]), ejabberd_redis:hdel(NodeHostKey, [<>]), ejabberd_redis:publish( ?SM_KEY, term_to_binary({delete, Session#session.us})) end) of {ok, _} -> ok; Err -> Err end. -spec get_sessions() -> [#session{}]. get_sessions() -> lists:flatmap( fun(LServer) -> get_sessions(LServer) end, ejabberd_sm:get_vh_by_backend(?MODULE)). -spec get_sessions(binary()) -> [#session{}]. get_sessions(LServer) -> ServKey = server_to_key(LServer), case ejabberd_redis:hgetall(ServKey) of {ok, Vals} -> decode_session_list(Vals); {error, _} -> [] end. -spec get_sessions(binary(), binary()) -> {ok, [#session{}]} | {error, ejabberd_redis:error_reason()}. get_sessions(LUser, LServer) -> USKey = us_to_key({LUser, LServer}), case ejabberd_redis:hgetall(USKey) of {ok, Vals} -> {ok, decode_session_list(Vals)}; Err -> Err end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> ejabberd_redis:subscribe([?SM_KEY]), case clean_table() of ok -> {ok, #state{}}; {error, Why} -> {stop, Why} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({redis_message, ?SM_KEY, Data}, State) -> case binary_to_term(Data) of {delete, Key} -> ets_cache:delete(?SM_CACHE, Key); Msg -> ?WARNING_MSG("Unexpected redis message: ~p", [Msg]) end, {noreply, State}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== us_to_key({LUser, LServer}) -> <<(?SM_KEY)/binary, ":", LUser/binary, "@", LServer/binary>>. server_to_key(LServer) -> <<(?SM_KEY)/binary, ":", LServer/binary>>. us_sid_to_key(US, SID) -> term_to_binary({US, SID}). sid_to_key(SID) -> term_to_binary(SID). node_session_deletion_cursor(Node, Host) -> NodeName = node_host_to_key(Node, Host), <>. node_host_to_key(Node, Host) when is_atom(Node) -> NodeBin = atom_to_binary(node(), utf8), node_host_to_key(NodeBin, Host); node_host_to_key(NodeBin, Host) -> HostKey = server_to_key(Host), <>. decode_session_list(Vals) -> [binary_to_term(Val) || {_, Val} <- Vals]. clean_table() -> clean_table(node()). clean_table(Node) when is_atom(Node) -> clean_table(atom_to_binary(Node, utf8)); clean_table(Node) -> ?DEBUG("Cleaning Redis SM table... ", []), try lists:foreach( fun(Host) -> ok = clean_node_sessions(Node, Host) end, ejabberd_sm:get_vh_by_backend(?MODULE)) catch _:{badmatch, {error, _} = Err} -> ?ERROR_MSG("Failed to clean Redis SM table", []), Err end. clean_node_sessions(Node, Host) -> case load_script() of {ok, SHA} -> clean_node_sessions(Node, Host, SHA); Err -> Err end. clean_node_sessions(Node, Host, SHA) -> Keys = [node_host_to_key(Node, Host), server_to_key(Host), node_session_deletion_cursor(Node, Host)], case ejabberd_redis:evalsha(SHA, Keys, [1000]) of {ok, <<"0">>} -> ok; {ok, _Cursor} -> clean_node_sessions(Node, Host, SHA); {error, _} = Err -> Err end. load_script() -> case misc:read_lua("redis_sm.lua") of {ok, Data} -> case ejabberd_redis:info(server) of {ok, Info} -> case proplists:get_value(redis_version, Info) of V when V >= ?MIN_REDIS_VERSION -> ejabberd_redis:script_load(Data); V -> ?CRITICAL_MSG("Unsupported Redis version: ~ts. " "The version must be ~ts or above", [V, ?MIN_REDIS_VERSION]), {error, unsupported_redis_version} end; {error, _} = Err -> Err end; {error, _} = Err -> Err end. ejabberd-21.12/src/mod_register_web.erl0000644000232200023220000005233014154362354020450 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_register_web.erl %%% Author : Badlop %%% Purpose : Web page to register account and related tasks %%% Created : 4 May 2008 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_register_web). -author('badlop@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, mod_options/1, depends/2]). -export([mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("translate.hrl"). %%%---------------------------------------------------------------------- %%% gen_mod callbacks %%%---------------------------------------------------------------------- start(_Host, _Opts) -> %% case mod_register_web_opt:docroot(Opts, fun(A) -> A end, undefined) of ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_register, hard}]. %%%---------------------------------------------------------------------- %%% HTTP handlers %%%---------------------------------------------------------------------- process([], #request{method = 'GET', lang = Lang}) -> index_page(Lang); process([<<"register.css">>], #request{method = 'GET'}) -> serve_css(); process([Section], #request{method = 'GET', lang = Lang, host = Host, ip = {Addr, _Port}}) -> Host2 = case ejabberd_router:is_my_host(Host) of true -> Host; false -> <<"">> end, case Section of <<"new">> -> form_new_get(Host2, Lang, Addr); <<"delete">> -> form_del_get(Host2, Lang); <<"change_password">> -> form_changepass_get(Host2, Lang); _ -> {404, [], "Not Found"} end; process([<<"new">>], #request{method = 'POST', q = Q, ip = {Ip, _Port}, lang = Lang, host = _HTTPHost}) -> case form_new_post(Q, Ip) of {success, ok, {Username, Host, _Password}} -> Jid = jid:make(Username, Host), mod_register:send_registration_notifications(?MODULE, Jid, Ip), Text = translate:translate(Lang, ?T("Your XMPP account was successfully registered.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error creating the account: ")), translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; process([<<"delete">>], #request{method = 'POST', q = Q, lang = Lang, host = _HTTPHost}) -> case form_del_post(Q) of {atomic, ok} -> Text = translate:translate(Lang, ?T("Your XMPP account was successfully unregistered.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error deleting the account: ")), translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; %% TODO: Currently only the first vhost is usable. The web request record %% should include the host where the POST was sent. process([<<"change_password">>], #request{method = 'POST', q = Q, lang = Lang, host = _HTTPHost}) -> case form_changepass_post(Q) of {atomic, ok} -> Text = translate:translate(Lang, ?T("The password of your XMPP account was successfully changed.")), {200, [], Text}; Error -> ErrorText = list_to_binary([translate:translate(Lang, ?T("There was an error changing the password: ")), translate:translate(Lang, get_error_text(Error))]), {404, [], ErrorText} end; process(_Path, _Request) -> {404, [], "Not Found"}. %%%---------------------------------------------------------------------- %%% CSS %%%---------------------------------------------------------------------- serve_css() -> case css() of {ok, CSS} -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], CSS}; error -> {404, [], "CSS not found"} end. last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. -spec css() -> {ok, binary()} | error. css() -> Dir = misc:css_dir(), File = filename:join(Dir, "register.css"), case file:read_file(File) of {ok, Data} -> {ok, Data}; {error, Why} -> ?ERROR_MSG("Failed to read ~ts: ~ts", [File, file:format_error(Why)]), error end. meta() -> ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, {<<"content">>, <<"width=device-width, initial-scale=1">>}]). %%%---------------------------------------------------------------------- %%% Index page %%%---------------------------------------------------------------------- index_page(Lang) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("XMPP Account Registration")), ?XA(<<"link">>, [{<<"href">>, <<"register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("XMPP Account Registration")), ?XE(<<"ul">>, [?XE(<<"li">>, [?ACT(<<"new/">>, ?T("Register an XMPP account"))]), ?XE(<<"li">>, [?ACT(<<"change_password/">>, ?T("Change Password"))]), ?XE(<<"li">>, [?ACT(<<"delete/">>, ?T("Unregister an XMPP account"))])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %%%---------------------------------------------------------------------- %%% Formulary new account GET %%%---------------------------------------------------------------------- form_new_get(Host, Lang, IP) -> try build_captcha_li_list(Lang, IP) of CaptchaEls -> form_new_get2(Host, Lang, CaptchaEls) catch throw:Result -> ?DEBUG("Unexpected result when creating a captcha: ~p", [Result]), ejabberd_web:error(not_allowed) end. form_new_get2(Host, Lang, CaptchaEls) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Register an XMPP account")), ?XA(<<"link">>, [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Register an XMPP account")), ?XCT(<<"p">>, ?T("This page allows to register an XMPP " "account in this XMPP server. Your " "JID (Jabber ID) will be of the " "form: username@server. Please read carefully " "the instructions to fill correctly the " "fields.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, ([?XE(<<"li">>, [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, ?T("This is case insensitive: macbeth is " "the same that MacBeth and Macbeth.")), ?XC(<<"li">>, <<(translate:translate(Lang, ?T("Characters not allowed:")))/binary, " \" & ' / : < > @ ">>)])]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>), ?BR, ?XE(<<"ul">>, [?XCT(<<"li">>, ?T("Don't tell your password to anybody, " "not even the administrators of the XMPP " "server.")), ?XCT(<<"li">>, ?T("You can later change your password using " "an XMPP client.")), ?XCT(<<"li">>, ?T("Some XMPP clients can store your password " "in the computer, but you should do this only " "in your personal computer for safety reasons.")), ?XCT(<<"li">>, ?T("Memorize your password, or write it " "in a paper placed in a safe place. In " "XMPP there isn't an automated way " "to recover your password if you forget " "it."))])]), ?XE(<<"li">>, [?CT(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)])] ++ CaptchaEls ++ [?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"register">>, ?T("Register"))])]))])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %% Copied from mod_register.erl %% Function copied from ejabberd_logger_h.erl and customized %%%---------------------------------------------------------------------- %%% Formulary new POST %%%---------------------------------------------------------------------- form_new_post(Q, Ip) -> case catch get_register_parameters(Q) of [Username, Host, Password, Password, Id, Key] -> form_new_post(Username, Host, Password, {Id, Key}, Ip); [_Username, _Host, _Password, _Password2, false, false] -> {error, passwords_not_identical}; [_Username, _Host, _Password, _Password2, Id, Key] -> ejabberd_captcha:check_captcha(Id, Key), {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. get_register_parameters(Q) -> lists:map(fun (Key) -> case lists:keysearch(Key, 1, Q) of {value, {_Key, Value}} -> Value; false -> false end end, [<<"username">>, <<"host">>, <<"password">>, <<"password2">>, <<"id">>, <<"key">>]). form_new_post(Username, Host, Password, {false, false}, Ip) -> register_account(Username, Host, Password, Ip); form_new_post(Username, Host, Password, {Id, Key}, Ip) -> case ejabberd_captcha:check_captcha(Id, Key) of captcha_valid -> register_account(Username, Host, Password, Ip); captcha_non_valid -> {error, captcha_non_valid}; captcha_not_found -> {error, captcha_non_valid} end. %%%---------------------------------------------------------------------- %%% Formulary Captcha support for new GET/POST %%%---------------------------------------------------------------------- build_captcha_li_list(Lang, IP) -> case ejabberd_captcha:is_feature_available() of true -> build_captcha_li_list2(Lang, IP); false -> [] end. build_captcha_li_list2(Lang, IP) -> SID = <<"">>, From = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, To = #jid{user = <<"">>, server = <<"test">>, resource = <<"">>}, Args = [], case ejabberd_captcha:create_captcha( SID, From, To, Lang, IP, Args) of {ok, Id, _, _} -> case ejabberd_captcha:build_captcha_html(Id, Lang) of {_, {CImg, CText, CId, CKey}} -> [?XE(<<"li">>, [CText, ?C(<<" ">>), CId, CKey, ?BR, CImg])]; Error -> throw(Error) end; Error -> throw(Error) end. %%%---------------------------------------------------------------------- %%% Formulary change password GET %%%---------------------------------------------------------------------- form_changepass_get(Host, Lang) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Change Password")), ?XA(<<"link">>, [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Change Password")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Old Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"passwordold">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("New Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password Verification:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password2">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"changepass">>, ?T("Change Password"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %%%---------------------------------------------------------------------- %%% Formulary change password POST %%%---------------------------------------------------------------------- form_changepass_post(Q) -> case catch get_changepass_parameters(Q) of [Username, Host, PasswordOld, Password, Password] -> try_change_password(Username, Host, PasswordOld, Password); [_Username, _Host, _PasswordOld, _Password, _Password2] -> {error, passwords_not_identical}; _ -> {error, wrong_parameters} end. get_changepass_parameters(Q) -> %% @spec(Username,Host,PasswordOld,Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, password_not_changed} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"host">>, <<"passwordold">>, <<"password">>, <<"password2">>]). try_change_password(Username, Host, PasswordOld, Password) -> try change_password(Username, Host, PasswordOld, Password) of {atomic, ok} -> {atomic, ok} catch error:{badmatch, Error} -> {error, Error} end. change_password(Username, Host, PasswordOld, Password) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, PasswordOld), ok = ejabberd_auth:set_password(Username, Host, Password), case check_password(Username, Host, Password) of password_correct -> {atomic, ok}; password_incorrect -> {error, password_not_changed} end. check_account_exists(Username, Host) -> case ejabberd_auth:user_exists(Username, Host) of true -> account_exists; false -> account_doesnt_exist end. check_password(Username, Host, Password) -> case ejabberd_auth:check_password(Username, <<"">>, Host, Password) of true -> password_correct; false -> password_incorrect end. %%%---------------------------------------------------------------------- %%% Formulary delete account GET %%%---------------------------------------------------------------------- form_del_get(Host, Lang) -> HeadEls = [meta(), ?XCT(<<"title">>, ?T("Unregister an XMPP account")), ?XA(<<"link">>, [{<<"href">>, <<"../register.css">>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}])], Els = [?XACT(<<"h1">>, [{<<"class">>, <<"title">>}, {<<"style">>, <<"text-align:center;">>}], ?T("Unregister an XMPP account")), ?XCT(<<"p">>, ?T("This page allows to unregister an XMPP " "account in this XMPP server.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"ol">>, [?XE(<<"li">>, [?CT(?T("Username:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"username">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Server:")), ?C(<<" ">>), ?INPUTS(<<"text">>, <<"host">>, Host, <<"20">>)]), ?XE(<<"li">>, [?CT(?T("Password:")), ?C(<<" ">>), ?INPUTS(<<"password">>, <<"password">>, <<"">>, <<"20">>)]), ?XE(<<"li">>, [?INPUTT(<<"submit">>, <<"unregister">>, ?T("Unregister"))])])])], {200, [{<<"Server">>, <<"ejabberd">>}, {<<"Content-Type">>, <<"text/html">>}], ejabberd_web:make_xhtml(HeadEls, Els)}. %% @spec(Username, Host, Password, Ip) -> {success, ok, {Username, Host, Password} | %% {success, exists, {Username, Host, Password}} | %% {error, not_allowed} | %% {error, invalid_jid} register_account(Username, Host, Password, Ip) -> try mod_register_opt:access(Host) of Access -> case jid:make(Username, Host) of error -> {error, invalid_jid}; JID -> case acl:match_rule(Host, Access, JID) of deny -> {error, not_allowed}; allow -> register_account2(Username, Host, Password, Ip) end end catch _:{module_not_loaded, mod_register, _Host} -> {error, host_unknown} end. register_account2(Username, Host, Password, Ip) -> case mod_register:try_register(Username, Host, Password, Ip, ?MODULE) of ok -> {success, ok, {Username, Host, Password}}; Other -> Other end. %%%---------------------------------------------------------------------- %%% Formulary delete POST %%%---------------------------------------------------------------------- form_del_post(Q) -> case catch get_unregister_parameters(Q) of [Username, Host, Password] -> try_unregister_account(Username, Host, Password); _ -> {error, wrong_parameters} end. get_unregister_parameters(Q) -> %% @spec(Username, Host, Password) -> {atomic, ok} | %% {error, account_doesnt_exist} | %% {error, account_exists} | %% {error, password_incorrect} lists:map(fun (Key) -> {value, {_Key, Value}} = lists:keysearch(Key, 1, Q), Value end, [<<"username">>, <<"host">>, <<"password">>]). try_unregister_account(Username, Host, Password) -> try unregister_account(Username, Host, Password) of {atomic, ok} -> {atomic, ok} catch error:{badmatch, Error} -> {error, Error} end. unregister_account(Username, Host, Password) -> account_exists = check_account_exists(Username, Host), password_correct = check_password(Username, Host, Password), ok = ejabberd_auth:remove_user(Username, Host, Password), account_doesnt_exist = check_account_exists(Username, Host), {atomic, ok}. %%%---------------------------------------------------------------------- %%% Error texts %%%---------------------------------------------------------------------- get_error_text({error, captcha_non_valid}) -> ?T("The captcha you entered is wrong"); get_error_text({error, exists}) -> ?T("The account already exists"); get_error_text({error, password_incorrect}) -> ?T("Incorrect password"); get_error_text({error, host_unknown}) -> ?T("Host unknown"); get_error_text({error, account_doesnt_exist}) -> ?T("Account doesn't exist"); get_error_text({error, account_exists}) -> ?T("The account was not unregistered"); get_error_text({error, password_not_changed}) -> ?T("The password was not changed"); get_error_text({error, passwords_not_identical}) -> ?T("The passwords are different"); get_error_text({error, wrong_parameters}) -> ?T("Wrong parameters in the web formulary"); get_error_text({error, Why}) -> mod_register:format_error(Why). mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides a web page where users can:"), "", ?T("- Register a new account on the server."), "", ?T("- Change the password from an existing account on the server."), "", ?T("- Unregister an existing account on the server."), "", ?T("This module supports http://../basic/#captcha[CAPTCHA] " "to register a new account. " "To enable this feature, configure the " "top-level _`captcha_cmd`_ and " "top-level _`captcha_url`_ options."), "", ?T("As an example usage, the users of the host 'localhost' can " "visit the page: 'https://localhost:5280/register/' It is " "important to include the last / character in the URL, " "otherwise the subpages URL will be incorrect."), "", ?T("This module is enabled in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers], " "no need to enable in 'modules'."), ?T("The module depends on _`mod_register`_ where all the " "configuration is performed.")], example => ["listen:", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " /register: mod_register_web", "", "modules:", " mod_register: {}"]}. ejabberd-21.12/src/mod_sic.erl0000644000232200023220000001012214154362354016536 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_sic.erl %%% Author : Karim Gemayel %%% Purpose : XEP-0279 Server IP Check %%% Created : 6 Mar 2010 by Karim Gemayel %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_sic). -protocol({xep, 279, '0.2'}). -author('karim.gemayel@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq/1, process_sm_iq/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_SIC_0, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_SIC_0, ?MODULE, process_sm_iq), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_SIC_1, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_SIC_1, ?MODULE, process_sm_iq). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_SIC_0), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SIC_0), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_SIC_1), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SIC_1). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. process_local_iq(#iq{from = #jid{user = User, server = Server, resource = Resource}, type = get} = IQ) -> get_ip({User, Server, Resource}, IQ); process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). process_sm_iq(#iq{from = #jid{user = User, server = Server, resource = Resource}, to = #jid{user = User, server = Server}, type = get} = IQ) -> get_ip({User, Server, Resource}, IQ); process_sm_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). get_ip({User, Server, Resource}, #iq{lang = Lang, sub_els = [#sic{xmlns = NS}]} = IQ) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of {IP, Port} when is_tuple(IP) -> Result = case NS of ?NS_SIC_0 -> #sic{ip = IP, xmlns = NS}; ?NS_SIC_1 -> #sic{ip = IP, port = Port, xmlns = NS} end, xmpp:make_iq_result(IQ, Result); _ -> Txt = ?T("User session not found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end. mod_options(_Host) -> []. mod_doc() -> #{desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0279.html" "[XEP-0279: Server IP Check]. This protocol enables " "a client to discover its external IP address."), "", ?T("WARNING: The protocol extension is deferred and seems " "like there are no clients supporting it, so using this " "module is not recommended and, furthermore, the module " "might be removed in the future.")]}. ejabberd-21.12/src/mod_proxy65_redis.erl0000644000232200023220000001237214154362354020513 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 31 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_redis). -behaviour(mod_proxy65). %% API -export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). -include("logger.hrl"). -record(proxy65, {pid_t :: pid(), pid_i :: pid() | undefined, jid_i :: binary() | undefined}). %%%=================================================================== %%% API %%%=================================================================== init() -> ?DEBUG("Cleaning Redis 'proxy65' table...", []), NodeKey = node_key(), case ejabberd_redis:smembers(NodeKey) of {ok, SIDs} -> SIDKeys = [sid_key(S) || S <- SIDs], JIDs = lists:flatmap( fun(SIDKey) -> case ejabberd_redis:get(SIDKey) of {ok, Val} -> try binary_to_term(Val) of #proxy65{jid_i = J} when is_binary(J) -> [jid_key(J)]; _ -> [] catch _:badarg -> [] end; _ -> [] end end, SIDKeys), ejabberd_redis:multi( fun() -> if SIDs /= [] -> ejabberd_redis:del(SIDKeys), if JIDs /= [] -> ejabberd_redis:del(JIDs); true -> ok end; true -> ok end, ejabberd_redis:del([NodeKey]) end), ok; {error, _} -> {error, db_failure} end. register_stream(SID, Pid) -> SIDKey = sid_key(SID), try {ok, Val} = ejabberd_redis:get(SIDKey), try binary_to_term(Val) of #proxy65{pid_i = undefined} = R -> NewVal = term_to_binary(R#proxy65{pid_i = Pid}), ok = ejabberd_redis:set(SIDKey, NewVal); _ -> {error, conflict} catch _:badarg when Val == undefined -> NewVal = term_to_binary(#proxy65{pid_t = Pid}), {ok, _} = ejabberd_redis:multi( fun() -> ejabberd_redis:set(SIDKey, NewVal), ejabberd_redis:sadd(node_key(), [SID]) end), ok; _:badarg -> ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", [SIDKey, Val]), {error, db_failure} end catch _:{badmatch, {error, _}} -> {error, db_failure} end. unregister_stream(SID) -> SIDKey = sid_key(SID), NodeKey = node_key(), try {ok, Val} = ejabberd_redis:get(SIDKey), try binary_to_term(Val) of #proxy65{jid_i = JID} when is_binary(JID) -> JIDKey = jid_key(JID), {ok, _} = ejabberd_redis:multi( fun() -> ejabberd_redis:del([SIDKey]), ejabberd_redis:srem(JIDKey, [SID]), ejabberd_redis:srem(NodeKey, [SID]) end), ok; _ -> {ok, _} = ejabberd_redis:multi( fun() -> ejabberd_redis:del([SIDKey]), ejabberd_redis:srem(NodeKey, [SID]) end), ok catch _:badarg when Val == undefined -> ok; _:badarg -> ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", [SIDKey, Val]), {error, db_failure} end catch _:{badmatch, {error, _}} -> {error, db_failure} end. activate_stream(SID, IJID, MaxConnections, _Node) -> SIDKey = sid_key(SID), JIDKey = jid_key(IJID), try {ok, Val} = ejabberd_redis:get(SIDKey), try binary_to_term(Val) of #proxy65{pid_t = TPid, pid_i = IPid, jid_i = undefined} = R when is_pid(IPid) -> {ok, Num} = ejabberd_redis:scard(JIDKey), if Num >= MaxConnections -> {error, {limit, IPid, TPid}}; true -> NewVal = term_to_binary(R#proxy65{jid_i = IJID}), {ok, _} = ejabberd_redis:multi( fun() -> ejabberd_redis:sadd(JIDKey, [SID]), ejabberd_redis:set(SIDKey, NewVal) end), {ok, IPid, TPid} end; #proxy65{jid_i = JID} when is_binary(JID) -> {error, conflict}; _ -> {error, notfound} catch _:badarg when Val == undefined -> {error, notfound}; _:badarg -> ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", [SIDKey, Val]), {error, db_failure} end catch _:{badmatch, {error, _}} -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== sid_key(SID) -> <<"ejabberd:proxy65:sid:", SID/binary>>. jid_key(JID) -> <<"ejabberd:proxy65:initiator:", JID/binary>>. node_key() -> Node = erlang:atom_to_binary(node(), latin1), <<"ejabberd:proxy65:node:", Node/binary>>. ejabberd-21.12/src/ejabberd_backend_sup.erl0000644000232200023220000000334614154362354021227 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 24 Feb 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_backend_sup). -behaviour(supervisor). %% API -export([start_link/0]). %% Supervisor callbacks -export([init/1]). %%%=================================================================== %%% API functions %%%=================================================================== start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([]) -> {ok, {{one_for_one, 10, 1}, []}}. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_sip.erl0000644000232200023220000003675014154362354016572 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_sip.erl %%% Author : Evgeny Khramtsov %%% Purpose : SIP RFC-3261 %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2014-2021 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(mod_sip). -protocol({rfc, 3261}). -include("logger.hrl"). -include("translate.hrl"). -ifndef(SIP). -export([start/2, stop/1, depends/2, mod_options/1, mod_doc/0]). start(_, _) -> ?CRITICAL_MSG("ejabberd is not compiled with SIP support", []), {error, sip_not_compiled}. stop(_) -> ok. depends(_, _) -> []. mod_options(_) -> []. mod_doc() -> #{desc => [?T("SIP support has not been enabled.")]}. -else. -behaviour(gen_mod). -behaviour(esip). %% API -export([start/2, stop/1, reload/3, make_response/2, is_my_host/1, at_my_host/1]). -export([data_in/2, data_out/2, message_in/2, message_out/2, request/2, request/3, response/2, locate/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include_lib("esip/include/esip.hrl"). %%%=================================================================== %%% API %%%=================================================================== start(_Host, _Opts) -> ejabberd:start_app(esip), esip:set_config_value(max_server_transactions, 10000), esip:set_config_value(max_client_transactions, 10000), esip:set_config_value( software, <<"ejabberd ", (ejabberd_option:version())/binary>>), esip:set_config_value(module, ?MODULE), Spec = {mod_sip_registrar, {mod_sip_registrar, start_link, []}, transient, 2000, worker, [mod_sip_registrar]}, TmpSupSpec = {mod_sip_proxy_sup, {ejabberd_tmp_sup, start_link, [mod_sip_proxy_sup, mod_sip_proxy]}, permanent, infinity, supervisor, [ejabberd_tmp_sup]}, supervisor:start_child(ejabberd_gen_mod_sup, Spec), supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec), ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. data_in(Data, #sip_socket{type = Transport, addr = {MyIP, MyPort}, peer = {PeerIP, PeerPort}}) -> ?DEBUG( "SIP [~p/in] ~ts:~p -> ~ts:~p:~n~ts", [Transport, inet_parse:ntoa(PeerIP), PeerPort, inet_parse:ntoa(MyIP), MyPort, Data]). data_out(Data, #sip_socket{type = Transport, addr = {MyIP, MyPort}, peer = {PeerIP, PeerPort}}) -> ?DEBUG( "SIP [~p/out] ~ts:~p -> ~ts:~p:~n~ts", [Transport, inet_parse:ntoa(MyIP), MyPort, inet_parse:ntoa(PeerIP), PeerPort, Data]). message_in(#sip{type = request, method = M} = Req, SIPSock) when M /= <<"ACK">>, M /= <<"CANCEL">> -> case action(Req, SIPSock) of {relay, _LServer} -> ok; Action -> request(Req, SIPSock, undefined, Action) end; message_in(ping, SIPSock) -> mod_sip_registrar:ping(SIPSock); message_in(_, _) -> ok. message_out(_, _) -> ok. response(_Resp, _SIPSock) -> ok. request(#sip{method = <<"ACK">>} = Req, SIPSock) -> case action(Req, SIPSock) of {relay, LServer} -> mod_sip_proxy:route(Req, LServer, [{authenticated, true}]); {proxy_auth, LServer} -> mod_sip_proxy:route(Req, LServer, [{authenticated, false}]); _ -> ok end; request(_Req, _SIPSock) -> ok. request(Req, SIPSock, TrID) -> request(Req, SIPSock, TrID, action(Req, SIPSock)). request(Req, SIPSock, TrID, Action) -> case Action of to_me -> process(Req, SIPSock); register -> mod_sip_registrar:request(Req, SIPSock); loop -> make_response(Req, #sip{status = 483, type = response}); {unsupported, Require} -> make_response(Req, #sip{status = 420, type = response, hdrs = [{'unsupported', Require}]}); {relay, LServer} -> case mod_sip_proxy:start(LServer, []) of {ok, Pid} -> mod_sip_proxy:route(Req, SIPSock, TrID, Pid), {mod_sip_proxy, route, [Pid]}; Err -> ?WARNING_MSG("Failed to proxy request ~p: ~p", [Req, Err]), Err end; {proxy_auth, LServer} -> make_response( Req, #sip{status = 407, type = response, hdrs = [{'proxy-authenticate', make_auth_hdr(LServer)}]}); {auth, LServer} -> make_response( Req, #sip{status = 401, type = response, hdrs = [{'www-authenticate', make_auth_hdr(LServer)}]}); deny -> make_response(Req, #sip{status = 403, type = response}); not_found -> make_response(Req, #sip{status = 480, type = response}) end. locate(_SIPMsg) -> ok. find(#uri{user = User, host = Host}) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), if LUser == <<"">> -> to_me; true -> case mod_sip_registrar:find_sockets(LUser, LServer) of [] -> not_found; [_|_] -> {relay, LServer} end end. %%%=================================================================== %%% Internal functions %%%=================================================================== action(#sip{method = <<"REGISTER">>, type = request, hdrs = Hdrs, uri = #uri{user = <<"">>} = URI} = Req, SIPSock) -> case at_my_host(URI) of true -> Require = esip:get_hdrs('require', Hdrs) -- supported(), case Require of [_|_] -> {unsupported, Require}; _ -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), case at_my_host(ToURI) of true -> case check_auth(Req, 'authorization', SIPSock) of true -> register; false -> {auth, jid:nameprep(ToURI#uri.host)} end; false -> deny end end; false -> deny end; action(#sip{method = Method, hdrs = Hdrs, type = request} = Req, SIPSock) -> case esip:get_hdr('max-forwards', Hdrs) of 0 when Method == <<"OPTIONS">> -> to_me; 0 -> loop; _ -> Require = esip:get_hdrs('proxy-require', Hdrs) -- supported(), case Require of [_|_] -> {unsupported, Require}; _ -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), {_, FromURI, _} = esip:get_hdr('from', Hdrs), case at_my_host(FromURI) of true -> case check_auth(Req, 'proxy-authorization', SIPSock) of true -> case at_my_host(ToURI) of true -> find(ToURI); false -> LServer = jid:nameprep(FromURI#uri.host), {relay, LServer} end; false -> {proxy_auth, FromURI#uri.host} end; false -> case at_my_host(ToURI) of true -> find(ToURI); false -> deny end end end end. check_auth(#sip{method = <<"CANCEL">>}, _, _SIPSock) -> true; check_auth(#sip{method = Method, hdrs = Hdrs, body = Body}, AuthHdr, _SIPSock) -> Issuer = case AuthHdr of 'authorization' -> to; 'proxy-authorization' -> from end, {_, #uri{user = User, host = Host}, _} = esip:get_hdr(Issuer, Hdrs), LUser = jid:nodeprep(User), LServer = jid:nameprep(Host), case lists:filter( fun({_, Params}) -> Username = esip:get_param(<<"username">>, Params), Realm = esip:get_param(<<"realm">>, Params), (LUser == esip:unquote(Username)) and (LServer == esip:unquote(Realm)) end, esip:get_hdrs(AuthHdr, Hdrs)) of [Auth|_] -> case ejabberd_auth:get_password_s(LUser, LServer) of <<"">> -> false; Password when is_binary(Password) -> esip:check_auth(Auth, Method, Body, Password); _ScramedPassword -> ?ERROR_MSG("Unable to authenticate ~ts@~ts against SCRAM'ed " "password", [LUser, LServer]), false end; [] -> false end. allow() -> [<<"OPTIONS">>, <<"REGISTER">>]. supported() -> [<<"path">>, <<"outbound">>]. process(#sip{method = <<"OPTIONS">>} = Req, _) -> make_response(Req, #sip{type = response, status = 200, hdrs = [{'allow', allow()}, {'supported', supported()}]}); process(#sip{method = <<"REGISTER">>} = Req, _) -> make_response(Req, #sip{type = response, status = 400}); process(Req, _) -> make_response(Req, #sip{type = response, status = 405, hdrs = [{'allow', allow()}]}). make_auth_hdr(LServer) -> {<<"Digest">>, [{<<"realm">>, esip:quote(LServer)}, {<<"qop">>, esip:quote(<<"auth">>)}, {<<"nonce">>, esip:quote(esip:make_hexstr(20))}]}. make_response(Req, Resp) -> esip:make_response(Req, Resp, esip:make_tag()). at_my_host(#uri{host = Host}) -> is_my_host(jid:nameprep(Host)). is_my_host(LServer) -> gen_mod:is_loaded(LServer, ?MODULE). mod_opt_type(always_record_route) -> econf:bool(); mod_opt_type(flow_timeout_tcp) -> econf:timeout(second); mod_opt_type(flow_timeout_udp) -> econf:timeout(second); mod_opt_type(record_route) -> econf:sip_uri(); mod_opt_type(routes) -> econf:list(econf:sip_uri()); mod_opt_type(via) -> econf:list( fun(L) when is_list(L) -> (econf:and_then( econf:options( #{type => econf:enum([tcp, tls, udp]), host => econf:domain(), port => econf:port()}, [{required, [type, host]}]), fun(Opts) -> Type = proplists:get_value(type, Opts), Host = proplists:get_value(host, Opts), Port = proplists:get_value(port, Opts), {Type, {Host, Port}} end))(L); (U) -> (econf:and_then( econf:url([tls, tcp, udp]), fun(URI) -> {ok, Type, Host, Port, _} = misc:uri_parse(URI), {Type, {unicode:characters_to_binary(Host), Port}} end))(U) end, [unique]). -spec mod_options(binary()) -> [{via, [{tcp | tls | udp, {binary(), 1..65535}}]} | {atom(), term()}]. mod_options(Host) -> Route = #uri{scheme = <<"sip">>, host = Host, params = [{<<"lr">>, <<>>}]}, [{always_record_route, true}, {flow_timeout_tcp, timer:seconds(120)}, {flow_timeout_udp, timer:seconds(29)}, {record_route, Route}, {routes, [Route]}, {via, []}]. mod_doc() -> #{desc => [?T("This module adds SIP proxy/registrar support " "for the corresponding virtual host."), "", ?T("NOTE: It is not enough to just load this module. " "You should also configure listeners and DNS records " "properly. For details see the section about the " "http://../listen/#ejabberd-sip[ejabberd_sip] listen module " "in the ejabberd Documentation.")], opts => [{always_record_route, #{value => "true | false", desc => ?T("Always insert \"Record-Route\" header into " "SIP messages. This approach allows to bypass " "NATs/firewalls a bit more easily. " "The default value is 'true'.")}}, {flow_timeout_tcp, #{value => "timeout()", desc => ?T("The option sets a keep-alive timer for " "https://tools.ietf.org/html/rfc5626[SIP outbound] " "TCP connections. The default value is '2' minutes.")}}, {flow_timeout_udp, #{value => "timeout()", desc => ?T("The options sets a keep-alive timer for " "https://tools.ietf.org/html/rfc5626[SIP outbound] " "UDP connections. The default value is '29' seconds.")}}, {record_route, #{value => ?T("URI"), desc => ?T("When the option 'always_record_route' is set to " "'true' or when https://tools.ietf.org/html/rfc5626" "[SIP outbound] is utilized, ejabberd inserts " "\"Record-Route\" header field with this 'URI' into " "a SIP message. The default is a SIP URI constructed " "from the virtual host on which the module is loaded.")}}, {routes, #{value => "[URI, ...]", desc => ?T("You can set a list of SIP URIs of routes pointing " "to this SIP proxy server. The default is a list containing " "a single SIP URI constructed from the virtual host " "on which the module is loaded.")}}, {via, #{value => "[URI, ...]", desc => ?T("A list to construct \"Via\" headers for " "inserting them into outgoing SIP messages. " "This is useful if you're running your SIP proxy " "in a non-standard network topology. Every 'URI' " "element in the list must be in the form of " "\"scheme://host:port\", where \"transport\" " "must be 'tls', 'tcp', or 'udp', \"host\" must " "be a domain name or an IP address and \"port\" " "must be an internet port number. Note that all " "parts of the 'URI' are mandatory (e.g. you " "cannot omit \"port\" or \"scheme\").")}}], example => ["modules:", " ...", " mod_sip:", " always_record_route: false", " record_route: \"sip:example.com;lr\"", " routes:", " - \"sip:example.com;lr\"", " - \"sip:sip.example.com;lr\"", " flow_timeout_udp: 30 sec", " flow_timeout_tcp: 1 min", " via:", " - tls://sip-tls.example.com:5061", " - tcp://sip-tcp.example.com:5060", " - udp://sip-udp.example.com:5060", " ..."]}. -endif. ejabberd-21.12/src/mod_mam_sql.erl0000644000232200023220000004430214154362354017420 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_mam_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_mam_sql). -behaviour(mod_mam). %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3, is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_mam.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("mod_muc_room.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")), ejabberd_sql:sql_query( LServer, ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")). remove_room(LServer, LName, LHost) -> LUser = jid:encode({LName, LHost, <<>>}), remove_user(LUser, LServer). remove_from_archive(LUser, LServer, none) -> case ejabberd_sql:sql_query(LServer, ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of {error, Reason} -> {error, Reason}; _ -> ok end; remove_from_archive(LUser, LServer, WithJid) -> Peer = jid:encode(jid:remove_resource(WithJid)), case ejabberd_sql:sql_query(LServer, ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of {error, Reason} -> {error, Reason}; _ -> ok end. delete_old_messages(ServerHost, TimeStamp, Type) -> TS = misc:now_to_usec(TimeStamp), case Type of all -> ejabberd_sql:sql_query( ServerHost, ?SQL("delete from archive" " where timestamp < %(TS)d and %(ServerHost)H")); _ -> SType = misc:atom_to_binary(Type), ejabberd_sql:sql_query( ServerHost, ?SQL("delete from archive" " where timestamp < %(TS)d" " and kind=%(SType)s" " and %(ServerHost)H")) end, ok. extended_fields() -> [{withtext, <<"">>}]. store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS) -> SUser = case Type of chat -> LUser; groupchat -> jid:encode({LUser, LHost, <<>>}) end, BarePeer = jid:encode( jid:tolower( jid:remove_resource(Peer))), LPeer = jid:encode( jid:tolower(Peer)), Body = fxml:get_subtag_cdata(Pkt, <<"body">>), SType = misc:atom_to_binary(Type), SqlType = ejabberd_option:sql_type(LServer), XML = case mod_mam_opt:compress_xml(LServer) of true -> J1 = case Type of chat -> jid:encode({LUser, LHost, <<>>}); groupchat -> SUser end, xml_compress:encode(Pkt, J1, LPeer); _ -> fxml:element_to_binary(Pkt) end, case SqlType of mssql -> case ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "archive", ["username=%(SUser)s", "server_host=%(LServer)s", "timestamp=%(TS)d", "peer=%(LPeer)s", "bare_peer=%(BarePeer)s", "xml=N%(XML)s", "txt=N%(Body)s", "kind=%(SType)s", "nick=%(Nick)s"])) of {updated, _} -> ok; Err -> Err end; _ -> case ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "archive", ["username=%(SUser)s", "server_host=%(LServer)s", "timestamp=%(TS)d", "peer=%(LPeer)s", "bare_peer=%(BarePeer)s", "xml=%(XML)s", "txt=%(Body)s", "kind=%(SType)s", "nick=%(Nick)s"])) of {updated, _} -> ok; Err -> Err end end. write_prefs(LUser, _LServer, #archive_prefs{default = Default, never = Never, always = Always}, ServerHost) -> SDefault = erlang:atom_to_binary(Default, utf8), SAlways = misc:term_to_expr(Always), SNever = misc:term_to_expr(Never), case ?SQL_UPSERT( ServerHost, "archive_prefs", ["!username=%(LUser)s", "!server_host=%(ServerHost)s", "def=%(SDefault)s", "always=%(SAlways)s", "never=%(SNever)s"]) of ok -> ok; Err -> Err end. get_prefs(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs" " where username=%(LUser)s and %(LServer)H")) of {selected, [{SDefault, SAlways, SNever}]} -> Default = erlang:binary_to_existing_atom(SDefault, utf8), Always = ejabberd_sql:decode_term(SAlways), Never = ejabberd_sql:decode_term(SNever), {ok, #archive_prefs{us = {LUser, LServer}, default = Default, always = Always, never = Never}}; _ -> error end. select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, MAMQuery, RSM, MsgType, Flags) -> User = case MsgType of chat -> LUser; _ -> jid:encode(JidArchive) end, {Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none), do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags). -spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(), #rsm_set{} | undefined, all | only_count | only_messages) -> {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} | {error, db_failure}. select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, MAMQuery, RSM, Flags) -> Extra = case gen_mod:db_mod(LServer, mod_muc) of mod_muc_sql -> subscribers_table; _ -> SubRooms = case mod_muc_admin:find_hosts(LServer) of [First|_] -> case mod_muc:get_subscribed_rooms(First, JidRequestor) of {ok, L} -> L; {error, _} -> [] end; _ -> [] end, [jid:encode(Jid) || {Jid, _, _} <- SubRooms] end, {Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra), do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags). do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM, MsgType, Query, CountQuery, Flags) -> % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a % reasonable limit on how many stanzas may be pushed to a client in one % request. If a query returns a number of stanzas greater than this limit % and the client did not specify a limit using RSM then the server should % return a policy-violation error to the client." We currently don't do this % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer. QRes = case Flags of all -> {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)}; only_messages -> {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}}; only_count -> {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)} end, case QRes of {{selected, _, Res}, {selected, _, [[Count]]}} -> {Max, Direction, _} = get_max_direction_id(RSM), {Res1, IsComplete} = if Max >= 0 andalso Max /= undefined andalso length(Res) > Max -> if Direction == before -> {lists:nthtail(1, Res), false}; true -> {lists:sublist(Res, Max), false} end; true -> {Res, true} end, MucState = #state{config = #config{anonymous = true}}, JidArchiveS = jid:encode(jid:remove_resource(JidArchive)), {lists:flatmap( fun([TS, XML, PeerBin, Kind, Nick]) -> case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, MsgType, JidRequestor, JidArchive) of {ok, El} -> [{TS, binary_to_integer(TS), El}]; {error, _} -> [] end; ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser -> case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick, MsgType, JidRequestor, JidArchive) of {ok, El} -> [{TS, binary_to_integer(TS), El}]; {error, _} -> [] end; ([User, TS, XML, PeerBin, Kind, Nick]) -> case make_archive_el(User, TS, XML, PeerBin, Kind, Nick, {groupchat, member, MucState}, JidRequestor, jid:decode(User)) of {ok, El} -> mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}], JidRequestor); {error, _} -> [] end end, Res1), IsComplete, binary_to_integer(Count)}; _ -> {[], false, 0} end. export(_Server) -> [{archive_prefs, fun(Host, #archive_prefs{us = {LUser, LServer}, default = Default, always = Always, never = Never}) when LServer == Host -> SDefault = erlang:atom_to_binary(Default, utf8), SAlways = misc:term_to_expr(Always), SNever = misc:term_to_expr(Never), [?SQL_INSERT( "archive_prefs", ["username=%(LUser)s", "server_host=%(LServer)s", "def=%(SDefault)s", "always=%(SAlways)s", "never=%(SNever)s"])]; (_Host, _R) -> [] end}, {archive_msg, fun(Host, #archive_msg{us ={LUser, LServer}, id = _ID, timestamp = TS, peer = Peer, type = Type, nick = Nick, packet = Pkt}) when LServer == Host -> TStmp = misc:now_to_usec(TS), SUser = case Type of chat -> LUser; groupchat -> jid:encode({LUser, LServer, <<>>}) end, BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))), LPeer = jid:encode(jid:tolower(Peer)), XML = fxml:element_to_binary(Pkt), Body = fxml:get_subtag_cdata(Pkt, <<"body">>), SType = misc:atom_to_binary(Type), SqlType = ejabberd_option:sql_type(Host), case SqlType of mssql -> [?SQL_INSERT( "archive", ["username=%(SUser)s", "server_host=%(LServer)s", "timestamp=%(TStmp)d", "peer=%(LPeer)s", "bare_peer=%(BarePeer)s", "xml=N%(XML)s", "txt=N%(Body)s", "kind=%(SType)s", "nick=%(Nick)s"])]; _ -> [?SQL_INSERT( "archive", ["username=%(SUser)s", "server_host=%(LServer)s", "timestamp=%(TStmp)d", "peer=%(LPeer)s", "bare_peer=%(BarePeer)s", "xml=%(XML)s", "txt=%(Body)s", "kind=%(SType)s", "nick=%(Nick)s"])] end; (_Host, _R) -> [] end}]. is_empty_for_user(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(1)d from archive" " where username=%(LUser)s and %(LServer)H limit 1")) of {selected, [{1}]} -> false; _ -> true end. is_empty_for_room(LServer, LName, LHost) -> LUser = jid:encode({LName, LHost, <<>>}), is_empty_for_user(LUser, LServer). %%%=================================================================== %%% Internal functions %%%=================================================================== make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) -> Start = proplists:get_value(start, MAMQuery), End = proplists:get_value('end', MAMQuery), With = proplists:get_value(with, MAMQuery), WithText = proplists:get_value(withtext, MAMQuery), {Max, Direction, ID} = get_max_direction_id(RSM), ODBCType = ejabberd_option:sql_type(LServer), ToString = fun(S) -> ejabberd_sql:to_string_literal(ODBCType, S) end, LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql -> [<<" limit ">>, integer_to_binary(Max+1)]; true -> [] end, TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql -> [<<" TOP ">>, integer_to_binary(Max+1)]; true -> [] end, WithTextClause = if is_binary(WithText), WithText /= <<>> -> [<<" and match (txt) against (">>, ToString(WithText), <<")">>]; true -> [] end, WithClause = case catch jid:tolower(With) of {_, _, <<>>} -> [<<" and bare_peer=">>, ToString(jid:encode(With))]; {_, _, _} -> [<<" and peer=">>, ToString(jid:encode(With))]; _ -> [] end, PageClause = case catch binary_to_integer(ID) of I when is_integer(I), I >= 0 -> case Direction of before -> [<<" AND timestamp < ">>, ID]; 'after' -> [<<" AND timestamp > ">>, ID]; _ -> [] end; _ -> [] end, StartClause = case Start of {_, _, _} -> [<<" and timestamp >= ">>, integer_to_binary(misc:now_to_usec(Start))]; _ -> [] end, EndClause = case End of {_, _, _} -> [<<" and timestamp <= ">>, integer_to_binary(misc:now_to_usec(End))]; _ -> [] end, SUser = ToString(User), SServer = ToString(LServer), HostMatch = case ejabberd_sql:use_new_schema() of true -> [<<" and server_host=", SServer/binary>>]; _ -> <<"">> end, {UserSel, UserWhere} = case ExtraUsernames of Users when is_list(Users) -> EscUsers = [ToString(U) || U <- [User | Users]], {<<" username,">>, [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]}; subscribers_table -> SJid = ToString(jid:encode({User, LServer, <<>>})), RoomName = case ODBCType of sqlite -> <<"room || '@' || host">>; _ -> <<"concat(room, '@', host)">> end, {<<" username,">>, [<<" (username = ">>, SUser, <<" or username in (select ">>, RoomName, <<" from muc_room_subscribers where jid=">>, SJid, HostMatch, <<"))">>]}; _ -> {<<>>, [<<" username=">>, SUser]} end, Query = [<<"SELECT ">>, TopClause, UserSel, <<" timestamp, xml, peer, kind, nick" " FROM archive WHERE">>, UserWhere, HostMatch, WithClause, WithTextClause, StartClause, EndClause, PageClause], QueryPage = case Direction of before -> % ID can be empty because of % XEP-0059: Result Set Management % 2.5 Requesting the Last Page in a Result Set [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>, Query, <<" ORDER BY timestamp DESC ">>, LimitClause, <<") AS t ORDER BY timestamp ASC;">>]; _ -> [Query, <<" ORDER BY timestamp ASC ">>, LimitClause, <<";">>] end, {QueryPage, [<<"SELECT COUNT(*) FROM archive WHERE ">>, UserWhere, HostMatch, WithClause, WithTextClause, StartClause, EndClause, <<";">>]}. -spec get_max_direction_id(rsm_set() | undefined) -> {integer() | undefined, before | 'after' | undefined, binary()}. get_max_direction_id(RSM) -> case RSM of #rsm_set{max = Max, before = Before} when is_binary(Before) -> {Max, before, Before}; #rsm_set{max = Max, 'after' = After} when is_binary(After) -> {Max, 'after', After}; #rsm_set{max = Max} -> {Max, undefined, <<>>}; _ -> {undefined, undefined, <<>>} end. -spec make_archive_el(binary(), binary(), binary(), binary(), binary(), binary(), _, jid(), jid()) -> {ok, xmpp_element()} | {error, invalid_jid | invalid_timestamp | invalid_xml}. make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchive) -> case xml_compress:decode(XML, User, Peer) of #xmlel{} = El -> try binary_to_integer(TS) of TSInt -> try jid:decode(Peer) of PeerJID -> Now = misc:usec_to_now(TSInt), PeerLJID = jid:tolower(PeerJID), T = case Kind of <<"">> -> chat; null -> chat; _ -> misc:binary_to_atom(Kind) end, mod_mam:msg_to_el( #archive_msg{timestamp = Now, id = TS, packet = El, type = T, nick = Nick, peer = PeerLJID}, MsgType, JidRequestor, JidArchive) catch _:{bad_jid, _} -> ?ERROR_MSG("Malformed 'peer' field with value " "'~ts' detected for user ~ts in table " "'archive': invalid JID", [Peer, jid:encode(JidArchive)]), {error, invalid_jid} end catch _:_ -> ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' " "detected for user ~ts in table 'archive': " "not an integer", [TS, jid:encode(JidArchive)]), {error, invalid_timestamp} end; {error, {_, Reason}} -> ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected " "for user ~ts in table 'archive': ~ts", [XML, jid:encode(JidArchive), Reason]), {error, invalid_xml} end. ejabberd-21.12/src/ejabberd_local.erl0000644000232200023220000001311114154362354020032 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_local.erl %%% Author : Alexey Shchepin %%% Purpose : Route local packets %%% Created : 30 Nov 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_local). -author('alexey@process-one.net'). -behaviour(gen_server). %% API -export([start/0, start_link/0]). -export([route/1, get_features/1, bounce_resource_packet/1, host_up/1, host_down/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% deprecated functions: use ejabberd_router:route_iq/3,4 -export([route_iq/2, route_iq/3]). -deprecated([{route_iq, 2}, {route_iq, 3}]). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). -record(state, {}). %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- start() -> ChildSpec = {?MODULE, {?MODULE, start_link, []}, transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_sup, ChildSpec). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec route(stanza()) -> ok. route(Packet) -> ?DEBUG("Local route:~n~ts", [xmpp:pp(Packet)]), Type = xmpp:get_type(Packet), To = xmpp:get_to(Packet), if To#jid.luser /= <<"">> -> ejabberd_sm:route(Packet); is_record(Packet, iq), To#jid.lresource == <<"">> -> gen_iq_handler:handle(?MODULE, Packet); Type == result; Type == error -> ok; true -> ejabberd_hooks:run(local_send_to_resource_hook, To#jid.lserver, [Packet]) end. -spec route_iq(iq(), function()) -> ok. route_iq(IQ, Fun) -> route_iq(IQ, Fun, undefined). -spec route_iq(iq(), function(), undefined | non_neg_integer()) -> ok. route_iq(IQ, Fun, Timeout) -> ejabberd_router:route_iq(IQ, Fun, undefined, Timeout). -spec bounce_resource_packet(stanza()) -> ok | stop. bounce_resource_packet(#presence{to = #jid{lresource = <<"">>}}) -> ok; bounce_resource_packet(#message{to = #jid{lresource = <<"">>}, type = headline}) -> ok; bounce_resource_packet(Packet) -> Lang = xmpp:get_lang(Packet), Txt = ?T("No available resource found"), Err = xmpp:err_item_not_found(Txt, Lang), ejabberd_router:route_error(Packet, Err), stop. -spec get_features(binary()) -> [binary()]. get_features(Host) -> gen_iq_handler:get_features(?MODULE, Host). %%==================================================================== %% gen_server callbacks %%==================================================================== init([]) -> process_flag(trap_exit, true), lists:foreach(fun host_up/1, ejabberd_option:hosts()), ejabberd_hooks:add(host_up, ?MODULE, host_up, 10), ejabberd_hooks:add(host_down, ?MODULE, host_down, 100), gen_iq_handler:start(?MODULE), update_table(), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({route, Packet}, State) -> route(Packet), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> lists:foreach(fun host_down/1, ejabberd_option:hosts()), ejabberd_hooks:delete(host_up, ?MODULE, host_up, 10), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 100), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec update_table() -> ok. update_table() -> catch mnesia:delete_table(iq_response), ok. host_up(Host) -> Owner = case whereis(?MODULE) of undefined -> self(); Pid -> Pid end, ejabberd_router:register_route(Host, Host, {apply, ?MODULE, route}, Owner), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, bounce_resource_packet, 100). host_down(Host) -> Owner = case whereis(?MODULE) of undefined -> self(); Pid -> Pid end, ejabberd_router:unregister_route(Host, Owner), ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, bounce_resource_packet, 100). ejabberd-21.12/src/nodetree_tree.erl0000644000232200023220000001672314154362354017762 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : nodetree_tree.erl %%% Author : Christophe Romain %%% Purpose : Standard node tree plugin %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the default PubSub node tree plugin. %%%

It is used as a default for all unknown PubSub node type. It can serve %%% as a developer basis and reference to build its own custom pubsub node tree %%% types.

%%%

PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

%%%

The API isn't stabilized yet. The pubsub plugin %%% development is still a work in progress. However, the system is already %%% usable and useful as is. Please, send us comments, feedback and %%% improvements.

-module(nodetree_tree). -behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). -include_lib("stdlib/include/qlc.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, get_nodes/1, get_all_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). init(_Host, _ServerHost, _Options) -> ejabberd_mnesia:create(?MODULE, pubsub_node, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_node)}, {index, [id]}]), %% mnesia:transform_table(pubsub_state, ignore, StatesFields) ok. terminate(_Host, _ServerHost) -> ok. options() -> [{virtual_tree, false}]. set_node(Node) when is_record(Node, pubsub_node) -> mnesia:write(Node). get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> case mnesia:read({pubsub_node, {Host, Node}}) of [Record] when is_record(Record, pubsub_node) -> Record; _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. get_node(Nidx) -> case mnesia:index_read(pubsub_node, Nidx, #pubsub_node.id) of [Record] when is_record(Record, pubsub_node) -> Record; _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. get_nodes(Host) -> get_nodes(Host, infinity). get_nodes(Host, infinity) -> mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}); get_nodes(Host, Limit) -> case mnesia:select( pubsub_node, ets:fun2ms( fun(#pubsub_node{nodeid = {H, _}} = Node) when H == Host -> Node end), Limit, read) of '$end_of_table' -> []; {Nodes, _} -> Nodes end. get_all_nodes({_U, _S, _R} = Owner) -> Host = jid:tolower(jid:remove_resource(Owner)), mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}); get_all_nodes(Host) -> mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}) ++ mnesia:match_object(#pubsub_node{nodeid = {{'_', Host, '_'}, '_'}, _ = '_'}). get_parentnodes(Host, Node, _From) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of [Record] when is_record(Record, pubsub_node) -> Record#pubsub_node.parents; _ -> [] end. get_parentnodes_tree(Host, Node, _From) -> get_parentnodes_tree(Host, Node, 0, []). get_parentnodes_tree(Host, Node, Level, Acc) -> case catch mnesia:read({pubsub_node, {Host, Node}}) of [Record] when is_record(Record, pubsub_node) -> Tree = [{Level, [Record]}|Acc], case Record#pubsub_node.parents of [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); _ -> Tree end; _ -> Acc end. get_subnodes(Host, <<>>, infinity) -> mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parents = [], _ = '_'}); get_subnodes(Host, <<>>, Limit) -> case mnesia:select( pubsub_node, ets:fun2ms( fun(#pubsub_node{nodeid = {H, _}, parents = []} = Node) when H == Host -> Node end), Limit, read) of '$end_of_table' -> []; {Nodes, _} -> Nodes end; get_subnodes(Host, Node, infinity) -> Q = qlc:q([N || #pubsub_node{nodeid = {NHost, _}, parents = Parents} = N <- mnesia:table(pubsub_node), Host == NHost, lists:member(Node, Parents)]), qlc:e(Q); get_subnodes(Host, Node, Limit) -> case mnesia:select( pubsub_node, ets:fun2ms( fun(#pubsub_node{nodeid = {H, _}, parents = Ps} = N) when H == Host andalso Ps /= [] -> N end), Limit, read) of '$end_of_table' -> []; {Nodes, _} -> lists:filter( fun(#pubsub_node{parents = Parents}) -> lists:member(Node, Parents) end, Nodes) end. get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(Host, Node) -> case get_node(Host, Node) of {error, _} -> []; Rec -> BasePlugin = misc:binary_to_atom(<<"node_", (Rec#pubsub_node.type)/binary>>), {result, BasePath} = BasePlugin:node_to_path(Node), mnesia:foldl(fun (#pubsub_node{nodeid = {H, N}} = R, Acc) -> Plugin = misc:binary_to_atom(<<"node_", (R#pubsub_node.type)/binary>>), {result, Path} = Plugin:node_to_path(N), case lists:prefix(BasePath, Path) and (H == Host) of true -> [R | Acc]; false -> Acc end end, [], pubsub_node) end. create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), case mnesia:read({pubsub_node, {Host, Node}}) of [] -> ParentExists = case Host of {_U, _S, _R} -> %% This is special case for PEP handling %% PEP does not uses hierarchy true; _ -> case Parents of [] -> true; [Parent | _] -> case catch mnesia:read({pubsub_node, {Host, Parent}}) of [#pubsub_node{owners = [{<<>>, Host, <<>>}]}] -> true; [#pubsub_node{owners = Owners}] -> lists:member(BJID, Owners); _ -> false end; _ -> false end end, case ParentExists of true -> Nidx = pubsub_index:new(node), mnesia:write(#pubsub_node{nodeid = {Host, Node}, id = Nidx, parents = Parents, type = Type, owners = [BJID], options = Options}), {ok, Nidx}; false -> {error, xmpp:err_forbidden()} end; _ -> {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())} end. delete_node(Host, Node) -> Removed = get_subnodes_tree(Host, Node), lists:foreach(fun (#pubsub_node{nodeid = {_, SubNode}, id = SubNidx}) -> pubsub_index:free(node, SubNidx), mnesia:delete({pubsub_node, {Host, SubNode}}) end, Removed), Removed. ejabberd-21.12/src/mod_pubsub.erl0000644000232200023220000047510114154362354017274 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_pubsub.erl %%% Author : Christophe Romain %%% Purpose : Publish Subscribe service (XEP-0060) %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% Support for subscription-options and multi-subscribe features was %%% added by Brian Cully (bjc AT kublai.com). Subscriptions and options are %%% stored in the pubsub_subscription table, with a link to them provided %%% by the subscriptions field of pubsub_state. For information on %%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6, %%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see %%% XEP-0060 section 12.18. -module(mod_pubsub). -behaviour(gen_mod). -behaviour(gen_server). -author('christophe.romain@process-one.net'). -protocol({xep, 60, '1.14'}). -protocol({xep, 163, '1.2'}). -protocol({xep, 248, '0.2'}). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("pubsub.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -include("ejabberd_commands.hrl"). -define(STDTREE, <<"tree">>). -define(STDNODE, <<"flat">>). -define(PEPNODE, <<"pep">>). %% exports for hooks -export([presence_probe/3, caps_add/3, caps_update/3, in_subscription/2, out_subscription/1, on_self_presence/1, on_user_offline/2, remove_user/2, disco_local_identity/5, disco_local_features/5, disco_local_items/5, disco_sm_identity/5, disco_sm_features/5, disco_sm_items/5, c2s_handle_info/2]). %% exported iq handlers -export([iq_sm/1, process_disco_info/1, process_disco_items/1, process_pubsub/1, process_pubsub_owner/1, process_vcard/1, process_commands/1]). %% exports for console debug manual use -export([create_node/5, create_node/7, delete_node/3, subscribe_node/5, unsubscribe_node/5, publish_item/6, publish_item/8, delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3, get_cached_item/2, get_configure/5, set_configure/5, tree_action/3, node_action/4, node_call/4]). %% general helpers for plugins -export([extended_error/2, service_jid/1, tree/1, tree/2, plugin/2, plugins/1, config/3, host/1, serverhost/1]). %% pubsub#errors -export([err_closed_node/0, err_configuration_required/0, err_invalid_jid/0, err_invalid_options/0, err_invalid_payload/0, err_invalid_subid/0, err_item_forbidden/0, err_item_required/0, err_jid_required/0, err_max_items_exceeded/0, err_max_nodes_exceeded/0, err_nodeid_required/0, err_not_in_roster_group/0, err_not_subscribed/0, err_payload_too_big/0, err_payload_required/0, err_pending_subscription/0, err_precondition_not_met/0, err_presence_subscription_required/0, err_subid_required/0, err_too_many_subscriptions/0, err_unsupported/1, err_unsupported_access_model/0]). %% API and gen_server callbacks -export([start/2, stop/1, init/1, handle_call/3, handle_cast/2, handle_info/2, mod_doc/0, terminate/2, code_change/3, depends/2, mod_opt_type/1, mod_options/1]). %% ejabberd commands -export([get_commands_spec/0, delete_old_items/1, delete_expired_items/0]). -export([route/1]). %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- -export_type([ host/0, hostPubsub/0, hostPEP/0, %% nodeIdx/0, nodeId/0, itemId/0, subId/0, payload/0, %% nodeOption/0, nodeOptions/0, subOption/0, subOptions/0, pubOption/0, pubOptions/0, %% affiliation/0, subscription/0, accessModel/0, publishModel/0 ]). %% -type payload() defined here because the -type xmlel() is not accessible %% from pubsub.hrl -type(payload() :: [] | [xmlel(),...]). -export_type([ pubsubNode/0, pubsubState/0, pubsubItem/0, pubsubSubscription/0, pubsubLastItem/0 ]). -type(pubsubNode() :: #pubsub_node{ nodeid :: {Host::mod_pubsub:host(), Node::mod_pubsub:nodeId()}, id :: Nidx::mod_pubsub:nodeIdx(), parents :: [Node::mod_pubsub:nodeId()], type :: Type::binary(), owners :: [Owner::ljid(),...], options :: Opts::mod_pubsub:nodeOptions() } ). -type(pubsubState() :: #pubsub_state{ stateid :: {Entity::ljid(), Nidx::mod_pubsub:nodeIdx()}, nodeidx :: Nidx::mod_pubsub:nodeIdx(), items :: [ItemId::mod_pubsub:itemId()], affiliation :: Affs::mod_pubsub:affiliation(), subscriptions :: [{Sub::mod_pubsub:subscription(), SubId::mod_pubsub:subId()}] } ). -type(pubsubItem() :: #pubsub_item{ itemid :: {ItemId::mod_pubsub:itemId(), Nidx::mod_pubsub:nodeIdx()}, nodeidx :: Nidx::mod_pubsub:nodeIdx(), creation :: {erlang:timestamp(), ljid()}, modification :: {erlang:timestamp(), ljid()}, payload :: mod_pubsub:payload() } ). -type(pubsubSubscription() :: #pubsub_subscription{ subid :: SubId::mod_pubsub:subId(), options :: [] | mod_pubsub:subOptions() } ). -type(pubsubLastItem() :: #pubsub_last_item{ nodeid :: {binary(), mod_pubsub:nodeIdx()}, itemid :: mod_pubsub:itemId(), creation :: {erlang:timestamp(), ljid()}, payload :: mod_pubsub:payload() } ). -record(state, { server_host, hosts, access, pep_mapping = [], ignore_pep_from_offline = true, last_item_cache = false, max_items_node = ?MAXITEMS, max_subscriptions_node = undefined, default_node_config = [], nodetree = <<"nodetree_", (?STDTREE)/binary>>, plugins = [?STDNODE], db_type }). -type(state() :: #state{ server_host :: binary(), hosts :: [mod_pubsub:hostPubsub()], access :: atom(), pep_mapping :: [{binary(), binary()}], ignore_pep_from_offline :: boolean(), last_item_cache :: boolean(), max_items_node :: non_neg_integer()|unlimited, max_subscriptions_node :: non_neg_integer()|undefined, default_node_config :: [{atom(), binary()|boolean()|integer()|atom()}], nodetree :: binary(), plugins :: [binary(),...], db_type :: atom() } ). -type subs_by_depth() :: [{integer(), [{#pubsub_node{}, [{ljid(), subId(), subOptions()}]}]}]. start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- -spec init([binary() | [{_,_}],...]) -> {'ok',state()}. init([ServerHost|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), Hosts = gen_mod:get_opt_hosts(Opts), Access = mod_pubsub_opt:access_createnode(Opts), PepOffline = mod_pubsub_opt:ignore_pep_from_offline(Opts), LastItemCache = mod_pubsub_opt:last_item_cache(Opts), MaxItemsNode = mod_pubsub_opt:max_items_node(Opts), MaxSubsNode = mod_pubsub_opt:max_subscriptions_node(Opts), ejabberd_mnesia:create(?MODULE, pubsub_last_item, [{ram_copies, [node()]}, {attributes, record_info(fields, pubsub_last_item)}]), DBMod = gen_mod:db_mod(Opts, ?MODULE), AllPlugins = lists:flatmap( fun(Host) -> DBMod:init(Host, ServerHost, Opts), ejabberd_router:register_route( Host, ServerHost, {apply, ?MODULE, route}), {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), DefaultModule = plugin(Host, hd(Plugins)), DefaultNodeCfg = merge_config( [mod_pubsub_opt:default_node_config(Opts), DefaultModule:options()]), lists:foreach( fun(H) -> T = gen_mod:get_module_proc(H, config), try ets:new(T, [set, named_table]), ets:insert(T, {nodetree, NodeTree}), ets:insert(T, {plugins, Plugins}), ets:insert(T, {last_item_cache, LastItemCache}), ets:insert(T, {max_items_node, MaxItemsNode}), ets:insert(T, {max_subscriptions_node, MaxSubsNode}), ets:insert(T, {default_node_config, DefaultNodeCfg}), ets:insert(T, {pep_mapping, PepMapping}), ets:insert(T, {ignore_pep_from_offline, PepOffline}), ets:insert(T, {host, Host}), ets:insert(T, {access, Access}) catch error:badarg when H == ServerHost -> ok end end, [Host, ServerHost]), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB, ?MODULE, process_pubsub), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER, ?MODULE, process_pubsub_owner), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_vcard), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, ?MODULE, process_commands), Plugins end, Hosts), ejabberd_hooks:add(c2s_self_presence, ServerHost, ?MODULE, on_self_presence, 75), ejabberd_hooks:add(c2s_terminated, ServerHost, ?MODULE, on_user_offline, 75), ejabberd_hooks:add(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), ejabberd_hooks:add(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), ejabberd_hooks:add(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), ejabberd_hooks:add(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), ejabberd_hooks:add(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), ejabberd_hooks:add(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), ejabberd_hooks:add(remove_user, ServerHost, ?MODULE, remove_user, 50), ejabberd_hooks:add(c2s_handle_info, ServerHost, ?MODULE, c2s_handle_info, 50), case lists:member(?PEPNODE, AllPlugins) of true -> ejabberd_hooks:add(caps_add, ServerHost, ?MODULE, caps_add, 80), ejabberd_hooks:add(caps_update, ServerHost, ?MODULE, caps_update, 80), ejabberd_hooks:add(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), ejabberd_hooks:add(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), ejabberd_hooks:add(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB, ?MODULE, iq_sm), gen_iq_handler:add_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER, ?MODULE, iq_sm); false -> ok end, ejabberd_commands:register_commands(?MODULE, get_commands_spec()), NodeTree = config(ServerHost, nodetree), Plugins = config(ServerHost, plugins), PepMapping = config(ServerHost, pep_mapping), DBType = mod_pubsub_opt:db_type(ServerHost), {ok, #state{hosts = Hosts, server_host = ServerHost, access = Access, pep_mapping = PepMapping, ignore_pep_from_offline = PepOffline, last_item_cache = LastItemCache, max_items_node = MaxItemsNode, nodetree = NodeTree, plugins = Plugins, db_type = DBType}}. depends(ServerHost, Opts) -> [Host|_] = gen_mod:get_opt_hosts(Opts), Plugins = mod_pubsub_opt:plugins(Opts), Db = mod_pubsub_opt:db_type(Opts), lists:flatmap( fun(Name) -> Plugin = plugin(Db, Name), try apply(Plugin, depends, [Host, ServerHost, Opts]) catch _:undef -> [] end end, Plugins). %% @doc Call the init/1 function for each plugin declared in the config file. %% The default plugin module is implicit. %%

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

%%

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

init_plugins(Host, ServerHost, Opts) -> TreePlugin = tree(Host, mod_pubsub_opt:nodetree(Opts)), TreePlugin:init(Host, ServerHost, Opts), Plugins = mod_pubsub_opt:plugins(Opts), PepMapping = mod_pubsub_opt:pep_mapping(Opts), PluginsOK = lists:foldl( fun (Name, Acc) -> Plugin = plugin(Host, Name), apply(Plugin, init, [Host, ServerHost, Opts]), [Name | Acc] end, [], Plugins), {lists:reverse(PluginsOK), TreePlugin, PepMapping}. terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> lists:foreach( fun (Name) -> Plugin = plugin(Host, Name), Plugin:terminate(Host, ServerHost) end, Plugins), TreePlugin:terminate(Host, ServerHost), ok. %% ------- %% disco hooks handling functions %% -spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_local_identity(Acc, _From, To, <<>>, _Lang) -> case lists:member(?PEPNODE, plugins(host(To#jid.lserver))) of true -> [#identity{category = <<"pubsub">>, type = <<"pep">>} | Acc]; false -> Acc end; disco_local_identity(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_local_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]} | empty. disco_local_features(Acc, _From, To, <<>>, _Lang) -> Host = host(To#jid.lserver), Feats = case Acc of {result, I} -> I; _ -> [] end, {result, Feats ++ [?NS_PUBSUB|[feature(F) || F <- features(Host, <<>>)]]}; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_local_items({error, stanza_error()} | {result, [disco_item()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [disco_item()]} | empty. disco_local_items(Acc, _From, _To, <<>>, _Lang) -> Acc; disco_local_items(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_sm_identity(Acc, From, To, Node, _Lang) -> disco_identity(jid:tolower(jid:remove_resource(To)), Node, From) ++ Acc. -spec disco_identity(host(), binary(), jid()) -> [identity()]. disco_identity(_Host, <<>>, _From) -> [#identity{category = <<"pubsub">>, type = <<"pep">>}]; disco_identity(Host, Node, From) -> Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, _} -> {result, [#identity{category = <<"pubsub">>, type = <<"pep">>}, #identity{category = <<"pubsub">>, type = <<"leaf">>, name = get_option(Options, title, <<>>)}]}; _ -> {result, []} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> Result; _ -> [] end. -spec disco_sm_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures} = _Acc, From, To, Node, _Lang) -> {result, OtherFeatures ++ disco_features(jid:tolower(jid:remove_resource(To)), Node, From)}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_features(ljid(), binary(), jid()) -> [binary()]. disco_features(Host, <<>>, _From) -> [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]; disco_features(Host, Node, From) -> Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, _} -> {result, [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, <<"pep">>)]]}; _ -> {result, []} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> Result; _ -> [] end. -spec disco_sm_items({error, stanza_error()} | {result, [disco_item()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [disco_item()]}. disco_sm_items(empty, From, To, Node, Lang) -> disco_sm_items({result, []}, From, To, Node, Lang); disco_sm_items({result, OtherItems}, From, To, Node, _Lang) -> {result, lists:usort(OtherItems ++ disco_items(jid:tolower(jid:remove_resource(To)), Node, From))}; disco_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. -spec disco_items(ljid(), binary(), jid()) -> [disco_item()]. disco_items(Host, <<>>, From) -> MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), Action = fun(#pubsub_node{nodeid = {_, Node}, options = Options, type = Type, id = Nidx, owners = O}, Acc) -> Owners = node_owners_call(Host, Type, Nidx, O), case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, _} -> [#disco_item{node = Node, jid = jid:make(Host), name = get_option(Options, title, <<>>)} | Acc]; _ -> Acc end end, NodeBloc = fun() -> case tree_call(Host, get_nodes, [Host, MaxNodes]) of Nodes when is_list(Nodes) -> {result, lists:foldl(Action, [], Nodes)}; Error -> Error end end, case transaction(Host, NodeBloc, sync_dirty) of {result, Items} -> Items; _ -> [] end; disco_items(Host, Node, From) -> Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) of {result, Items} -> {result, [#disco_item{jid = jid:make(Host), name = ItemId} || #pubsub_item{itemid = {ItemId, _}} <- Items]}; _ -> {result, []} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> Result; _ -> [] end. %% ------- %% presence and session hooks handling functions %% -spec caps_add(jid(), jid(), [binary()]) -> ok. caps_add(JID, JID, Features) -> %% Send the owner his last PEP items. send_last_pep(JID, JID, Features); caps_add(#jid{lserver = S1} = From, #jid{lserver = S2} = To, Features) when S1 =/= S2 -> %% When a remote contact goes online while the local user is offline, the %% remote contact won't receive last items from the local user even if %% ignore_pep_from_offline is set to false. To work around this issue a bit, %% we'll also send the last items to remote contacts when the local user %% connects. That's the reason to use the caps_add hook instead of the %% presence_probe_hook for remote contacts: The latter is only called when a %% contact becomes available; the former is also executed when the local %% user goes online (because that triggers the contact to send a presence %% packet with CAPS). send_last_pep(To, From, Features); caps_add(_From, _To, _Features) -> ok. -spec caps_update(jid(), jid(), [binary()]) -> ok. caps_update(From, To, Features) -> send_last_pep(To, From, Features). -spec presence_probe(jid(), jid(), pid()) -> ok. presence_probe(#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, _Pid) -> %% ignore presence_probe from my other resources ok; presence_probe(#jid{lserver = S} = From, #jid{lserver = S} = To, _Pid) -> send_last_pep(To, From, unknown); presence_probe(_From, _To, _Pid) -> %% ignore presence_probe from remote contacts, those are handled via caps_add ok. -spec on_self_presence({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. on_self_presence({_, #{pres_last := _}} = Acc) -> % Just a presence update. Acc; on_self_presence({#presence{type = available}, #{jid := JID}} = Acc) -> send_last_items(JID), Acc; on_self_presence(Acc) -> Acc. -spec on_user_offline(ejabberd_c2s:state(), atom()) -> ejabberd_c2s:state(). on_user_offline(#{jid := JID} = C2SState, _Reason) -> purge_offline(jid:tolower(JID)), C2SState; on_user_offline(C2SState, _Reason) -> C2SState. %% ------- %% subscription hooks handling functions %% -spec out_subscription(presence()) -> any(). out_subscription(#presence{type = subscribed, from = From, to = To}) -> if From#jid.lserver == To#jid.lserver -> send_last_pep(jid:remove_resource(From), To, unknown); true -> ok end; out_subscription(_) -> ok. -spec in_subscription(boolean(), presence()) -> true. in_subscription(_, #presence{to = To, from = Owner, type = unsubscribed}) -> unsubscribe_user(jid:remove_resource(To), Owner), true; in_subscription(_, _) -> true. -spec unsubscribe_user(jid(), jid()) -> ok. unsubscribe_user(Entity, Owner) -> lists:foreach( fun(ServerHost) -> unsubscribe_user(ServerHost, Entity, Owner) end, lists:usort( lists:foldl( fun(UserHost, Acc) -> case gen_mod:is_loaded(UserHost, mod_pubsub) of true -> [UserHost|Acc]; false -> Acc end end, [], [Entity#jid.lserver, Owner#jid.lserver]))). -spec unsubscribe_user(binary(), jid(), jid()) -> ok. unsubscribe_user(Host, Entity, Owner) -> BJID = jid:tolower(jid:remove_resource(Owner)), lists:foreach( fun (PType) -> case node_action(Host, PType, get_entity_subscriptions, [Host, Entity]) of {result, Subs} -> lists:foreach( fun({#pubsub_node{options = Options, owners = O, id = Nidx}, subscribed, _, JID}) -> Unsubscribe = match_option(Options, access_model, presence) andalso lists:member(BJID, node_owners_action(Host, PType, Nidx, O)), case Unsubscribe of true -> node_action(Host, PType, unsubscribe_node, [Nidx, Entity, JID, all]); false -> ok end; (_) -> ok end, Subs); _ -> ok end end, plugins(Host)). %% ------- %% user remove hook handling function %% -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Entity = jid:make(LUser, LServer), Host = host(LServer), HomeTreeBase = <<"/home/", LServer/binary, "/", LUser/binary>>, lists:foreach( fun(PType) -> case node_action(Host, PType, get_entity_subscriptions, [Host, Entity]) of {result, Subs} -> lists:foreach( fun({#pubsub_node{id = Nidx}, _, _, JID}) -> node_action(Host, PType, unsubscribe_node, [Nidx, Entity, JID, all]); (_) -> ok end, Subs), case node_action(Host, PType, get_entity_affiliations, [Host, Entity]) of {result, Affs} -> lists:foreach( fun({#pubsub_node{nodeid = {H, N}, parents = []}, owner}) -> delete_node(H, N, Entity); ({#pubsub_node{nodeid = {H, N}, type = Type}, owner}) when N == HomeTreeBase, Type == <<"hometree">> -> delete_node(H, N, Entity); ({#pubsub_node{id = Nidx}, _}) -> case node_action(Host, PType, get_state, [Nidx, jid:tolower(Entity)]) of {result, #pubsub_state{items = ItemIds}} -> node_action(Host, PType, remove_extra_items, [Nidx, 0, ItemIds]), node_action(Host, PType, set_affiliation, [Nidx, Entity, none]); _ -> ok end end, Affs); _ -> ok end; _ -> ok end end, plugins(Host)). handle_call(server_host, _From, State) -> {reply, State#state.server_host, State}; handle_call(plugins, _From, State) -> {reply, State#state.plugins, State}; handle_call(pep_mapping, _From, State) -> {reply, State#state.pep_mapping, State}; handle_call(nodetree, _From, State) -> {reply, State#state.nodetree, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({route, Packet}, State) -> try route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{hosts = Hosts, server_host = ServerHost, nodetree = TreePlugin, plugins = Plugins}) -> case lists:member(?PEPNODE, Plugins) of true -> ejabberd_hooks:delete(caps_add, ServerHost, ?MODULE, caps_add, 80), ejabberd_hooks:delete(caps_update, ServerHost, ?MODULE, caps_update, 80), ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 75), ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 75), ejabberd_hooks:delete(disco_sm_items, ServerHost, ?MODULE, disco_sm_items, 75), gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_sm, ServerHost, ?NS_PUBSUB_OWNER); false -> ok end, ejabberd_hooks:delete(c2s_self_presence, ServerHost, ?MODULE, on_self_presence, 75), ejabberd_hooks:delete(c2s_terminated, ServerHost, ?MODULE, on_user_offline, 75), ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 75), ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, disco_local_features, 75), ejabberd_hooks:delete(disco_local_items, ServerHost, ?MODULE, disco_local_items, 75), ejabberd_hooks:delete(presence_probe_hook, ServerHost, ?MODULE, presence_probe, 80), ejabberd_hooks:delete(roster_in_subscription, ServerHost, ?MODULE, in_subscription, 50), ejabberd_hooks:delete(roster_out_subscription, ServerHost, ?MODULE, out_subscription, 50), ejabberd_hooks:delete(remove_user, ServerHost, ?MODULE, remove_user, 50), ejabberd_hooks:delete(c2s_handle_info, ServerHost, ?MODULE, c2s_handle_info, 50), lists:foreach( fun(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PUBSUB_OWNER), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS), terminate_plugins(Host, ServerHost, Plugins, TreePlugin), ejabberd_router:unregister_route(Host) end, Hosts), case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_info(#iq{from = From, to = To, lang = Lang, type = get, sub_els = [#disco_info{node = Node}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Info = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<>>, <<>>]), case iq_disco_info(ServerHost, Host, Node, From, Lang) of {result, IQRes} -> XData = IQRes#disco_info.xdata ++ Info, xmpp:make_iq_result(IQ, IQRes#disco_info{node = Node, xdata = XData}); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_items(#iq{type = get, from = From, to = To, sub_els = [#disco_items{node = Node} = SubEl]} = IQ) -> Host = To#jid.lserver, case iq_disco_items(Host, Node, From, SubEl#disco_items.rsm) of {result, IQRes} -> xmpp:make_iq_result(IQ, IQRes#disco_items{node = Node}); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec process_pubsub(iq()) -> iq(). process_pubsub(#iq{to = To} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Access = config(ServerHost, access), case iq_pubsub(Host, Access, IQ) of {result, IQRes} -> xmpp:make_iq_result(IQ, IQRes); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec process_pubsub_owner(iq()) -> iq(). process_pubsub_owner(#iq{to = To} = IQ) -> Host = To#jid.lserver, case iq_pubsub_owner(Host, IQ) of {result, IQRes} -> xmpp:make_iq_result(IQ, IQRes); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), xmpp:make_iq_result(IQ, iq_get_vcard(ServerHost, Lang)); process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). -spec process_commands(iq()) -> iq(). process_commands(#iq{type = set, to = To, from = From, sub_els = [#adhoc_command{} = Request]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Plugins = config(ServerHost, plugins), Access = config(ServerHost, access), case adhoc_request(Host, ServerHost, From, Request, Access, Plugins) of {error, Error} -> xmpp:make_error(IQ, Error); Response -> xmpp:make_iq_result( IQ, xmpp_util:make_adhoc_response(Request, Response)) end; process_commands(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). -spec route(stanza()) -> ok. route(#iq{to = To} = IQ) when To#jid.lresource == <<"">> -> ejabberd_router:process_iq(IQ); route(Packet) -> To = xmpp:get_to(Packet), case To of #jid{luser = <<>>, lresource = <<>>} -> case Packet of #message{type = T} when T /= error -> case find_authorization_response(Packet) of undefined -> ok; {error, Err} -> ejabberd_router:route_error(Packet, Err); AuthResponse -> handle_authorization_response( To#jid.lserver, Packet, AuthResponse) end; _ -> Err = xmpp:err_service_unavailable(), ejabberd_router:route_error(Packet, Err) end; _ -> Err = xmpp:err_item_not_found(), ejabberd_router:route_error(Packet, Err) end. -spec command_disco_info(binary(), binary(), jid()) -> {result, disco_info()}. command_disco_info(_Host, ?NS_COMMANDS, _From) -> {result, #disco_info{identities = [#identity{category = <<"automation">>, type = <<"command-list">>}]}}; command_disco_info(_Host, ?NS_PUBSUB_GET_PENDING, _From) -> {result, #disco_info{identities = [#identity{category = <<"automation">>, type = <<"command-node">>}], features = [?NS_COMMANDS]}}. -spec node_disco_info(binary(), binary(), jid()) -> {result, disco_info()} | {error, stanza_error()}. node_disco_info(Host, Node, From) -> node_disco_info(Host, Node, From, true, true). -spec node_disco_info(binary(), binary(), jid(), boolean(), boolean()) -> {result, disco_info()} | {error, stanza_error()}. node_disco_info(Host, Node, _From, _Identity, _Features) -> Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> NodeType = case get_option(Options, node_type) of collection -> <<"collection">>; _ -> <<"leaf">> end, Affs = case node_call(Host, Type, get_node_affiliations, [Nidx]) of {result, As} -> As; _ -> [] end, Subs = case node_call(Host, Type, get_node_subscriptions, [Nidx]) of {result, Ss} -> Ss; _ -> [] end, Meta = [{title, get_option(Options, title, <<>>)}, {description, get_option(Options, description, <<>>)}, {owner, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= owner]}, {publisher, [jid:make(LJID) || {LJID, Aff} <- Affs, Aff =:= publisher]}, {access_model, get_option(Options, access_model, open)}, {publish_model, get_option(Options, publish_model, publishers)}, {num_subscribers, length(Subs)}], XData = #xdata{type = result, fields = pubsub_meta_data:encode(Meta)}, Is = [#identity{category = <<"pubsub">>, type = NodeType}], Fs = [?NS_PUBSUB | [feature(F) || F <- plugin_features(Host, Type)]], {result, #disco_info{identities = Is, features = Fs, xdata = [XData]}} end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; Other -> Other end. -spec iq_disco_info(binary(), binary(), binary(), jid(), binary()) -> {result, disco_info()} | {error, stanza_error()}. iq_disco_info(ServerHost, Host, SNode, From, Lang) -> [Node | _] = case SNode of <<>> -> [<<>>]; _ -> str:tokens(SNode, <<"!">>) end, case Node of <<>> -> Name = mod_pubsub_opt:name(ServerHost), {result, #disco_info{ identities = [#identity{ category = <<"pubsub">>, type = <<"service">>, name = translate:translate(Lang, Name)}], features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_PUBSUB, ?NS_COMMANDS, ?NS_VCARD | [feature(F) || F <- features(Host, Node)]]}}; ?NS_COMMANDS -> command_disco_info(Host, Node, From); ?NS_PUBSUB_GET_PENDING -> command_disco_info(Host, Node, From); _ -> node_disco_info(Host, Node, From) end. -spec iq_disco_items(host(), binary(), jid(), undefined | rsm_set()) -> {result, disco_items()} | {error, stanza_error()}. iq_disco_items(Host, <<>>, _From, _RSM) -> MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), case tree_action(Host, get_subnodes, [Host, <<>>, MaxNodes]) of {error, #stanza_error{}} = Err -> Err; Nodes when is_list(Nodes) -> Items = lists:map( fun(#pubsub_node{nodeid = {_, SubNode}, options = Options}) -> case get_option(Options, title) of false -> #disco_item{jid = jid:make(Host), node = SubNode}; Title -> #disco_item{jid = jid:make(Host), name = Title, node = SubNode} end end, Nodes), {result, #disco_items{items = Items}} end; iq_disco_items(Host, ?NS_COMMANDS, _From, _RSM) -> {result, #disco_items{items = [#disco_item{jid = jid:make(Host), node = ?NS_PUBSUB_GET_PENDING, name = ?T("Get Pending")}]}}; iq_disco_items(_Host, ?NS_PUBSUB_GET_PENDING, _From, _RSM) -> {result, #disco_items{}}; iq_disco_items(Host, Item, From, RSM) -> case str:tokens(Item, <<"!">>) of [_Node, _ItemId] -> {result, #disco_items{}}; [Node] -> MaxNodes = mod_pubsub_opt:max_nodes_discoitems(serverhost(Host)), Action = fun(#pubsub_node{id = Nidx, type = Type, options = Options, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), {NodeItems, RsmOut} = case get_allowed_items_call( Host, Nidx, From, Type, Options, Owners, RSM) of {result, R} -> R; _ -> {[], undefined} end, case tree_call(Host, get_subnodes, [Host, Node, MaxNodes]) of SubNodes when is_list(SubNodes) -> Nodes = lists:map( fun(#pubsub_node{nodeid = {_, SubNode}, options = SubOptions}) -> case get_option(SubOptions, title) of false -> #disco_item{jid = jid:make(Host), node = SubNode}; Title -> #disco_item{jid = jid:make(Host), name = Title, node = SubNode} end end, SubNodes), Items = lists:flatmap( fun(#pubsub_item{itemid = {RN, _}}) -> case node_call(Host, Type, get_item_name, [Host, Node, RN]) of {result, Name} -> [#disco_item{jid = jid:make(Host), name = Name}]; _ -> [] end end, NodeItems), {result, #disco_items{items = Nodes ++ Items, rsm = RsmOut}}; Error -> Error end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; Other -> Other end end. -spec iq_sm(iq()) -> iq(). iq_sm(#iq{to = To, sub_els = [SubEl]} = IQ) -> LOwner = jid:tolower(jid:remove_resource(To)), Res = case xmpp:get_ns(SubEl) of ?NS_PUBSUB -> iq_pubsub(LOwner, all, IQ); ?NS_PUBSUB_OWNER -> iq_pubsub_owner(LOwner, IQ) end, case Res of {result, IQRes} -> xmpp:make_iq_result(IQ, IQRes); {error, Error} -> xmpp:make_error(IQ, Error) end. -spec iq_get_vcard(binary(), binary()) -> vcard_temp(). iq_get_vcard(ServerHost, Lang) -> case mod_pubsub_opt:vcard(ServerHost) of undefined -> Desc = misc:get_descr(Lang, ?T("ejabberd Publish-Subscribe module")), #vcard_temp{fn = <<"ejabberd/mod_pubsub">>, url = ejabberd_config:get_uri(), desc = Desc}; VCard -> VCard end. -spec iq_pubsub(binary() | ljid(), atom(), iq()) -> {result, pubsub()} | {error, stanza_error()}. iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang, sub_els = [SubEl]}) -> case {IQType, SubEl} of {set, #pubsub{create = Node, configure = Configure, _ = undefined}} when is_binary(Node) -> ServerHost = serverhost(Host), Plugins = config(ServerHost, plugins), Config = case Configure of {_, XData} -> decode_node_config(XData, Host, Lang); undefined -> [] end, Type = hd(Plugins), case Config of {error, _} = Err -> Err; _ -> create_node(Host, ServerHost, Node, From, Type, Access, Config) end; {set, #pubsub{publish = #ps_publish{node = Node, items = Items}, publish_options = XData, configure = _, _ = undefined}} -> ServerHost = serverhost(Host), case Items of [#ps_item{id = ItemId, sub_els = Payload}] -> case decode_publish_options(XData, Lang) of {error, _} = Err -> Err; PubOpts -> publish_item(Host, ServerHost, Node, From, ItemId, Payload, PubOpts, Access) end; [] -> publish_item(Host, ServerHost, Node, From, <<>>, [], [], Access); _ -> {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} end; {set, #pubsub{retract = #ps_retract{node = Node, notify = Notify, items = Items}, _ = undefined}} -> case Items of [#ps_item{id = ItemId}] -> if ItemId /= <<>> -> delete_item(Host, Node, From, ItemId, Notify); true -> {error, extended_error(xmpp:err_bad_request(), err_item_required())} end; [] -> {error, extended_error(xmpp:err_bad_request(), err_item_required())}; _ -> {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())} end; {set, #pubsub{subscribe = #ps_subscribe{node = Node, jid = JID}, options = Options, _ = undefined}} -> Config = case Options of #ps_options{xdata = XData, jid = undefined, node = <<>>} -> decode_subscribe_options(XData, Lang); #ps_options{xdata = _XData, jid = #jid{}} -> Txt = ?T("Attribute 'jid' is not allowed here"), {error, xmpp:err_bad_request(Txt, Lang)}; #ps_options{xdata = _XData} -> Txt = ?T("Attribute 'node' is not allowed here"), {error, xmpp:err_bad_request(Txt, Lang)}; _ -> [] end, case Config of {error, _} = Err -> Err; _ -> subscribe_node(Host, Node, From, JID, Config) end; {set, #pubsub{unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId}, _ = undefined}} -> unsubscribe_node(Host, Node, From, JID, SubId); {get, #pubsub{items = #ps_items{node = Node, max_items = MaxItems, subid = SubId, items = Items}, rsm = RSM, _ = undefined}} -> ItemIds = [ItemId || #ps_item{id = ItemId} <- Items, ItemId /= <<>>], get_items(Host, Node, From, SubId, MaxItems, ItemIds, RSM); {get, #pubsub{subscriptions = {Node, _}, _ = undefined}} -> Plugins = config(serverhost(Host), plugins), get_subscriptions(Host, Node, From, Plugins); {get, #pubsub{affiliations = {Node, _}, _ = undefined}} -> Plugins = config(serverhost(Host), plugins), get_affiliations(Host, Node, From, Plugins); {_, #pubsub{options = #ps_options{jid = undefined}, _ = undefined}} -> {error, extended_error(xmpp:err_bad_request(), err_jid_required())}; {_, #pubsub{options = #ps_options{node = <<>>}, _ = undefined}} -> {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; {get, #pubsub{options = #ps_options{node = Node, subid = SubId, jid = JID}, _ = undefined}} -> get_options(Host, Node, JID, SubId, Lang); {set, #pubsub{options = #ps_options{node = Node, subid = SubId, jid = JID, xdata = XData}, _ = undefined}} -> case decode_subscribe_options(XData, Lang) of {error, _} = Err -> Err; Config -> set_options(Host, Node, JID, SubId, Config) end; {set, #pubsub{}} -> {error, xmpp:err_bad_request()}; _ -> {error, xmpp:err_feature_not_implemented()} end. -spec iq_pubsub_owner(binary() | ljid(), iq()) -> {result, pubsub_owner() | undefined} | {error, stanza_error()}. iq_pubsub_owner(Host, #iq{type = IQType, from = From, lang = Lang, sub_els = [SubEl]}) -> case {IQType, SubEl} of {get, #pubsub_owner{configure = {Node, undefined}, _ = undefined}} -> ServerHost = serverhost(Host), get_configure(Host, ServerHost, Node, From, Lang); {set, #pubsub_owner{configure = {Node, XData}, _ = undefined}} -> case XData of undefined -> {error, xmpp:err_bad_request(?T("No data form found"), Lang)}; #xdata{type = cancel} -> {result, #pubsub_owner{}}; #xdata{type = submit} -> case decode_node_config(XData, Host, Lang) of {error, _} = Err -> Err; Config -> set_configure(Host, Node, From, Config, Lang) end; #xdata{} -> {error, xmpp:err_bad_request(?T("Incorrect data form"), Lang)} end; {get, #pubsub_owner{default = {Node, undefined}, _ = undefined}} -> get_default(Host, Node, From, Lang); {set, #pubsub_owner{delete = {Node, _}, _ = undefined}} -> delete_node(Host, Node, From); {set, #pubsub_owner{purge = Node, _ = undefined}} when Node /= undefined -> purge_node(Host, Node, From); {get, #pubsub_owner{subscriptions = {Node, []}, _ = undefined}} -> get_subscriptions(Host, Node, From); {set, #pubsub_owner{subscriptions = {Node, Subs}, _ = undefined}} -> set_subscriptions(Host, Node, From, Subs); {get, #pubsub_owner{affiliations = {Node, []}, _ = undefined}} -> get_affiliations(Host, Node, From); {set, #pubsub_owner{affiliations = {Node, Affs}, _ = undefined}} -> set_affiliations(Host, Node, From, Affs); {_, #pubsub_owner{}} -> {error, xmpp:err_bad_request()}; _ -> {error, xmpp:err_feature_not_implemented()} end. -spec adhoc_request(binary(), binary(), jid(), adhoc_command(), atom(), [binary()]) -> adhoc_command() | {error, stanza_error()}. adhoc_request(Host, _ServerHost, Owner, #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, action = execute, xdata = undefined}, _Access, Plugins) -> send_pending_node_form(Host, Owner, Lang, Plugins); adhoc_request(Host, _ServerHost, Owner, #adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang, action = execute, xdata = #xdata{} = XData} = Request, _Access, _Plugins) -> case decode_get_pending(XData, Lang) of {error, _} = Err -> Err; Config -> Node = proplists:get_value(node, Config), case send_pending_auth_events(Host, Node, Owner, Lang) of ok -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = completed}); Err -> Err end end; adhoc_request(_Host, _ServerHost, _Owner, #adhoc_command{action = cancel}, _Access, _Plugins) -> #adhoc_command{status = canceled}; adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), {error, xmpp:err_item_not_found()}. -spec send_pending_node_form(binary(), jid(), binary(), [binary()]) -> adhoc_command() | {error, stanza_error()}. send_pending_node_form(Host, Owner, Lang, Plugins) -> Filter = fun (Type) -> lists:member(<<"get-pending">>, plugin_features(Host, Type)) end, case lists:filter(Filter, Plugins) of [] -> Err = extended_error(xmpp:err_feature_not_implemented(), err_unsupported('get-pending')), {error, Err}; Ps -> case get_pending_nodes(Host, Owner, Ps) of {ok, Nodes} -> Form = [{node, <<>>, lists:zip(Nodes, Nodes)}], XForm = #xdata{type = form, fields = pubsub_get_pending:encode(Form, Lang)}, #adhoc_command{status = executing, action = execute, xdata = XForm}; Err -> Err end end. -spec get_pending_nodes(binary(), jid(), [binary()]) -> {ok, [binary()]} | {error, stanza_error()}. get_pending_nodes(Host, Owner, Plugins) -> Tr = fun (Type) -> case node_call(Host, Type, get_pending_nodes, [Host, Owner]) of {result, Nodes} -> Nodes; _ -> [] end end, Action = fun() -> {result, lists:flatmap(Tr, Plugins)} end, case transaction(Host, Action, sync_dirty) of {result, Res} -> {ok, Res}; Err -> Err end. %% @doc

Send a subscription approval form to Owner for all pending %% subscriptions on Host and Node.

-spec send_pending_auth_events(binary(), binary(), jid(), binary()) -> ok | {error, stanza_error()}. send_pending_auth_events(Host, Node, Owner, Lang) -> ?DEBUG("Sending pending auth events for ~ts on ~ts:~ts", [jid:encode(Owner), Host, Node]), Action = fun(#pubsub_node{id = Nidx, type = Type}) -> case lists:member(<<"get-pending">>, plugin_features(Host, Type)) of true -> case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of {result, owner} -> node_call(Host, Type, get_node_subscriptions, [Nidx]); _ -> {error, xmpp:err_forbidden( ?T("Owner privileges required"), Lang)} end; false -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('get-pending'))} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {N, Subs}} -> lists:foreach( fun({J, pending, _SubId}) -> send_authorization_request(N, jid:make(J)); ({J, pending}) -> send_authorization_request(N, jid:make(J)); (_) -> ok end, Subs); Err -> Err end. %%% authorization handling -spec send_authorization_request(#pubsub_node{}, jid()) -> ok. send_authorization_request(#pubsub_node{nodeid = {Host, Node}, type = Type, id = Nidx, owners = O}, Subscriber) -> %% TODO: pass lang to this function Lang = <<"en">>, Fs = pubsub_subscribe_authorization:encode( [{node, Node}, {subscriber_jid, Subscriber}, {allow, false}], Lang), X = #xdata{type = form, title = translate:translate( Lang, ?T("PubSub subscriber request")), instructions = [translate:translate( Lang, ?T("Choose whether to approve this entity's " "subscription."))], fields = Fs}, Stanza = #message{from = service_jid(Host), sub_els = [X]}, lists:foreach( fun (Owner) -> ejabberd_router:route(xmpp:set_to(Stanza, jid:make(Owner))) end, node_owners_action(Host, Type, Nidx, O)). -spec find_authorization_response(message()) -> undefined | pubsub_subscribe_authorization:result() | {error, stanza_error()}. find_authorization_response(Packet) -> case xmpp:get_subtag(Packet, #xdata{type = form}) of #xdata{type = cancel} -> undefined; #xdata{type = submit, fields = Fs} -> try pubsub_subscribe_authorization:decode(Fs) of Result -> Result catch _:{pubsub_subscribe_authorization, Why} -> Lang = xmpp:get_lang(Packet), Txt = pubsub_subscribe_authorization:format_error(Why), {error, xmpp:err_bad_request(Txt, Lang)} end; #xdata{} -> {error, xmpp:err_bad_request()}; false -> undefined end. %% @doc Send a message to JID with the supplied Subscription -spec send_authorization_approval(binary(), jid(), binary(), subscribed | none) -> ok. send_authorization_approval(Host, JID, SNode, Subscription) -> Event = #ps_event{subscription = #ps_subscription{jid = JID, node = SNode, type = Subscription}}, Stanza = #message{from = service_jid(Host), to = JID, sub_els = [Event]}, ejabberd_router:route(Stanza). -spec handle_authorization_response(binary(), message(), pubsub_subscribe_authorization:result()) -> ok. handle_authorization_response(Host, #message{from = From} = Packet, Response) -> Node = proplists:get_value(node, Response), Subscriber = proplists:get_value(subscriber_jid, Response), Allow = proplists:get_value(allow, Response), Lang = xmpp:get_lang(Packet), FromLJID = jid:tolower(jid:remove_resource(From)), Action = fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), case lists:member(FromLJID, Owners) of true -> case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of {result, Subs} -> update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs); {error, _} = Err -> Err end; false -> {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)} end end, case transaction(Host, Node, Action, sync_dirty) of {error, Error} -> ejabberd_router:route_error(Packet, Error); {result, {_, _NewSubscription}} -> %% XXX: notify about subscription state change, section 12.11 ok end. -spec update_auth(binary(), binary(), _, _, jid() | error, boolean(), _) -> {result, ok} | {error, stanza_error()}. update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) -> Sub= lists:filter(fun ({pending, _}) -> true; (_) -> false end, Subs), case Sub of [{pending, SubId}|_] -> NewSub = case Allow of true -> subscribed; false -> none end, node_call(Host, Type, set_subscriptions, [Nidx, Subscriber, NewSub, SubId]), send_authorization_approval(Host, Subscriber, Node, NewSub), {result, ok}; _ -> Txt = ?T("No pending subscriptions found"), {error, xmpp:err_unexpected_request(Txt, ejabberd_option:language())} end. %% @doc

Create new pubsub nodes

%%

In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:

%%
    %%
  • The service does not support node creation.
  • %%
  • Only entities that are registered with the service are allowed to create nodes but the requesting entity is not registered.
  • %%
  • The requesting entity does not have sufficient privileges to create nodes.
  • %%
  • The requested Node already exists.
  • %%
  • The request did not include a Node and "instant nodes" are not supported.
  • %%
%%

ote: node creation is a particular case, error return code is evaluated at many places:

%%
    %%
  • iq_pubsub checks if service supports node creation (type exists)
  • %%
  • create_node checks if instant nodes are supported
  • %%
  • create_node asks node plugin if entity have sufficient privilege
  • %%
  • nodetree create_node checks if nodeid already exists
  • %%
  • node plugin create_node just sets default affiliation/subscription
  • %%
-spec create_node(host(), binary(), binary(), jid(), binary()) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, Node, Owner, Type) -> create_node(Host, ServerHost, Node, Owner, Type, all, []). -spec create_node(host(), binary(), binary(), jid(), binary(), atom(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. create_node(Host, ServerHost, <<>>, Owner, Type, Access, Configuration) -> case lists:member(<<"instant-nodes">>, plugin_features(Host, Type)) of true -> Node = p1_rand:get_string(), case create_node(Host, ServerHost, Node, Owner, Type, Access, Configuration) of {result, _} -> {result, #pubsub{create = Node}}; Error -> Error end; false -> {error, extended_error(xmpp:err_not_acceptable(), err_nodeid_required())} end; create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> Type = select_type(ServerHost, Host, Node, GivenType), NodeOptions = merge_config( [node_config(Node, ServerHost), Configuration, node_options(Host, Type)]), CreateNode = fun() -> Parent = case node_call(Host, Type, node_to_path, [Node]) of {result, [Node]} -> <<>>; {result, Path} -> element(2, node_call(Host, Type, path_to_node, [lists:sublist(Path, length(Path)-1)])) end, Parents = case Parent of <<>> -> []; _ -> [Parent] end, case node_call(Host, Type, create_node_permission, [Host, ServerHost, Node, Parent, Owner, Access]) of {result, true} -> case tree_call(Host, create_node, [Host, Node, Type, Owner, NodeOptions, Parents]) of {ok, Nidx} -> case get_node_subs_by_depth(Host, Node, Owner) of {result, SubsByDepth} -> case node_call(Host, Type, create_node, [Nidx, Owner]) of {result, Result} -> {result, {Nidx, SubsByDepth, Result}}; Error -> Error end; Error -> Error end; {error, {virtual, Nidx}} -> case node_call(Host, Type, create_node, [Nidx, Owner]) of {result, Result} -> {result, {Nidx, [], Result}}; Error -> Error end; Error -> Error end; {result, _} -> Txt = ?T("You're not allowed to create nodes"), {error, xmpp:err_forbidden(Txt, ejabberd_option:language())}; Err -> Err end end, Reply = #pubsub{create = Node}, case transaction(Host, CreateNode, transaction) of {result, {Nidx, SubsByDepth, {Result, broadcast}}} -> broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth), ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, Nidx, NodeOptions]), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {Nidx, _SubsByDepth, Result}} -> ejabberd_hooks:run(pubsub_create_node, ServerHost, [ServerHost, Host, Node, Nidx, NodeOptions]), case Result of default -> {result, Reply}; _ -> {result, Result} end; Error -> %% in case we change transaction to sync_dirty... %% node_call(Host, Type, delete_node, [Host, Node]), %% tree_call(Host, delete_node, [Host, Node]), Error end. %% @doc

Delete specified node and all children.

%%

There are several reasons why the node deletion request might fail:

%%
    %%
  • The requesting entity does not have sufficient privileges to delete the node.
  • %%
  • The node is the root collection node, which cannot be deleted.
  • %%
  • The specified node does not exist.
  • %%
-spec delete_node(host(), binary(), jid()) -> {result, pubsub_owner()} | {error, stanza_error()}. delete_node(_Host, <<>>, _Owner) -> {error, xmpp:err_not_allowed(?T("No node specified"), ejabberd_option:language())}; delete_node(Host, Node, Owner) -> Action = fun(#pubsub_node{type = Type, id = Nidx}) -> case node_call(Host, Type, get_affiliation, [Nidx, Owner]) of {result, owner} -> case get_node_subs_by_depth(Host, Node, service_jid(Host)) of {result, SubsByDepth} -> case tree_call(Host, delete_node, [Host, Node]) of Removed when is_list(Removed) -> case node_call(Host, Type, delete_node, [Removed]) of {result, Res} -> {result, {SubsByDepth, Res}}; Error -> Error end; Error -> Error end; Error -> Error end; {result, _} -> Lang = ejabberd_option:language(), {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; Error -> Error end end, Reply = undefined, ServerHost = serverhost(Host), case transaction(Host, Node, Action, transaction) of {result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} -> lists:foreach(fun ({RNode, _RSubs}) -> {RH, RN} = RNode#pubsub_node.nodeid, RNidx = RNode#pubsub_node.id, RType = RNode#pubsub_node.type, ROptions = RNode#pubsub_node.options, unset_cached_item(RH, RNidx), broadcast_removed_node(RH, RN, RNidx, RType, ROptions, SubsByDepth), ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, RNidx]) end, Removed), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {_, {_, {Result, Removed}}}} -> lists:foreach(fun ({RNode, _RSubs}) -> {RH, RN} = RNode#pubsub_node.nodeid, RNidx = RNode#pubsub_node.id, unset_cached_item(RH, RNidx), ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, RH, RN, RNidx]) end, Removed), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {TNode, {_, Result}}} -> Nidx = TNode#pubsub_node.id, unset_cached_item(Host, Nidx), ejabberd_hooks:run(pubsub_delete_node, ServerHost, [ServerHost, Host, Node, Nidx]), case Result of default -> {result, Reply}; _ -> {result, Result} end; Error -> Error end. %% @see node_hometree:subscribe_node/5 %% @doc

Accepts or rejects subcription requests on a PubSub node.

%%

There are several reasons why the subscription request might fail:

%%
    %%
  • The bare JID portions of the JIDs do not match.
  • %%
  • The node has an access model of "presence" and the requesting entity is not subscribed to the owner's presence.
  • %%
  • The node has an access model of "roster" and the requesting entity is not in one of the authorized roster groups.
  • %%
  • The node has an access model of "whitelist" and the requesting entity is not on the whitelist.
  • %%
  • The service requires payment for subscriptions to the node.
  • %%
  • The requesting entity is anonymous and the service does not allow anonymous entities to subscribe.
  • %%
  • The requesting entity has a pending subscription.
  • %%
  • The requesting entity is blocked from subscribing (e.g., because having an affiliation of outcast).
  • %%
  • The node does not support subscriptions.
  • %%
  • The node does not exist.
  • %%
-spec subscribe_node(host(), binary(), jid(), jid(), [{binary(), [binary()]}]) -> {result, pubsub()} | {error, stanza_error()}. subscribe_node(Host, Node, From, JID, Configuration) -> SubModule = subscription_plugin(Host), SubOpts = case SubModule:parse_options_xform(Configuration) of {result, GoodSubOpts} -> GoodSubOpts; _ -> invalid end, Subscriber = jid:tolower(JID), Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> Features = plugin_features(Host, Type), SubscribeFeature = lists:member(<<"subscribe">>, Features), OptionsFeature = lists:member(<<"subscription-options">>, Features), HasOptions = not (SubOpts == []), SubscribeConfig = get_option(Options, subscribe), AccessModel = get_option(Options, access_model), SendLast = get_option(Options, send_last_published_item), AllowedGroups = get_option(Options, roster_groups_allowed, []), CanSubscribe = case get_max_subscriptions_node(Host) of Max when is_integer(Max) -> case node_call(Host, Type, get_node_subscriptions, [Nidx]) of {result, NodeSubs} -> SubsNum = lists:foldl( fun ({_, subscribed, _}, Acc) -> Acc+1; (_, Acc) -> Acc end, 0, NodeSubs), SubsNum < Max; _ -> true end; _ -> true end, if not SubscribeFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('subscribe'))}; not SubscribeConfig -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('subscribe'))}; HasOptions andalso not OptionsFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('subscription-options'))}; SubOpts == invalid -> {error, extended_error(xmpp:err_bad_request(), err_invalid_options())}; not CanSubscribe -> %% fallback to closest XEP compatible result, assume we are not allowed to subscribe {error, extended_error(xmpp:err_not_allowed(), err_closed_node())}; true -> Owners = node_owners_call(Host, Type, Nidx, O), {PS, RG} = get_presence_and_roster_permissions(Host, JID, Owners, AccessModel, AllowedGroups), node_call(Host, Type, subscribe_node, [Nidx, From, Subscriber, AccessModel, SendLast, PS, RG, SubOpts]) end end, Reply = fun (Subscription) -> Sub = case Subscription of {subscribed, SubId} -> #ps_subscription{jid = JID, type = subscribed, subid = SubId}; Other -> #ps_subscription{jid = JID, type = Other} end, #pubsub{subscription = Sub#ps_subscription{node = Node}} end, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, subscribed, SubId, send_last}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, send_items(Host, Node, Nidx, Type, Options, Subscriber, last), ServerHost = serverhost(Host), ejabberd_hooks:run(pubsub_subscribe_node, ServerHost, [ServerHost, Host, Node, Subscriber, SubId]), case Result of default -> {result, Reply({subscribed, SubId})}; _ -> {result, Result} end; {result, {_TNode, {default, subscribed, SubId}}} -> {result, Reply({subscribed, SubId})}; {result, {_TNode, {Result, subscribed, _SubId}}} -> {result, Result}; {result, {TNode, {default, pending, _SubId}}} -> send_authorization_request(TNode, JID), {result, Reply(pending)}; {result, {TNode, {Result, pending}}} -> send_authorization_request(TNode, JID), {result, Result}; {result, {_, Result}} -> {result, Result}; Error -> Error end. %% @doc

Unsubscribe JID from the Node.

%%

There are several reasons why the unsubscribe request might fail:

%%
    %%
  • The requesting entity has multiple subscriptions to the node but does not specify a subscription ID.
  • %%
  • The request does not specify an existing subscriber.
  • %%
  • The requesting entity does not have sufficient privileges to unsubscribe the specified JID.
  • %%
  • The node does not exist.
  • %%
  • The request specifies a subscription ID that is not valid or current.
  • %%
-spec unsubscribe_node(host(), binary(), jid(), jid(), binary()) -> {result, undefined} | {error, stanza_error()}. unsubscribe_node(Host, Node, From, JID, SubId) -> Subscriber = jid:tolower(JID), Action = fun (#pubsub_node{type = Type, id = Nidx}) -> node_call(Host, Type, unsubscribe_node, [Nidx, From, Subscriber, SubId]) end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, default}} -> ServerHost = serverhost(Host), ejabberd_hooks:run(pubsub_unsubscribe_node, ServerHost, [ServerHost, Host, Node, Subscriber, SubId]), {result, undefined}; Error -> Error end. %% @doc

Publish item to a PubSub node.

%%

The permission to publish an item must be verified by the plugin implementation.

%%

There are several reasons why the publish request might fail:

%%
    %%
  • The requesting entity does not have sufficient privileges to publish.
  • %%
  • The node does not support item publication.
  • %%
  • The node does not exist.
  • %%
  • The payload size exceeds a service-defined limit.
  • %%
  • The item contains more than one payload element or the namespace of the root payload element does not match the configured namespace for the node.
  • %%
  • The request does not match the node configuration.
  • %%
-spec publish_item(host(), binary(), binary(), jid(), binary(), [xmlel()]) -> {result, pubsub()} | {error, stanza_error()}. publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, [], all). publish_item(Host, ServerHost, Node, Publisher, <<>>, Payload, PubOpts, Access) -> publish_item(Host, ServerHost, Node, Publisher, uniqid(), Payload, PubOpts, Access); publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload, PubOpts, Access) -> Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> Features = plugin_features(Host, Type), PublishFeature = lists:member(<<"publish">>, Features), PublishModel = get_option(Options, publish_model), DeliverPayloads = get_option(Options, deliver_payloads), PersistItems = get_option(Options, persist_items), MaxItems = max_items(Host, Options), PayloadCount = payload_xmlelements(Payload), PayloadSize = byte_size(term_to_binary(Payload)) - 2, PayloadMaxSize = get_option(Options, max_payload_size), PreconditionsMet = preconditions_met(PubOpts, Options), if not PublishFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported(publish))}; not PreconditionsMet -> {error, extended_error(xmpp:err_conflict(), err_precondition_not_met())}; PayloadSize > PayloadMaxSize -> {error, extended_error(xmpp:err_not_acceptable(), err_payload_too_big())}; (DeliverPayloads or PersistItems) and (PayloadCount == 0) -> {error, extended_error(xmpp:err_bad_request(), err_item_required())}; (DeliverPayloads or PersistItems) and (PayloadCount > 1) -> {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())}; (not (DeliverPayloads or PersistItems)) and (PayloadCount > 0) -> {error, extended_error(xmpp:err_bad_request(), err_item_forbidden())}; true -> node_call(Host, Type, publish_item, [Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, PubOpts]) end end, Reply = #pubsub{publish = #ps_publish{node = Node, items = [#ps_item{id = ItemId}]}}, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, Broadcast, Removed}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, BrPayload = case Broadcast of broadcast -> Payload; PluginPayload -> PluginPayload end, set_cached_item(Host, Nidx, ItemId, Publisher, BrPayload), case get_option(Options, deliver_notifications) of true -> broadcast_publish_item(Host, Node, Nidx, Type, Options, ItemId, Publisher, BrPayload, Removed); false -> ok end, ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, BrPayload]), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {TNode, {default, Removed}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed), set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Reply}; {result, {TNode, {Result, Removed}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, broadcast_retract_items(Host, Node, Nidx, Type, Options, Removed), set_cached_item(Host, Nidx, ItemId, Publisher, Payload), {result, Result}; {result, {_, default}} -> {result, Reply}; {result, {_, Result}} -> {result, Result}; {error, #stanza_error{reason = 'item-not-found'}} -> Type = select_type(ServerHost, Host, Node), case lists:member(<<"auto-create">>, plugin_features(Host, Type)) of true -> case create_node(Host, ServerHost, Node, Publisher, Type, Access, PubOpts) of {result, #pubsub{create = NewNode}} -> publish_item(Host, ServerHost, NewNode, Publisher, ItemId, Payload, PubOpts, Access); _ -> {error, xmpp:err_item_not_found()} end; false -> Txt = ?T("Automatic node creation is not enabled"), {error, xmpp:err_item_not_found(Txt, ejabberd_option:language())} end; Error -> Error end. %% @doc

Delete item from a PubSub node.

%%

The permission to delete an item must be verified by the plugin implementation.

%%

There are several reasons why the item retraction request might fail:

%%
    %%
  • The publisher does not have sufficient privileges to delete the requested item.
  • %%
  • The node or item does not exist.
  • %%
  • The request does not specify a node.
  • %%
  • The request does not include an element or the element does not specify an ItemId.
  • %%
  • The node does not support persistent items.
  • %%
  • The service does not support the deletion of items.
  • %%
-spec delete_item(host(), binary(), jid(), binary()) -> {result, undefined} | {error, stanza_error()}. delete_item(Host, Node, Publisher, ItemId) -> delete_item(Host, Node, Publisher, ItemId, false). delete_item(_, <<>>, _, _, _) -> {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; delete_item(Host, Node, Publisher, ItemId, ForceNotify) -> Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> Features = plugin_features(Host, Type), PersistentFeature = lists:member(<<"persistent-items">>, Features), DeleteFeature = lists:member(<<"delete-items">>, Features), PublishModel = get_option(Options, publish_model), if %%-> iq_pubsub just does that matches %% %% Request does not specify an item %% {error, extended_error(?ERR_BAD_REQUEST, "item-required")}; not PersistentFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('persistent-items'))}; not DeleteFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('delete-items'))}; true -> node_call(Host, Type, delete_item, [Nidx, Publisher, PublishModel, ItemId]) end end, Reply = undefined, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, broadcast}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, broadcast_retract_items(Host, Node, Nidx, Type, Options, [ItemId], ForceNotify), case get_cached_item(Host, Nidx) of #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); _ -> ok end, case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {_, default}} -> {result, Reply}; {result, {_, Result}} -> {result, Result}; Error -> Error end. %% @doc

Delete all items of specified node owned by JID.

%%

There are several reasons why the node purge request might fail:

%%
    %%
  • The node or service does not support node purging.
  • %%
  • The requesting entity does not have sufficient privileges to purge the node.
  • %%
  • The node is not configured to persist items.
  • %%
  • The specified node does not exist.
  • %%
-spec purge_node(mod_pubsub:host(), binary(), jid()) -> {result, undefined} | {error, stanza_error()}. purge_node(Host, Node, Owner) -> Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> Features = plugin_features(Host, Type), PurgeFeature = lists:member(<<"purge-nodes">>, Features), PersistentFeature = lists:member(<<"persistent-items">>, Features), PersistentConfig = get_option(Options, persist_items), if not PurgeFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('purge-nodes'))}; not PersistentFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('persistent-items'))}; not PersistentConfig -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('persistent-items'))}; true -> node_call(Host, Type, purge_node, [Nidx, Owner]) end end, Reply = undefined, case transaction(Host, Node, Action, transaction) of {result, {TNode, {Result, broadcast}}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, Options = TNode#pubsub_node.options, broadcast_purge_node(Host, Node, Nidx, Type, Options), unset_cached_item(Host, Nidx), case Result of default -> {result, Reply}; _ -> {result, Result} end; {result, {_, default}} -> {result, Reply}; {result, {_, Result}} -> {result, Result}; Error -> Error end. %% @doc

Return the items of a given node.

%%

The number of items to return is limited by MaxItems.

%%

The permission are not checked in this function.

-spec get_items(host(), binary(), jid(), binary(), undefined | non_neg_integer(), [binary()], undefined | rsm_set()) -> {result, pubsub()} | {error, stanza_error()}. get_items(Host, Node, From, SubId, MaxItems, ItemIds, undefined) when MaxItems =/= undefined -> get_items(Host, Node, From, SubId, MaxItems, ItemIds, #rsm_set{max = MaxItems, before = <<>>}); get_items(Host, Node, From, SubId, _MaxItems, ItemIds, RSM) -> Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx, owners = O}) -> Features = plugin_features(Host, Type), RetreiveFeature = lists:member(<<"retrieve-items">>, Features), PersistentFeature = lists:member(<<"persistent-items">>, Features), AccessModel = get_option(Options, access_model), AllowedGroups = get_option(Options, roster_groups_allowed, []), if not RetreiveFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('retrieve-items'))}; not PersistentFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('persistent-items'))}; true -> Owners = node_owners_call(Host, Type, Nidx, O), {PS, RG} = get_presence_and_roster_permissions( Host, From, Owners, AccessModel, AllowedGroups), case ItemIds of [ItemId] -> NotFound = xmpp:err_item_not_found(), case node_call(Host, Type, get_item, [Nidx, ItemId, From, AccessModel, PS, RG, undefined]) of {error, NotFound} -> {result, {[], undefined}}; Result -> Result end; _ -> node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, SubId, RSM]) end end end, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Items, RsmOut}}} -> SendItems = case ItemIds of [] -> Items; _ -> lists:filter( fun(#pubsub_item{itemid = {ItemId, _}}) -> lists:member(ItemId, ItemIds) end, Items) end, Options = TNode#pubsub_node.options, {result, #pubsub{items = items_els(Node, Options, SendItems), rsm = RsmOut}}; {result, {TNode, Item}} -> Options = TNode#pubsub_node.options, {result, #pubsub{items = items_els(Node, Options, [Item])}}; Error -> Error end. %% Seems like this function broken get_items(Host, Node) -> Action = fun (#pubsub_node{type = Type, id = Nidx}) -> node_call(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, {Items, _}}} -> Items; Error -> Error end. %% This function is broken too? get_item(Host, Node, ItemId) -> Action = fun (#pubsub_node{type = Type, id = Nidx}) -> node_call(Host, Type, get_item, [Nidx, ItemId]) end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Items}} -> Items; Error -> Error end. -spec get_allowed_items_call(host(), nodeIdx(), jid(), binary(), nodeOptions(), [ljid()]) -> {result, [#pubsub_item{}]} | {error, stanza_error()}. get_allowed_items_call(Host, Nidx, From, Type, Options, Owners) -> case get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, undefined) of {result, {Items, _RSM}} -> {result, Items}; Error -> Error end. -spec get_allowed_items_call(host(), nodeIdx(), jid(), binary(), nodeOptions(), [ljid()], undefined | rsm_set()) -> {result, {[#pubsub_item{}], undefined | rsm_set()}} | {error, stanza_error()}. get_allowed_items_call(Host, Nidx, From, Type, Options, Owners, RSM) -> AccessModel = get_option(Options, access_model), AllowedGroups = get_option(Options, roster_groups_allowed, []), {PS, RG} = get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups), node_call(Host, Type, get_items, [Nidx, From, AccessModel, PS, RG, undefined, RSM]). -spec get_last_items(host(), binary(), nodeIdx(), ljid(), last | integer()) -> [#pubsub_item{}]. get_last_items(Host, Type, Nidx, LJID, last) -> % hack to handle section 6.1.7 of XEP-0060 get_last_items(Host, Type, Nidx, LJID, 1); get_last_items(Host, Type, Nidx, LJID, 1) -> case get_cached_item(Host, Nidx) of undefined -> case node_action(Host, Type, get_last_items, [Nidx, LJID, 1]) of {result, Items} -> Items; _ -> [] end; LastItem -> [LastItem] end; get_last_items(Host, Type, Nidx, LJID, Count) when Count > 1 -> case node_action(Host, Type, get_last_items, [Nidx, LJID, Count]) of {result, Items} -> Items; _ -> [] end; get_last_items(_Host, _Type, _Nidx, _LJID, _Count) -> []. -spec get_only_item(host(), binary(), nodeIdx(), ljid()) -> [#pubsub_item{}]. get_only_item(Host, Type, Nidx, LJID) -> case get_cached_item(Host, Nidx) of undefined -> case node_action(Host, Type, get_only_item, [Nidx, LJID]) of {result, Items} when length(Items) < 2 -> Items; {result, Items} -> [hd(lists:keysort(#pubsub_item.modification, Items))]; _ -> [] end; LastItem -> [LastItem] end. %% @doc

Return the list of affiliations as an XMPP response.

-spec get_affiliations(host(), binary(), jid(), [binary()]) -> {result, pubsub()} | {error, stanza_error()}. get_affiliations(Host, Node, JID, Plugins) when is_list(Plugins) -> Result = lists:foldl( fun(Type, {Status, Acc}) -> Features = plugin_features(Host, Type), RetrieveFeature = lists:member(<<"retrieve-affiliations">>, Features), if not RetrieveFeature -> {{error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('retrieve-affiliations'))}, Acc}; true -> case node_action(Host, Type, get_entity_affiliations, [Host, JID]) of {result, Affs} -> {Status, [Affs | Acc]}; {error, _} = Err -> {Err, Acc} end end end, {ok, []}, Plugins), case Result of {ok, Affs} -> Entities = lists:flatmap( fun({_, none}) -> []; ({#pubsub_node{nodeid = {_, NodeId}}, Aff}) -> if (Node == <<>>) or (Node == NodeId) -> [#ps_affiliation{node = NodeId, type = Aff}]; true -> [] end; (_) -> [] end, lists:usort(lists:flatten(Affs))), {result, #pubsub{affiliations = {<<>>, Entities}}}; {Error, _} -> Error end. -spec get_affiliations(host(), binary(), jid()) -> {result, pubsub_owner()} | {error, stanza_error()}. get_affiliations(Host, Node, JID) -> Action = fun(#pubsub_node{type = Type, id = Nidx}) -> Features = plugin_features(Host, Type), RetrieveFeature = lists:member(<<"modify-affiliations">>, Features), {result, Affiliation} = node_call(Host, Type, get_affiliation, [Nidx, JID]), if not RetrieveFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('modify-affiliations'))}; Affiliation /= owner -> {error, xmpp:err_forbidden(?T("Owner privileges required"), ejabberd_option:language())}; true -> node_call(Host, Type, get_node_affiliations, [Nidx]) end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, []}} -> {error, xmpp:err_item_not_found()}; {result, {_, Affs}} -> Entities = lists:flatmap( fun({_, none}) -> []; ({AJID, Aff}) -> [#ps_affiliation{jid = AJID, type = Aff}] end, Affs), {result, #pubsub_owner{affiliations = {Node, Entities}}}; Error -> Error end. -spec set_affiliations(host(), binary(), jid(), [ps_affiliation()]) -> {result, undefined} | {error, stanza_error()}. set_affiliations(Host, Node, From, Affs) -> Owner = jid:tolower(jid:remove_resource(From)), Action = fun(#pubsub_node{type = Type, id = Nidx, owners = O} = N) -> Owners = node_owners_call(Host, Type, Nidx, O), case lists:member(Owner, Owners) of true -> OwnerJID = jid:make(Owner), FilteredAffs = case Owners of [Owner] -> [Aff || Aff <- Affs, Aff#ps_affiliation.jid /= OwnerJID]; _ -> Affs end, lists:foreach( fun(#ps_affiliation{jid = JID, type = Affiliation}) -> node_call(Host, Type, set_affiliation, [Nidx, JID, Affiliation]), case Affiliation of owner -> NewOwner = jid:tolower(jid:remove_resource(JID)), NewOwners = [NewOwner | Owners], tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); none -> OldOwner = jid:tolower(jid:remove_resource(JID)), case lists:member(OldOwner, Owners) of true -> NewOwners = Owners -- [OldOwner], tree_call(Host, set_node, [N#pubsub_node{owners = NewOwners}]); _ -> ok end; _ -> ok end end, FilteredAffs), {result, undefined}; _ -> {error, xmpp:err_forbidden( ?T("Owner privileges required"), ejabberd_option:language())} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; Other -> Other end. -spec get_options(binary(), binary(), jid(), binary(), binary()) -> {result, xdata()} | {error, stanza_error()}. get_options(Host, Node, JID, SubId, Lang) -> Action = fun (#pubsub_node{type = Type, id = Nidx}) -> case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of true -> get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type); false -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('subscription-options'))} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_Node, XForm}} -> {result, XForm}; Error -> Error end. -spec get_options_helper(binary(), jid(), binary(), binary(), _, binary(), binary()) -> {result, pubsub()} | {error, stanza_error()}. get_options_helper(Host, JID, Lang, Node, Nidx, SubId, Type) -> Subscriber = jid:tolower(JID), case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of {result, Subs} -> SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], case {SubId, SubIds} of {_, []} -> {error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())}; {<<>>, [SID]} -> read_sub(Host, Node, Nidx, Subscriber, SID, Lang); {<<>>, _} -> {error, extended_error(xmpp:err_not_acceptable(), err_subid_required())}; {_, _} -> ValidSubId = lists:member(SubId, SubIds), if ValidSubId -> read_sub(Host, Node, Nidx, Subscriber, SubId, Lang); true -> {error, extended_error(xmpp:err_not_acceptable(), err_invalid_subid())} end end; {error, _} = Error -> Error end. -spec read_sub(binary(), binary(), nodeIdx(), ljid(), binary(), binary()) -> {result, pubsub()}. read_sub(Host, Node, Nidx, Subscriber, SubId, Lang) -> SubModule = subscription_plugin(Host), XData = case SubModule:get_subscription(Subscriber, Nidx, SubId) of {error, notfound} -> undefined; {result, #pubsub_subscription{options = Options}} -> {result, X} = SubModule:get_options_xform(Lang, Options), X end, {result, #pubsub{options = #ps_options{jid = jid:make(Subscriber), subid = SubId, node = Node, xdata = XData}}}. -spec set_options(binary(), binary(), jid(), binary(), [{binary(), [binary()]}]) -> {result, undefined} | {error, stanza_error()}. set_options(Host, Node, JID, SubId, Configuration) -> Action = fun (#pubsub_node{type = Type, id = Nidx}) -> case lists:member(<<"subscription-options">>, plugin_features(Host, Type)) of true -> set_options_helper(Host, Configuration, JID, Nidx, SubId, Type); false -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('subscription-options'))} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_Node, Result}} -> {result, Result}; Error -> Error end. -spec set_options_helper(binary(), [{binary(), [binary()]}], jid(), nodeIdx(), binary(), binary()) -> {result, undefined} | {error, stanza_error()}. set_options_helper(Host, Configuration, JID, Nidx, SubId, Type) -> SubModule = subscription_plugin(Host), SubOpts = case SubModule:parse_options_xform(Configuration) of {result, GoodSubOpts} -> GoodSubOpts; _ -> invalid end, Subscriber = jid:tolower(JID), case node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]) of {result, Subs} -> SubIds = [Id || {Sub, Id} <- Subs, Sub == subscribed], case {SubId, SubIds} of {_, []} -> {error, extended_error(xmpp:err_not_acceptable(), err_not_subscribed())}; {<<>>, [SID]} -> write_sub(Host, Nidx, Subscriber, SID, SubOpts); {<<>>, _} -> {error, extended_error(xmpp:err_not_acceptable(), err_subid_required())}; {_, _} -> write_sub(Host, Nidx, Subscriber, SubId, SubOpts) end; {error, _} = Err -> Err end. -spec write_sub(binary(), nodeIdx(), ljid(), binary(), _) -> {result, undefined} | {error, stanza_error()}. write_sub(_Host, _Nidx, _Subscriber, _SubId, invalid) -> {error, extended_error(xmpp:err_bad_request(), err_invalid_options())}; write_sub(_Host, _Nidx, _Subscriber, _SubId, []) -> {result, undefined}; write_sub(Host, Nidx, Subscriber, SubId, Options) -> SubModule = subscription_plugin(Host), case SubModule:set_subscription(Subscriber, Nidx, SubId, Options) of {result, _} -> {result, undefined}; {error, _} -> {error, extended_error(xmpp:err_not_acceptable(), err_invalid_subid())} end. %% @doc

Return the list of subscriptions as an XMPP response.

-spec get_subscriptions(host(), binary(), jid(), [binary()]) -> {result, pubsub()} | {error, stanza_error()}. get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> Result = lists:foldl(fun (Type, {Status, Acc}) -> Features = plugin_features(Host, Type), RetrieveFeature = lists:member(<<"retrieve-subscriptions">>, Features), if not RetrieveFeature -> {{error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('retrieve-subscriptions'))}, Acc}; true -> Subscriber = jid:remove_resource(JID), case node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]) of {result, Subs} -> {Status, [Subs | Acc]}; {error, _} = Err -> {Err, Acc} end end end, {ok, []}, Plugins), case Result of {ok, Subs} -> Entities = lists:flatmap(fun ({#pubsub_node{nodeid = {_, SubsNode}}, Sub}) -> case Node of <<>> -> [#ps_subscription{jid = jid:remove_resource(JID), node = SubsNode, type = Sub}]; SubsNode -> [#ps_subscription{jid = jid:remove_resource(JID), type = Sub}]; _ -> [] end; ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubId, SubJID}) -> case Node of <<>> -> [#ps_subscription{jid = SubJID, subid = SubId, type = Sub, node = SubsNode}]; SubsNode -> [#ps_subscription{jid = SubJID, subid = SubId, type = Sub}]; _ -> [] end; ({#pubsub_node{nodeid = {_, SubsNode}}, Sub, SubJID}) -> case Node of <<>> -> [#ps_subscription{jid = SubJID, type = Sub, node = SubsNode}]; SubsNode -> [#ps_subscription{jid = SubJID, type = Sub}]; _ -> [] end end, lists:usort(lists:flatten(Subs))), {result, #pubsub{subscriptions = {<<>>, Entities}}}; {Error, _} -> Error end. -spec get_subscriptions(host(), binary(), jid()) -> {result, pubsub_owner()} | {error, stanza_error()}. get_subscriptions(Host, Node, JID) -> Action = fun(#pubsub_node{type = Type, id = Nidx}) -> Features = plugin_features(Host, Type), RetrieveFeature = lists:member(<<"manage-subscriptions">>, Features), case node_call(Host, Type, get_affiliation, [Nidx, JID]) of {result, Affiliation} -> if not RetrieveFeature -> {error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('manage-subscriptions'))}; Affiliation /= owner -> Lang = ejabberd_option:language(), {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; true -> node_call(Host, Type, get_node_subscriptions, [Nidx]) end; Error -> Error end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Subs}} -> Entities = lists:flatmap( fun({_, none}) -> []; ({_, pending, _}) -> []; ({AJID, Sub}) -> [#ps_subscription{jid = AJID, type = Sub}]; ({AJID, Sub, SubId}) -> [#ps_subscription{jid = AJID, type = Sub, subid = SubId}] end, Subs), {result, #pubsub_owner{subscriptions = {Node, Entities}}}; Error -> Error end. -spec get_subscriptions_for_send_last(host(), binary(), atom(), jid(), ljid(), ljid()) -> [{#pubsub_node{}, subId(), ljid()}]. get_subscriptions_for_send_last(Host, PType, sql, JID, LJID, BJID) -> case node_action(Host, PType, get_entity_subscriptions_for_send_last, [Host, JID]) of {result, Subs} -> [{Node, SubId, SubJID} || {Node, Sub, SubId, SubJID} <- Subs, Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID)]; _ -> [] end; %% sql version already filter result by on_sub_and_presence get_subscriptions_for_send_last(Host, PType, _, JID, LJID, BJID) -> case node_action(Host, PType, get_entity_subscriptions, [Host, JID]) of {result, Subs} -> [{Node, SubId, SubJID} || {Node, Sub, SubId, SubJID} <- Subs, Sub =:= subscribed, (SubJID == LJID) or (SubJID == BJID), match_option(Node, send_last_published_item, on_sub_and_presence)]; _ -> [] end. -spec set_subscriptions(host(), binary(), jid(), [ps_subscription()]) -> {result, undefined} | {error, stanza_error()}. set_subscriptions(Host, Node, From, Entities) -> Owner = jid:tolower(jid:remove_resource(From)), Notify = fun(#ps_subscription{jid = JID, type = Sub}) -> Stanza = #message{ from = service_jid(Host), to = JID, sub_els = [#ps_event{ subscription = #ps_subscription{ jid = JID, type = Sub, node = Node}}]}, ejabberd_router:route(Stanza) end, Action = fun(#pubsub_node{type = Type, id = Nidx, owners = O}) -> Owners = node_owners_call(Host, Type, Nidx, O), case lists:member(Owner, Owners) of true -> Result = lists:foldl( fun(_, {error, _} = Err) -> Err; (#ps_subscription{jid = JID, type = Sub, subid = SubId} = Entity, _) -> case node_call(Host, Type, set_subscriptions, [Nidx, JID, Sub, SubId]) of {error, _} = Err -> Err; _ -> Notify(Entity) end end, ok, Entities), case Result of ok -> {result, undefined}; {error, _} = Err -> Err end; _ -> {error, xmpp:err_forbidden( ?T("Owner privileges required"), ejabberd_option:language())} end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; Other -> Other end. -spec get_presence_and_roster_permissions( host(), jid(), [ljid()], accessModel(), [binary()]) -> {boolean(), boolean()}. get_presence_and_roster_permissions(Host, From, Owners, AccessModel, AllowedGroups) -> if (AccessModel == presence) or (AccessModel == roster) -> case Host of {User, Server, _} -> get_roster_info(User, Server, From, AllowedGroups); _ -> [{OUser, OServer, _} | _] = Owners, get_roster_info(OUser, OServer, From, AllowedGroups) end; true -> {true, true} end. -spec get_roster_info(binary(), binary(), ljid() | jid(), [binary()]) -> {boolean(), boolean()}. get_roster_info(_, _, {<<>>, <<>>, _}, _) -> {false, false}; get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> LJID = {SubscriberUser, SubscriberServer, <<>>}, {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold(roster_get_jid_info, OwnerServer, {none, none, []}, [OwnerUser, OwnerServer, LJID]), PresenceSubscription = Subscription == both orelse Subscription == from orelse {OwnerUser, OwnerServer} == {SubscriberUser, SubscriberServer}, RosterGroup = lists:any(fun (Group) -> lists:member(Group, AllowedGroups) end, Groups), {PresenceSubscription, RosterGroup}; get_roster_info(OwnerUser, OwnerServer, JID, AllowedGroups) -> get_roster_info(OwnerUser, OwnerServer, jid:tolower(JID), AllowedGroups). -spec preconditions_met(pubsub_publish_options:result(), pubsub_node_config:result()) -> boolean(). preconditions_met(PubOpts, NodeOpts) -> lists:all(fun(Opt) -> lists:member(Opt, NodeOpts) end, PubOpts). -spec service_jid(jid() | ljid() | binary()) -> jid(). service_jid(#jid{} = Jid) -> Jid; service_jid({U, S, R}) -> jid:make(U, S, R); service_jid(Host) -> jid:make(Host). %% @doc

Check if a notification must be delivered or not based on %% node and subscription options.

-spec is_to_deliver(ljid(), items | nodes, integer(), nodeOptions(), subOptions()) -> boolean(). is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> sub_to_deliver(LJID, NotifyType, Depth, SubOptions) andalso node_to_deliver(LJID, NodeOptions). -spec sub_to_deliver(ljid(), items | nodes, integer(), subOptions()) -> boolean(). sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> lists:all(fun (Option) -> sub_option_can_deliver(NotifyType, Depth, Option) end, SubOptions). -spec node_to_deliver(ljid(), nodeOptions()) -> boolean(). node_to_deliver(LJID, NodeOptions) -> presence_can_deliver(LJID, get_option(NodeOptions, presence_based_delivery)). -spec sub_option_can_deliver(items | nodes, integer(), _) -> boolean(). sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; sub_option_can_deliver(_, _, {deliver, false}) -> false; sub_option_can_deliver(_, _, {expire, When}) -> erlang:timestamp() < When; sub_option_can_deliver(_, _, _) -> true. -spec presence_can_deliver(ljid(), boolean()) -> boolean(). presence_can_deliver(_, false) -> true; presence_can_deliver({User, Server, Resource}, true) -> case ejabberd_sm:get_user_present_resources(User, Server) of [] -> false; Ss -> lists:foldl(fun (_, true) -> true; ({_, R}, _Acc) -> case Resource of <<>> -> true; R -> true; _ -> false end end, false, Ss) end. -spec state_can_deliver(ljid(), subOptions()) -> [ljid()]. state_can_deliver({U, S, R}, []) -> [{U, S, R}]; state_can_deliver({U, S, R}, SubOptions) -> case lists:keysearch(show_values, 1, SubOptions) of %% If not in suboptions, item can be delivered, case doesn't apply false -> [{U, S, R}]; %% If in a suboptions ... {_, {_, ShowValues}} -> Resources = case R of %% If the subscriber JID is a bare one, get all its resources <<>> -> user_resources(U, S); %% If the subscriber JID is a full one, use its resource R -> [R] end, lists:foldl(fun (Resource, Acc) -> get_resource_state({U, S, Resource}, ShowValues, Acc) end, [], Resources) end. -spec get_resource_state(ljid(), [binary()], [ljid()]) -> [ljid()]. get_resource_state({U, S, R}, ShowValues, JIDs) -> case ejabberd_sm:get_session_pid(U, S, R) of none -> %% If no PID, item can be delivered lists:append([{U, S, R}], JIDs); Pid -> Show = case ejabberd_c2s:get_presence(Pid) of #presence{type = unavailable} -> <<"unavailable">>; #presence{show = undefined} -> <<"online">>; #presence{show = Sh} -> atom_to_binary(Sh, latin1) end, case lists:member(Show, ShowValues) of %% If yes, item can be delivered true -> lists:append([{U, S, R}], JIDs); %% If no, item can't be delivered false -> JIDs end end. -spec payload_xmlelements([xmlel()]) -> non_neg_integer(). payload_xmlelements(Payload) -> payload_xmlelements(Payload, 0). -spec payload_xmlelements([xmlel()], non_neg_integer()) -> non_neg_integer(). payload_xmlelements([], Count) -> Count; payload_xmlelements([#xmlel{} | Tail], Count) -> payload_xmlelements(Tail, Count + 1); payload_xmlelements([_ | Tail], Count) -> payload_xmlelements(Tail, Count). -spec items_els(binary(), nodeOptions(), [#pubsub_item{}]) -> ps_items(). items_els(Node, Options, Items) -> Els = case get_option(Options, itemreply) of publisher -> [#ps_item{id = ItemId, sub_els = Payload, publisher = jid:encode(USR)} || #pubsub_item{itemid = {ItemId, _}, payload = Payload, modification = {_, USR}} <- Items]; _ -> [#ps_item{id = ItemId, sub_els = Payload} || #pubsub_item{itemid = {ItemId, _}, payload = Payload} <- Items] end, #ps_items{node = Node, items = Els}. %%%%%% broadcast functions -spec broadcast_publish_item(host(), binary(), nodeIdx(), binary(), nodeOptions(), binary(), jid(), [xmlel()], _) -> {result, boolean()}. broadcast_publish_item(Host, Node, Nidx, Type, NodeOptions, ItemId, From, Payload, Removed) -> case get_collection_subscriptions(Host, Node) of {result, SubsByDepth} -> ItemPublisher = case get_option(NodeOptions, itemreply) of publisher -> jid:encode(From); _ -> <<>> end, ItemPayload = case get_option(NodeOptions, deliver_payloads) of true -> Payload; false -> [] end, ItemsEls = #ps_items{node = Node, items = [#ps_item{id = ItemId, publisher = ItemPublisher, sub_els = ItemPayload}]}, Stanza = #message{ sub_els = [#ps_event{items = ItemsEls}]}, broadcast_stanza(Host, From, Node, Nidx, Type, NodeOptions, SubsByDepth, items, Stanza, true), case Removed of [] -> ok; _ -> case get_option(NodeOptions, notify_retract) of true -> RetractStanza = #message{ sub_els = [#ps_event{ items = #ps_items{ node = Node, retract = Removed}}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, items, RetractStanza, true); _ -> ok end end, {result, true}; _ -> {result, false} end. -spec broadcast_retract_items(host(), binary(), nodeIdx(), binary(), nodeOptions(), [itemId()]) -> {result, boolean()}. broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds) -> broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, false). -spec broadcast_retract_items(host(), binary(), nodeIdx(), binary(), nodeOptions(), [itemId()], boolean()) -> {result, boolean()}. broadcast_retract_items(_Host, _Node, _Nidx, _Type, _NodeOptions, [], _ForceNotify) -> {result, false}; broadcast_retract_items(Host, Node, Nidx, Type, NodeOptions, ItemIds, ForceNotify) -> case (get_option(NodeOptions, notify_retract) or ForceNotify) of true -> case get_collection_subscriptions(Host, Node) of {result, SubsByDepth} -> Stanza = #message{ sub_els = [#ps_event{ items = #ps_items{ node = Node, retract = ItemIds}}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, items, Stanza, true), {result, true}; _ -> {result, false} end; _ -> {result, false} end. -spec broadcast_purge_node(host(), binary(), nodeIdx(), binary(), nodeOptions()) -> {result, boolean()}. broadcast_purge_node(Host, Node, Nidx, Type, NodeOptions) -> case get_option(NodeOptions, notify_retract) of true -> case get_collection_subscriptions(Host, Node) of {result, SubsByDepth} -> Stanza = #message{sub_els = [#ps_event{purge = Node}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true}; _ -> {result, false} end; _ -> {result, false} end. -spec broadcast_removed_node(host(), binary(), nodeIdx(), binary(), nodeOptions(), subs_by_depth()) -> {result, boolean()}. broadcast_removed_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> case get_option(NodeOptions, notify_delete) of true -> case SubsByDepth of [] -> {result, false}; _ -> Stanza = #message{sub_els = [#ps_event{delete = {Node, <<>>}}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true} end; _ -> {result, false} end. -spec broadcast_created_node(host(), binary(), nodeIdx(), binary(), nodeOptions(), subs_by_depth()) -> {result, boolean()}. broadcast_created_node(_, _, _, _, _, []) -> {result, false}; broadcast_created_node(Host, Node, Nidx, Type, NodeOptions, SubsByDepth) -> Stanza = #message{sub_els = [#ps_event{create = Node}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, true), {result, true}. -spec broadcast_config_notification(host(), binary(), nodeIdx(), binary(), nodeOptions(), binary()) -> {result, boolean()}. broadcast_config_notification(Host, Node, Nidx, Type, NodeOptions, Lang) -> case get_option(NodeOptions, notify_config) of true -> case get_collection_subscriptions(Host, Node) of {result, SubsByDepth} -> Content = case get_option(NodeOptions, deliver_payloads) of true -> #xdata{type = result, fields = get_configure_xfields( Type, NodeOptions, Lang, [])}; false -> undefined end, Stanza = #message{ sub_els = [#ps_event{ configuration = {Node, Content}}]}, broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, nodes, Stanza, false), {result, true}; _ -> {result, false} end; _ -> {result, false} end. -spec get_collection_subscriptions(host(), nodeId()) -> {result, subs_by_depth()} | {error, stanza_error()}. get_collection_subscriptions(Host, Node) -> Action = fun() -> get_node_subs_by_depth(Host, Node, service_jid(Host)) end, transaction(Host, Action, sync_dirty). -spec get_node_subs_by_depth(host(), nodeId(), jid()) -> {result, subs_by_depth()} | {error, stanza_error()}. get_node_subs_by_depth(Host, Node, From) -> case tree_call(Host, get_parentnodes_tree, [Host, Node, From]) of ParentTree when is_list(ParentTree) -> {result, lists:filtermap( fun({Depth, Nodes}) -> case lists:filtermap( fun(N) -> case get_node_subs(Host, N) of {result, Result} -> {true, {N, Result}}; _ -> false end end, Nodes) of [] -> false; Subs -> {true, {Depth, Subs}} end end, ParentTree)}; Error -> Error end. -spec get_node_subs(host(), #pubsub_node{}) -> {result, [{ljid(), subId(), subOptions()}]} | {error, stanza_error()}. get_node_subs(Host, #pubsub_node{type = Type, id = Nidx}) -> WithOptions = lists:member(<<"subscription-options">>, plugin_features(Host, Type)), case node_call(Host, Type, get_node_subscriptions, [Nidx]) of {result, Subs} -> {result, get_options_for_subs(Host, Nidx, Subs, WithOptions)}; Other -> Other end. -spec get_options_for_subs(host(), nodeIdx(), [{ljid(), subscription(), subId()}], boolean()) -> [{ljid(), subId(), subOptions()}]. get_options_for_subs(_Host, _Nidx, Subs, false) -> lists:foldl(fun({JID, subscribed, SubID}, Acc) -> [{JID, SubID, []} | Acc]; (_, Acc) -> Acc end, [], Subs); get_options_for_subs(Host, Nidx, Subs, true) -> SubModule = subscription_plugin(Host), lists:foldl(fun({JID, subscribed, SubID}, Acc) -> case SubModule:get_subscription(JID, Nidx, SubID) of #pubsub_subscription{options = Options} -> [{JID, SubID, Options} | Acc]; {error, notfound} -> [{JID, SubID, []} | Acc] end; (_, Acc) -> Acc end, [], Subs). -spec broadcast_stanza(host(), nodeId(), nodeIdx(), binary(), nodeOptions(), subs_by_depth(), items | nodes, stanza(), boolean()) -> ok. broadcast_stanza(Host, _Node, _Nidx, _Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> NotificationType = get_option(NodeOptions, notification_type, headline), BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but useful Stanza = add_message_type( xmpp:set_from(BaseStanza, service_jid(Host)), NotificationType), %% Handles explicit subscriptions SubIDsByJID = subscribed_nodes_by_jid(NotifyType, SubsByDepth), lists:foreach(fun ({LJID, _NodeName, SubIDs}) -> LJIDs = case BroadcastAll of true -> {U, S, _} = LJID, [{U, S, R} || R <- user_resources(U, S)]; false -> [LJID] end, %% Determine if the stanza should have SHIM ('SubID' and 'name') headers StanzaToSend = case {SHIM, SubIDs} of {false, _} -> Stanza; %% If there's only one SubID, don't add it {true, [_]} -> Stanza; {true, SubIDs} -> add_shim_headers(Stanza, subid_shim(SubIDs)) end, lists:foreach(fun(To) -> ejabberd_router:route( xmpp:set_to(StanzaToSend, jid:make(To))) end, LJIDs) end, SubIDsByJID). -spec broadcast_stanza(host(), jid(), nodeId(), nodeIdx(), binary(), nodeOptions(), subs_by_depth(), items | nodes, stanza(), boolean()) -> ok. broadcast_stanza({LUser, LServer, LResource}, Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> broadcast_stanza({LUser, LServer, <<>>}, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM), %% Handles implicit presence subscriptions SenderResource = user_resource(LUser, LServer, LResource), NotificationType = get_option(NodeOptions, notification_type, headline), %% set the from address on the notification to the bare JID of the account owner %% Also, add "replyto" if entity has presence subscription to the account owner %% See XEP-0163 1.1 section 4.3.1 Owner = jid:make(LUser, LServer), FromBareJid = xmpp:set_from(BaseStanza, Owner), Stanza = add_extended_headers( add_message_type(FromBareJid, NotificationType), extended_headers([Publisher])), Pred = fun(To) -> delivery_permitted(Owner, To, NodeOptions) end, ejabberd_sm:route(jid:make(LUser, LServer, SenderResource), {pep_message, <<((Node))/binary, "+notify">>, Stanza, Pred}), ejabberd_router:route(xmpp:set_to(Stanza, jid:make(LUser, LServer))); broadcast_stanza(Host, _Publisher, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM) -> broadcast_stanza(Host, Node, Nidx, Type, NodeOptions, SubsByDepth, NotifyType, BaseStanza, SHIM). -spec c2s_handle_info(ejabberd_c2s:state(), term()) -> ejabberd_c2s:state(). c2s_handle_info(#{lserver := LServer} = C2SState, {pep_message, Feature, Packet, Pred}) when is_function(Pred) -> [maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) || {USR, Caps} <- mod_caps:list_features(C2SState), Pred(USR)], {stop, C2SState}; c2s_handle_info(#{lserver := LServer} = C2SState, {pep_message, Feature, Packet, {_, _, _} = USR}) -> case mod_caps:get_user_caps(USR, C2SState) of {ok, Caps} -> maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet); error -> ok end, {stop, C2SState}; c2s_handle_info(C2SState, _) -> C2SState. -spec send_items(host(), nodeId(), nodeIdx(), binary(), nodeOptions(), ljid(), last | integer()) -> ok. send_items(Host, Node, Nidx, Type, Options, LJID, Number) -> send_items(Host, Node, Nidx, Type, Options, Host, LJID, LJID, Number). send_items(Host, Node, Nidx, Type, Options, Publisher, SubLJID, ToLJID, Number) -> Items = case max_items(Host, Options) of 1 -> get_only_item(Host, Type, Nidx, SubLJID); _ -> get_last_items(Host, Type, Nidx, SubLJID, Number) end, case Items of [] -> ok; Items -> Delay = case Number of last -> % handle section 6.1.7 of XEP-0060 [Last] = Items, {Stamp, _USR} = Last#pubsub_item.modification, [#delay{stamp = Stamp}]; _ -> [] end, Stanza = #message{ sub_els = [#ps_event{items = items_els(Node, Options, Items)} | Delay]}, NotificationType = get_option(Options, notification_type, headline), send_stanza(Publisher, ToLJID, Node, add_message_type(Stanza, NotificationType)) end. -spec send_stanza(host(), ljid(), binary(), stanza()) -> ok. send_stanza({LUser, LServer, _} = Publisher, USR, Node, BaseStanza) -> Stanza = xmpp:set_from(BaseStanza, jid:make(LUser, LServer)), USRs = case USR of {PUser, PServer, <<>>} -> [{PUser, PServer, PRessource} || PRessource <- user_resources(PUser, PServer)]; _ -> [USR] end, lists:foreach( fun(To) -> ejabberd_sm:route( jid:make(Publisher), {pep_message, <<((Node))/binary, "+notify">>, add_extended_headers( Stanza, extended_headers([jid:make(Publisher)])), To}) end, USRs); send_stanza(Host, USR, _Node, Stanza) -> ejabberd_router:route( xmpp:set_from_to(Stanza, service_jid(Host), jid:make(USR))). -spec maybe_send_pep_stanza(binary(), ljid(), caps(), binary(), stanza()) -> ok. maybe_send_pep_stanza(LServer, USR, Caps, Feature, Packet) -> Features = mod_caps:get_features(LServer, Caps), case lists:member(Feature, Features) of true -> ejabberd_router:route(xmpp:set_to(Packet, jid:make(USR))); false -> ok end. -spec send_last_items(jid()) -> ok. send_last_items(JID) -> ServerHost = JID#jid.lserver, Host = host(ServerHost), DBType = config(ServerHost, db_type), LJID = jid:tolower(JID), BJID = jid:remove_resource(LJID), lists:foreach( fun(PType) -> Subs = get_subscriptions_for_send_last(Host, PType, DBType, JID, LJID, BJID), lists:foreach( fun({#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}, _, SubJID}) when Type == PType-> send_items(Host, Node, Nidx, PType, Options, Host, SubJID, LJID, 1); (_) -> ok end, lists:usort(Subs)) end, config(ServerHost, plugins)). % pep_from_offline hack can not work anymore, as sender c2s does not % exists when sender is offline, so we can't get match receiver caps % does it make sens to send PEP from an offline contact anyway ? % case config(ServerHost, ignore_pep_from_offline) of % false -> % Roster = ejabberd_hooks:run_fold(roster_get, ServerHost, [], % [{JID#jid.luser, ServerHost}]), % lists:foreach( % fun(#roster{jid = {U, S, R}, subscription = Sub}) % when Sub == both orelse Sub == from, % S == ServerHost -> % case user_resources(U, S) of % [] -> send_last_pep(jid:make(U, S, R), JID); % _ -> ok %% this is already handled by presence probe % end; % (_) -> % ok %% we can not do anything in any cases % end, Roster); % true -> % ok % end. send_last_pep(From, To, Features) -> ServerHost = From#jid.lserver, Host = host(ServerHost), Publisher = jid:tolower(From), Owner = jid:remove_resource(Publisher), NotifyNodes = case Features of _ when is_list(Features) -> lists:filtermap( fun(V) -> Vs = byte_size(V) - 7, case V of <> -> {true, NotNode}; _ -> false end end, Features); _ -> unknown end, case tree_action(Host, get_nodes, [Owner, infinity]) of Nodes when is_list(Nodes) -> lists:foreach( fun(#pubsub_node{nodeid = {_, Node}, type = Type, id = Nidx, options = Options}) -> MaybeNotify = case NotifyNodes of unknown -> true; _ -> lists:member(Node, NotifyNodes) end, case MaybeNotify andalso match_option(Options, send_last_published_item, on_sub_and_presence) of true -> case delivery_permitted(From, To, Options) of true -> LJID = jid:tolower(To), send_items(Owner, Node, Nidx, Type, Options, Publisher, LJID, LJID, 1); false -> ok end; _ -> ok end end, Nodes); _ -> ok end. -spec subscribed_nodes_by_jid(items | nodes, subs_by_depth()) -> [{ljid(), binary(), subId()}]. subscribed_nodes_by_jid(NotifyType, SubsByDepth) -> NodesToDeliver = fun (Depth, Node, Subs, Acc) -> NodeName = case Node#pubsub_node.nodeid of {_, N} -> N; Other -> Other end, NodeOptions = Node#pubsub_node.options, lists:foldl(fun({LJID, SubID, SubOptions}, {JIDs, Recipients}) -> case is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) of true -> case state_can_deliver(LJID, SubOptions) of [] -> {JIDs, Recipients}; [LJID] -> {JIDs, [{LJID, NodeName, [SubID]} | Recipients]}; JIDsToDeliver -> lists:foldl( fun(JIDToDeliver, {JIDsAcc, RecipientsAcc}) -> case lists:member(JIDToDeliver, JIDs) of %% check if the JIDs co-accumulator contains the Subscription Jid, false -> %% - if not, %% - add the Jid to JIDs list co-accumulator ; %% - create a tuple of the Jid, Nidx, and SubID (as list), %% and add the tuple to the Recipients list co-accumulator {[JIDToDeliver | JIDsAcc], [{JIDToDeliver, NodeName, [SubID]} | RecipientsAcc]}; true -> %% - if the JIDs co-accumulator contains the Jid %% get the tuple containing the Jid from the Recipient list co-accumulator {_, {JIDToDeliver, NodeName1, SubIDs}} = lists:keysearch(JIDToDeliver, 1, RecipientsAcc), %% delete the tuple from the Recipients list % v1 : Recipients1 = lists:keydelete(LJID, 1, Recipients), % v2 : Recipients1 = lists:keyreplace(LJID, 1, Recipients, {LJID, Nidx1, [SubID | SubIDs]}), %% add the SubID to the SubIDs list in the tuple, %% and add the tuple back to the Recipients list co-accumulator % v1.1 : {JIDs, lists:append(Recipients1, [{LJID, Nidx1, lists:append(SubIDs, [SubID])}])} % v1.2 : {JIDs, [{LJID, Nidx1, [SubID | SubIDs]} | Recipients1]} % v2: {JIDs, Recipients1} {JIDsAcc, lists:keyreplace(JIDToDeliver, 1, RecipientsAcc, {JIDToDeliver, NodeName1, [SubID | SubIDs]})} end end, {JIDs, Recipients}, JIDsToDeliver) end; false -> {JIDs, Recipients} end end, Acc, Subs) end, DepthsToDeliver = fun({Depth, SubsByNode}, Acc1) -> lists:foldl(fun({Node, Subs}, Acc2) -> NodesToDeliver(Depth, Node, Subs, Acc2) end, Acc1, SubsByNode) end, {_, JIDSubs} = lists:foldl(DepthsToDeliver, {[], []}, SubsByDepth), JIDSubs. -spec delivery_permitted(jid() | ljid(), jid() | ljid(), nodeOptions()) -> boolean(). delivery_permitted(From, To, Options) -> LFrom = jid:tolower(From), LTo = jid:tolower(To), RecipientIsOwner = jid:remove_resource(LFrom) == jid:remove_resource(LTo), %% TODO: Fix the 'whitelist'/'authorize' cases for last PEP notifications. %% Currently, only node owners receive those. case get_option(Options, access_model) of open -> true; presence -> true; whitelist -> RecipientIsOwner; authorize -> RecipientIsOwner; roster -> Grps = get_option(Options, roster_groups_allowed, []), {LUser, LServer, _} = LFrom, {_, IsInGrp} = get_roster_info(LUser, LServer, LTo, Grps), IsInGrp end. -spec user_resources(binary(), binary()) -> [binary()]. user_resources(User, Server) -> ejabberd_sm:get_user_resources(User, Server). -spec user_resource(binary(), binary(), binary()) -> binary(). user_resource(User, Server, <<>>) -> case user_resources(User, Server) of [R | _] -> R; _ -> <<>> end; user_resource(_, _, Resource) -> Resource. %%%%%%% Configuration handling -spec get_configure(host(), binary(), binary(), jid(), binary()) -> {error, stanza_error()} | {result, pubsub_owner()}. get_configure(Host, ServerHost, Node, From, Lang) -> Action = fun (#pubsub_node{options = Options, type = Type, id = Nidx}) -> case node_call(Host, Type, get_affiliation, [Nidx, From]) of {result, owner} -> Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), Fs = get_configure_xfields(Type, Options, Lang, Groups), {result, #pubsub_owner{ configure = {Node, #xdata{type = form, fields = Fs}}}}; {result, _} -> {error, xmpp:err_forbidden(?T("Owner privileges required"), Lang)}; Error -> Error end end, case transaction(Host, Node, Action, sync_dirty) of {result, {_, Result}} -> {result, Result}; Other -> Other end. -spec get_default(host(), binary(), jid(), binary()) -> {result, pubsub_owner()}. get_default(Host, Node, _From, Lang) -> Type = select_type(serverhost(Host), Host, Node), Options = node_options(Host, Type), Fs = get_configure_xfields(Type, Options, Lang, []), {result, #pubsub_owner{default = {<<>>, #xdata{type = form, fields = Fs}}}}. -spec match_option(#pubsub_node{} | [{atom(), any()}], atom(), any()) -> boolean(). match_option(Node, Var, Val) when is_record(Node, pubsub_node) -> match_option(Node#pubsub_node.options, Var, Val); match_option(Options, Var, Val) when is_list(Options) -> get_option(Options, Var) == Val; match_option(_, _, _) -> false. -spec get_option([{atom(), any()}], atom()) -> any(). get_option([], _) -> false; get_option(Options, Var) -> get_option(Options, Var, false). -spec get_option([{atom(), any()}], atom(), any()) -> any(). get_option(Options, Var, Def) -> case lists:keysearch(Var, 1, Options) of {value, {_Val, Ret}} -> Ret; _ -> Def end. -spec node_options(host(), binary()) -> [{atom(), any()}]. node_options(Host, Type) -> DefaultOpts = node_plugin_options(Host, Type), case config(Host, plugins) of [Type|_] -> config(Host, default_node_config, DefaultOpts); _ -> DefaultOpts end. -spec node_plugin_options(host(), binary()) -> [{atom(), any()}]. node_plugin_options(Host, Type) -> Module = plugin(Host, Type), case catch Module:options() of {'EXIT', {undef, _}} -> DefaultModule = plugin(Host, ?STDNODE), DefaultModule:options(); Result -> Result end. -spec node_owners_action(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. node_owners_action(Host, Type, Nidx, []) -> case node_action(Host, Type, get_node_affiliations, [Nidx]) of {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; _ -> [] end; node_owners_action(_Host, _Type, _Nidx, Owners) -> Owners. -spec node_owners_call(host(), binary(), nodeIdx(), [ljid()]) -> [ljid()]. node_owners_call(Host, Type, Nidx, []) -> case node_call(Host, Type, get_node_affiliations, [Nidx]) of {result, Affs} -> [LJID || {LJID, Aff} <- Affs, Aff =:= owner]; _ -> [] end; node_owners_call(_Host, _Type, _Nidx, Owners) -> Owners. node_config(Node, ServerHost) -> Opts = mod_pubsub_opt:force_node_config(ServerHost), node_config(Node, ServerHost, Opts). node_config(Node, ServerHost, [{RE, Opts}|NodeOpts]) -> case re:run(Node, RE) of {match, _} -> Opts; nomatch -> node_config(Node, ServerHost, NodeOpts) end; node_config(_, _, []) -> []. %% @spec (Host, Options) -> MaxItems %% Host = host() %% Options = [Option] %% Option = {Key::atom(), Value::term()} %% MaxItems = integer() | unlimited %% @doc

Return the maximum number of items for a given node.

%%

Unlimited means that there is no limit in the number of items that can %% be stored.

-spec max_items(host(), [{atom(), any()}]) -> non_neg_integer() | unlimited. max_items(Host, Options) -> case get_option(Options, persist_items) of true -> case get_option(Options, max_items) of I when is_integer(I), I < 0 -> 0; I when is_integer(I) -> I; _ -> get_max_items_node(Host) end; false -> case get_option(Options, send_last_published_item) of never -> 0; _ -> case is_last_item_cache_enabled(Host) of true -> 0; false -> 1 end end end. -spec item_expire(host(), [{atom(), any()}]) -> non_neg_integer() | infinity. item_expire(Host, Options) -> case get_option(Options, item_expire) of I when is_integer(I), I < 0 -> 0; I when is_integer(I) -> I; _ -> get_max_item_expire_node(Host) end. -spec get_configure_xfields(_, pubsub_node_config:result(), binary(), [binary()]) -> [xdata_field()]. get_configure_xfields(_Type, Options, Lang, Groups) -> pubsub_node_config:encode( lists:filtermap( fun({roster_groups_allowed, Value}) -> {true, {roster_groups_allowed, Value, Groups}}; ({sql, _}) -> false; ({rsm, _}) -> false; (_) -> true end, Options), Lang). %%

There are several reasons why the node configuration request might fail:

%%
    %%
  • The service does not support node configuration.
  • %%
  • The requesting entity does not have sufficient privileges to configure the node.
  • %%
  • The request did not specify a node.
  • %%
  • The node has no configuration options.
  • %%
  • The specified node does not exist.
  • %%
-spec set_configure(host(), binary(), jid(), [{binary(), [binary()]}], binary()) -> {result, undefined} | {error, stanza_error()}. set_configure(_Host, <<>>, _From, _Config, _Lang) -> {error, extended_error(xmpp:err_bad_request(), err_nodeid_required())}; set_configure(Host, Node, From, Config, Lang) -> Action = fun(#pubsub_node{options = Options, type = Type, id = Nidx} = N) -> case node_call(Host, Type, get_affiliation, [Nidx, From]) of {result, owner} -> OldOpts = case Options of [] -> node_options(Host, Type); _ -> Options end, NewOpts = merge_config( [node_config(Node, serverhost(Host)), Config, OldOpts]), case tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]) of {result, Nidx} -> {result, NewOpts}; ok -> {result, NewOpts}; Err -> Err end; {result, _} -> {error, xmpp:err_forbidden( ?T("Owner privileges required"), Lang)}; Error -> Error end end, case transaction(Host, Node, Action, transaction) of {result, {TNode, Options}} -> Nidx = TNode#pubsub_node.id, Type = TNode#pubsub_node.type, broadcast_config_notification(Host, Node, Nidx, Type, Options, Lang), {result, undefined}; Other -> Other end. -spec merge_config([[proplists:property()]]) -> [proplists:property()]. merge_config(ListOfConfigs) -> lists:ukeysort(1, lists:flatten(ListOfConfigs)). -spec decode_node_config(undefined | xdata(), binary(), binary()) -> pubsub_node_config:result() | {error, stanza_error()}. decode_node_config(undefined, _, _) -> []; decode_node_config(#xdata{fields = Fs}, Host, Lang) -> try Config = pubsub_node_config:decode(Fs), Max = get_max_items_node(Host), case {check_opt_range(max_items, Config, Max), check_opt_range(max_payload_size, Config, ?MAX_PAYLOAD_SIZE)} of {true, true} -> Config; {true, false} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_payload_size">>, ?NS_PUBSUB_NODE_CONFIG}}); {false, _} -> erlang:error( {pubsub_node_config, {bad_var_value, <<"pubsub#max_items">>, ?NS_PUBSUB_NODE_CONFIG}}) end catch _:{pubsub_node_config, Why} -> Txt = pubsub_node_config:format_error(Why), {error, xmpp:err_resource_constraint(Txt, Lang)} end. -spec decode_subscribe_options(undefined | xdata(), binary()) -> pubsub_subscribe_options:result() | {error, stanza_error()}. decode_subscribe_options(undefined, _) -> []; decode_subscribe_options(#xdata{fields = Fs}, Lang) -> try pubsub_subscribe_options:decode(Fs) catch _:{pubsub_subscribe_options, Why} -> Txt = pubsub_subscribe_options:format_error(Why), {error, xmpp:err_resource_constraint(Txt, Lang)} end. -spec decode_publish_options(undefined | xdata(), binary()) -> pubsub_publish_options:result() | {error, stanza_error()}. decode_publish_options(undefined, _) -> []; decode_publish_options(#xdata{fields = Fs}, Lang) -> try pubsub_publish_options:decode(Fs) catch _:{pubsub_publish_options, Why} -> Txt = pubsub_publish_options:format_error(Why), {error, xmpp:err_resource_constraint(Txt, Lang)} end. -spec decode_get_pending(xdata(), binary()) -> pubsub_get_pending:result() | {error, stanza_error()}. decode_get_pending(#xdata{fields = Fs}, Lang) -> try pubsub_get_pending:decode(Fs) catch _:{pubsub_get_pending, Why} -> Txt = pubsub_get_pending:format_error(Why), {error, xmpp:err_resource_constraint(Txt, Lang)} end. -spec check_opt_range(atom(), [proplists:property()], non_neg_integer() | unlimited) -> boolean(). check_opt_range(_Opt, _Opts, unlimited) -> true; check_opt_range(Opt, Opts, Max) -> case proplists:get_value(Opt, Opts, Max) of max -> true; Val -> Val =< Max end. -spec get_max_items_node(host()) -> unlimited | non_neg_integer(). get_max_items_node(Host) -> config(Host, max_items_node, ?MAXITEMS). -spec get_max_item_expire_node(host()) -> infinity | non_neg_integer(). get_max_item_expire_node(Host) -> config(Host, max_item_expire_node, infinity). -spec get_max_subscriptions_node(host()) -> undefined | non_neg_integer(). get_max_subscriptions_node(Host) -> config(Host, max_subscriptions_node, undefined). %%%% last item cache handling -spec is_last_item_cache_enabled(host()) -> boolean(). is_last_item_cache_enabled(Host) -> config(Host, last_item_cache, false). -spec set_cached_item(host(), nodeIdx(), binary(), jid(), [xmlel()]) -> ok. set_cached_item({_, ServerHost, _}, Nidx, ItemId, Publisher, Payload) -> set_cached_item(ServerHost, Nidx, ItemId, Publisher, Payload); set_cached_item(Host, Nidx, ItemId, Publisher, Payload) -> case is_last_item_cache_enabled(Host) of true -> Stamp = {erlang:timestamp(), jid:tolower(jid:remove_resource(Publisher))}, Item = #pubsub_last_item{nodeid = {Host, Nidx}, itemid = ItemId, creation = Stamp, payload = Payload}, mnesia:dirty_write(Item); _ -> ok end. -spec unset_cached_item(host(), nodeIdx()) -> ok. unset_cached_item({_, ServerHost, _}, Nidx) -> unset_cached_item(ServerHost, Nidx); unset_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of true -> mnesia:dirty_delete({pubsub_last_item, {Host, Nidx}}); _ -> ok end. -spec get_cached_item(host(), nodeIdx()) -> undefined | #pubsub_item{}. get_cached_item({_, ServerHost, _}, Nidx) -> get_cached_item(ServerHost, Nidx); get_cached_item(Host, Nidx) -> case is_last_item_cache_enabled(Host) of true -> case mnesia:dirty_read({pubsub_last_item, {Host, Nidx}}) of [#pubsub_last_item{itemid = ItemId, creation = Creation, payload = Payload}] -> #pubsub_item{itemid = {ItemId, Nidx}, payload = Payload, creation = Creation, modification = Creation}; _ -> undefined end; _ -> undefined end. %%%% plugin handling -spec host(binary()) -> binary(). host(ServerHost) -> config(ServerHost, host, <<"pubsub.", ServerHost/binary>>). -spec serverhost(host()) -> binary(). serverhost({_U, ServerHost, _R})-> serverhost(ServerHost); serverhost(Host) -> ejabberd_router:host_of_route(Host). -spec tree(host()) -> atom(). tree(Host) -> case config(Host, nodetree) of undefined -> tree(Host, ?STDTREE); Tree -> Tree end. -spec tree(host() | atom(), binary()) -> atom(). tree(_Host, <<"virtual">>) -> nodetree_virtual; % special case, virtual does not use any backend tree(Host, Name) -> submodule(Host, <<"nodetree">>, Name). -spec plugin(host() | atom(), binary()) -> atom(). plugin(Host, Name) -> submodule(Host, <<"node">>, Name). -spec plugins(host()) -> [binary()]. plugins(Host) -> case config(Host, plugins) of undefined -> [?STDNODE]; [] -> [?STDNODE]; Plugins -> Plugins end. -spec subscription_plugin(host() | atom()) -> atom(). subscription_plugin(Host) -> submodule(Host, <<"pubsub">>, <<"subscription">>). -spec submodule(host() | atom(), binary(), binary()) -> atom(). submodule(Db, Type, Name) when is_atom(Db) -> case Db of mnesia -> ejabberd:module_name([<<"pubsub">>, Type, Name]); _ -> ejabberd:module_name([<<"pubsub">>, Type, Name, misc:atom_to_binary(Db)]) end; submodule(Host, Type, Name) -> Db = mod_pubsub_opt:db_type(serverhost(Host)), submodule(Db, Type, Name). -spec config(binary(), any()) -> any(). config(ServerHost, Key) -> config(ServerHost, Key, undefined). -spec config(host(), any(), any()) -> any(). config({_User, Host, _Resource}, Key, Default) -> config(Host, Key, Default); config(ServerHost, Key, Default) -> case catch ets:lookup(gen_mod:get_module_proc(ServerHost, config), Key) of [{Key, Value}] -> Value; _ -> Default end. -spec select_type(binary(), host(), binary(), binary()) -> binary(). select_type(ServerHost, {_User, _Server, _Resource}, Node, _Type) -> case config(ServerHost, pep_mapping) of undefined -> ?PEPNODE; Mapping -> proplists:get_value(Node, Mapping, ?PEPNODE) end; select_type(ServerHost, _Host, _Node, Type) -> case config(ServerHost, plugins) of undefined -> Type; Plugins -> case lists:member(Type, Plugins) of true -> Type; false -> hd(Plugins) end end. -spec select_type(binary(), host(), binary()) -> binary(). select_type(ServerHost, Host, Node) -> select_type(ServerHost, Host, Node, hd(plugins(Host))). -spec feature(binary()) -> binary(). feature(<<"rsm">>) -> ?NS_RSM; feature(Feature) -> <<(?NS_PUBSUB)/binary, "#", Feature/binary>>. -spec features() -> [binary()]. features() -> [% see plugin "access-authorize", % OPTIONAL <<"access-open">>, % OPTIONAL this relates to access_model option in node_hometree <<"access-presence">>, % OPTIONAL this relates to access_model option in node_pep <<"access-whitelist">>, % OPTIONAL <<"collections">>, % RECOMMENDED <<"config-node">>, % RECOMMENDED <<"config-node-max">>, <<"create-and-configure">>, % RECOMMENDED <<"item-ids">>, % RECOMMENDED <<"last-published">>, % RECOMMENDED <<"member-affiliation">>, % RECOMMENDED <<"presence-notifications">>, % OPTIONAL <<"presence-subscribe">>, % RECOMMENDED <<"publisher-affiliation">>, % RECOMMENDED <<"publish-only-affiliation">>, % OPTIONAL <<"publish-options">>, % OPTIONAL <<"retrieve-default">>, <<"shim">>]. % RECOMMENDED % see plugin "retrieve-items", % RECOMMENDED % see plugin "retrieve-subscriptions", % RECOMMENDED % see plugin "subscribe", % REQUIRED % see plugin "subscription-options", % OPTIONAL % see plugin "subscription-notifications" % OPTIONAL -spec plugin_features(host(), binary()) -> [binary()]. plugin_features(Host, Type) -> Module = plugin(Host, Type), case catch Module:features() of {'EXIT', {undef, _}} -> []; Result -> Result end. -spec features(binary(), binary()) -> [binary()]. features(Host, <<>>) -> lists:usort(lists:foldl(fun (Plugin, Acc) -> Acc ++ plugin_features(Host, Plugin) end, features(), plugins(Host))); features(Host, Node) when is_binary(Node) -> Action = fun (#pubsub_node{type = Type}) -> {result, plugin_features(Host, Type)} end, case transaction(Host, Node, Action, sync_dirty) of {result, Features} -> lists:usort(features() ++ Features); _ -> features() end. %% @doc

node tree plugin call.

-spec tree_call(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_call({_User, Server, _Resource}, Function, Args) -> tree_call(Server, Function, Args); tree_call(Host, Function, Args) -> Tree = tree(Host), ?DEBUG("Tree_call apply(~ts, ~ts, ~p) @ ~ts", [Tree, Function, Args, Host]), case apply(Tree, Function, Args) of {error, #stanza_error{}} = Err -> Err; {error, {virtual, _}} = Err -> Err; {error, _} -> ErrTxt = ?T("Database failure"), Lang = ejabberd_option:language(), {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; Other -> Other end. -spec tree_action(host(), atom(), list()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). tree_action(Host, Function, Args) -> ?DEBUG("Tree_action ~p ~p ~p", [Host, Function, Args]), ServerHost = serverhost(Host), DBType = mod_pubsub_opt:db_type(ServerHost), Fun = fun () -> try tree_call(Host, Function, Args) catch ?EX_RULE(Class, Reason, St) when DBType == sql -> StackTrace = ?EX_STACK(St), ejabberd_sql:abort({exception, Class, Reason, StackTrace}) end end, Ret = case DBType of mnesia -> mnesia:sync_dirty(Fun); sql -> ejabberd_sql:sql_bloc(ServerHost, Fun); _ -> Fun() end, get_tree_action_result(Ret). -spec get_tree_action_result(any()) -> {error, stanza_error() | {virtual, nodeIdx()}} | any(). get_tree_action_result({atomic, Result}) -> Result; get_tree_action_result({aborted, {exception, Class, Reason, StackTrace}}) -> ?ERROR_MSG("Transaction aborted:~n** ~ts", [misc:format_exception(2, Class, Reason, StackTrace)]), get_tree_action_result({error, db_failure}); get_tree_action_result({aborted, Reason}) -> ?ERROR_MSG("Transaction aborted: ~p~n", [Reason]), get_tree_action_result({error, db_failure}); get_tree_action_result({error, #stanza_error{}} = Err) -> Err; get_tree_action_result({error, {virtual, _}} = Err) -> Err; get_tree_action_result({error, _}) -> ErrTxt = ?T("Database failure"), Lang = ejabberd_option:language(), {error, xmpp:err_internal_server_error(ErrTxt, Lang)}; get_tree_action_result(Other) -> %% This is very risky, but tree plugins design is really bad Other. %% @doc

node plugin call.

-spec node_call(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. node_call(Host, Type, Function, Args) -> ?DEBUG("Node_call ~p ~p ~p", [Type, Function, Args]), Module = plugin(Host, Type), case erlang:function_exported(Module, Function, length(Args)) of true -> case apply(Module, Function, Args) of {result, Result} -> {result, Result}; #pubsub_state{} = Result -> {result, Result}; {error, #stanza_error{}} = Err -> Err; {error, _} -> ErrTxt = ?T("Database failure"), Lang = ejabberd_option:language(), {error, xmpp:err_internal_server_error(ErrTxt, Lang)} end; false when Type /= ?STDNODE -> node_call(Host, ?STDNODE, Function, Args); false -> %% Let it crash with the stacktrace apply(Module, Function, Args) end. -spec node_action(host(), binary(), atom(), list()) -> {result, any()} | {error, stanza_error()}. node_action(Host, Type, Function, Args) -> ?DEBUG("Node_action ~p ~p ~p ~p", [Host, Type, Function, Args]), transaction(Host, fun() -> node_call(Host, Type, Function, Args) end, sync_dirty). %% @doc

plugin transaction handling.

-spec transaction(host(), binary(), fun((#pubsub_node{}) -> _), transaction | sync_dirty) -> {result, any()} | {error, stanza_error()}. transaction(Host, Node, Action, Trans) -> transaction( Host, fun() -> case tree_call(Host, get_node, [Host, Node]) of N when is_record(N, pubsub_node) -> case Action(N) of {result, Result} -> {result, {N, Result}}; {atomic, {result, Result}} -> {result, {N, Result}}; Other -> Other end; Error -> Error end end, Trans). -spec transaction(host(), fun(), transaction | sync_dirty) -> {result, any()} | {error, stanza_error()}. transaction(Host, Fun, Trans) -> ServerHost = serverhost(Host), DBType = mod_pubsub_opt:db_type(ServerHost), do_transaction(ServerHost, Fun, Trans, DBType). -spec do_transaction(binary(), fun(), transaction | sync_dirty, atom()) -> {result, any()} | {error, stanza_error()}. do_transaction(ServerHost, Fun, Trans, DBType) -> F = fun() -> try Fun() catch ?EX_RULE(Class, Reason, St) when (DBType == mnesia andalso Trans == transaction) orelse DBType == sql -> StackTrace = ?EX_STACK(St), Ex = {exception, Class, Reason, StackTrace}, case DBType of mnesia -> mnesia:abort(Ex); sql -> ejabberd_sql:abort(Ex) end end end, Res = case DBType of mnesia -> mnesia:Trans(F); sql -> SqlFun = case Trans of transaction -> sql_transaction; _ -> sql_bloc end, ejabberd_sql:SqlFun(ServerHost, F); _ -> F() end, get_transaction_response(Res). -spec get_transaction_response(any()) -> {result, any()} | {error, stanza_error()}. get_transaction_response({result, _} = Result) -> Result; get_transaction_response({error, #stanza_error{}} = Err) -> Err; get_transaction_response({atomic, Result}) -> get_transaction_response(Result); get_transaction_response({aborted, Err}) -> get_transaction_response(Err); get_transaction_response({error, _}) -> Lang = ejabberd_option:language(), {error, xmpp:err_internal_server_error(?T("Database failure"), Lang)}; get_transaction_response({exception, Class, Reason, StackTrace}) -> ?ERROR_MSG("Transaction aborted:~n** ~ts", [misc:format_exception(2, Class, Reason, StackTrace)]), get_transaction_response({error, db_failure}); get_transaction_response(Err) -> ?ERROR_MSG("Transaction error: ~p", [Err]), get_transaction_response({error, db_failure}). %%%% helpers %% Add pubsub-specific error element -spec extended_error(stanza_error(), ps_error()) -> stanza_error(). extended_error(StanzaErr, PubSubErr) -> StanzaErr#stanza_error{sub_els = [PubSubErr]}. -spec err_closed_node() -> ps_error(). err_closed_node() -> #ps_error{type = 'closed-node'}. -spec err_configuration_required() -> ps_error(). err_configuration_required() -> #ps_error{type = 'configuration-required'}. -spec err_invalid_jid() -> ps_error(). err_invalid_jid() -> #ps_error{type = 'invalid-jid'}. -spec err_invalid_options() -> ps_error(). err_invalid_options() -> #ps_error{type = 'invalid-options'}. -spec err_invalid_payload() -> ps_error(). err_invalid_payload() -> #ps_error{type = 'invalid-payload'}. -spec err_invalid_subid() -> ps_error(). err_invalid_subid() -> #ps_error{type = 'invalid-subid'}. -spec err_item_forbidden() -> ps_error(). err_item_forbidden() -> #ps_error{type = 'item-forbidden'}. -spec err_item_required() -> ps_error(). err_item_required() -> #ps_error{type = 'item-required'}. -spec err_jid_required() -> ps_error(). err_jid_required() -> #ps_error{type = 'jid-required'}. -spec err_max_items_exceeded() -> ps_error(). err_max_items_exceeded() -> #ps_error{type = 'max-items-exceeded'}. -spec err_max_nodes_exceeded() -> ps_error(). err_max_nodes_exceeded() -> #ps_error{type = 'max-nodes-exceeded'}. -spec err_nodeid_required() -> ps_error(). err_nodeid_required() -> #ps_error{type = 'nodeid-required'}. -spec err_not_in_roster_group() -> ps_error(). err_not_in_roster_group() -> #ps_error{type = 'not-in-roster-group'}. -spec err_not_subscribed() -> ps_error(). err_not_subscribed() -> #ps_error{type = 'not-subscribed'}. -spec err_payload_too_big() -> ps_error(). err_payload_too_big() -> #ps_error{type = 'payload-too-big'}. -spec err_payload_required() -> ps_error(). err_payload_required() -> #ps_error{type = 'payload-required'}. -spec err_pending_subscription() -> ps_error(). err_pending_subscription() -> #ps_error{type = 'pending-subscription'}. -spec err_precondition_not_met() -> ps_error(). err_precondition_not_met() -> #ps_error{type = 'precondition-not-met'}. -spec err_presence_subscription_required() -> ps_error(). err_presence_subscription_required() -> #ps_error{type = 'presence-subscription-required'}. -spec err_subid_required() -> ps_error(). err_subid_required() -> #ps_error{type = 'subid-required'}. -spec err_too_many_subscriptions() -> ps_error(). err_too_many_subscriptions() -> #ps_error{type = 'too-many-subscriptions'}. -spec err_unsupported(ps_feature()) -> ps_error(). err_unsupported(Feature) -> #ps_error{type = 'unsupported', feature = Feature}. -spec err_unsupported_access_model() -> ps_error(). err_unsupported_access_model() -> #ps_error{type = 'unsupported-access-model'}. -spec uniqid() -> mod_pubsub:itemId(). uniqid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). -spec add_message_type(message(), message_type()) -> message(). add_message_type(#message{} = Message, Type) -> Message#message{type = Type}. %% Place of changed at the bottom of the stanza %% cf. http://xmpp.org/extensions/xep-0060.html#publisher-publish-success-subid %% %% "[SHIM Headers] SHOULD be included after the event notification information %% (i.e., as the last child of the stanza)". -spec add_shim_headers(stanza(), [{binary(), binary()}]) -> stanza(). add_shim_headers(Stanza, Headers) -> xmpp:set_subtag(Stanza, #shim{headers = Headers}). -spec add_extended_headers(stanza(), [address()]) -> stanza(). add_extended_headers(Stanza, Addrs) -> xmpp:set_subtag(Stanza, #addresses{list = Addrs}). -spec subid_shim([binary()]) -> [{binary(), binary()}]. subid_shim(SubIds) -> [{<<"SubId">>, SubId} || SubId <- SubIds]. %% The argument is a list of Jids because this function could be used %% with the 'pubsub#replyto' (type=jid-multi) node configuration. -spec extended_headers([jid()]) -> [address()]. extended_headers(Jids) -> [#address{type = replyto, jid = Jid} || Jid <- Jids]. -spec purge_offline(ljid()) -> ok. purge_offline(LJID) -> Host = host(element(2, LJID)), Plugins = plugins(Host), Result = lists:foldl( fun(Type, {Status, Acc}) -> Features = plugin_features(Host, Type), case lists:member(<<"retrieve-affiliations">>, plugin_features(Host, Type)) of false -> {{error, extended_error(xmpp:err_feature_not_implemented(), err_unsupported('retrieve-affiliations'))}, Acc}; true -> Items = lists:member(<<"retract-items">>, Features) andalso lists:member(<<"persistent-items">>, Features), if Items -> case node_action(Host, Type, get_entity_affiliations, [Host, LJID]) of {result, Affs} -> {Status, [Affs | Acc]}; {error, _} = Err -> {Err, Acc} end; true -> {Status, Acc} end end end, {ok, []}, Plugins), case Result of {ok, Affs} -> lists:foreach( fun ({Node, Affiliation}) -> Options = Node#pubsub_node.options, Publisher = lists:member(Affiliation, [owner,publisher,publish_only]), Open = (get_option(Options, publish_model) == open), Purge = (get_option(Options, purge_offline) andalso get_option(Options, persist_items)), if (Publisher or Open) and Purge -> purge_offline(Host, LJID, Node); true -> ok end end, lists:usort(lists:flatten(Affs))); _ -> ok end. -spec purge_offline(host(), ljid(), #pubsub_node{}) -> ok | {error, stanza_error()}. purge_offline(Host, LJID, Node) -> Nidx = Node#pubsub_node.id, Type = Node#pubsub_node.type, Options = Node#pubsub_node.options, case node_action(Host, Type, get_items, [Nidx, service_jid(Host), undefined]) of {result, {[], _}} -> ok; {result, {Items, _}} -> {User, Server, Resource} = LJID, PublishModel = get_option(Options, publish_model), ForceNotify = get_option(Options, notify_retract), {_, NodeId} = Node#pubsub_node.nodeid, lists:foreach( fun(#pubsub_item{itemid = {ItemId, _}, modification = {_, {U, S, R}}}) when (U == User) and (S == Server) and (R == Resource) -> case node_action(Host, Type, delete_item, [Nidx, {U, S, <<>>}, PublishModel, ItemId]) of {result, {_, broadcast}} -> broadcast_retract_items(Host, NodeId, Nidx, Type, Options, [ItemId], ForceNotify), case get_cached_item(Host, Nidx) of #pubsub_item{itemid = {ItemId, Nidx}} -> unset_cached_item(Host, Nidx); _ -> ok end; _ -> ok end; (_) -> true end, Items); {error, #stanza_error{}} = Err -> Err; _ -> Txt = ?T("Database failure"), Lang = ejabberd_option:language(), {error, xmpp:err_internal_server_error(Txt, Lang)} end. -spec delete_old_items(non_neg_integer()) -> ok | error. delete_old_items(N) -> Results = lists:flatmap( fun(Host) -> case tree_action(Host, get_all_nodes, [Host]) of Nodes when is_list(Nodes) -> lists:map( fun(#pubsub_node{id = Nidx, type = Type}) -> case node_action(Host, Type, remove_extra_items, [Nidx , N]) of {result, _} -> ok; {error, _} -> error end end, Nodes); _ -> error end end, ejabberd_option:hosts()), case lists:member(error, Results) of true -> error; false -> ok end. -spec delete_expired_items() -> ok | error. delete_expired_items() -> Results = lists:flatmap( fun(Host) -> case tree_action(Host, get_all_nodes, [Host]) of Nodes when is_list(Nodes) -> lists:map( fun(#pubsub_node{id = Nidx, type = Type, options = Options}) -> case item_expire(Host, Options) of infinity -> ok; Seconds -> case node_action( Host, Type, remove_expired_items, [Nidx, Seconds]) of {result, []} -> ok; {result, [_|_]} -> unset_cached_item( Host, Nidx); {error, _} -> error end end end, Nodes); _ -> error end end, ejabberd_option:hosts()), case lists:member(error, Results) of true -> error; false -> ok end. -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> [#ejabberd_commands{name = delete_old_pubsub_items, tags = [purge], desc = "Keep only NUMBER of PubSub items per node", note = "added in 21.12", module = ?MODULE, function = delete_old_items, args_desc = ["Number of items to keep per node"], args = [{number, integer}], result = {res, rescode}, result_desc = "0 if command failed, 1 when succeeded", args_example = [1000], result_example = ok}, #ejabberd_commands{name = delete_expired_pubsub_items, tags = [purge], desc = "Delete expired PubSub items", note = "added in 21.12", module = ?MODULE, function = delete_expired_items, args = [], result = {res, rescode}, result_desc = "0 if command failed, 1 when succeeded", result_example = ok}]. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access_createnode) -> econf:acl(); mod_opt_type(name) -> econf:binary(); mod_opt_type(ignore_pep_from_offline) -> econf:bool(); mod_opt_type(last_item_cache) -> econf:bool(); mod_opt_type(max_items_node) -> econf:non_neg_int(unlimited); mod_opt_type(max_item_expire_node) -> econf:timeout(second, infinity); mod_opt_type(max_nodes_discoitems) -> econf:non_neg_int(infinity); mod_opt_type(max_subscriptions_node) -> econf:non_neg_int(); mod_opt_type(force_node_config) -> econf:map( econf:glob(), econf:map( econf:atom(), econf:either( econf:int(), econf:atom()), [{return, orddict}, unique])); mod_opt_type(default_node_config) -> econf:map( econf:atom(), econf:either( econf:int(), econf:atom()), [unique]); mod_opt_type(nodetree) -> econf:binary(); mod_opt_type(pep_mapping) -> econf:map(econf:binary(), econf:binary()); mod_opt_type(plugins) -> econf:list( econf:enum([<<"flat">>, <<"pep">>]), [unique]); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(vcard) -> econf:vcard_temp(). mod_options(Host) -> [{access_createnode, all}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {host, <<"pubsub.", Host/binary>>}, {hosts, []}, {name, ?T("Publish-Subscribe")}, {vcard, undefined}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {max_items_node, ?MAXITEMS}, {max_item_expire_node, infinity}, {max_nodes_discoitems, 100}, {nodetree, ?STDTREE}, {pep_mapping, []}, {plugins, [?STDNODE]}, {max_subscriptions_node, undefined}, {default_node_config, []}, {force_node_config, []}]. mod_doc() -> #{desc => [?T("This module offers a service for " "https://xmpp.org/extensions/xep-0060.html" "[XEP-0060: Publish-Subscribe]. The functionality in " "'mod_pubsub' can be extended using plugins. " "The plugin that implements PEP " "(https://xmpp.org/extensions/xep-0163.html" "[XEP-0163: Personal Eventing via Pubsub]) " "is enabled in the default ejabberd configuration file, " "and it requires _`mod_caps`_.")], opts => [{access_createnode, #{value => "AccessName", desc => ?T("This option restricts which users are allowed to " "create pubsub nodes using 'acl' and 'access'. " "By default any account in the local ejabberd server " "is allowed to create pubsub nodes. " "The default value is: 'all'.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to " "this module only.")}}, {default_node_config, #{value => "List of Key:Value", desc => ?T("To override default node configuration, regardless " "of node plugin. Value is a list of key-value " "definition. Node configuration still uses default " "configuration defined by node plugin, and overrides " "any items by value defined in this configurable list.")}}, {force_node_config, #{value => "List of Node and the list of its Key:Value", desc => ?T("Define the configuration for given nodes. " "The default value is: '[]'."), example => ["force_node_config:", " ## Avoid buggy clients to make their bookmarks public", " storage:bookmarks:", " access_model: whitelist"]}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber " "ID will be the hostname of the virtual host with the " "prefix \"pubsub.\". The keyword '@HOST@' is replaced " "with the real virtual host name.")}}, {ignore_pep_from_offline, #{value => "false | true", desc => ?T("To specify whether or not we should get last " "published PEP items from users in our roster which " "are offline when we connect. Value is 'true' or " "'false'. If not defined, pubsub assumes true so we " "only get last items of online contacts.")}}, {last_item_cache, #{value => "false | true", desc => ?T("To specify whether or not pubsub should cache last " "items. Value is 'true' or 'false'. If not defined, " "pubsub does not cache last items. On systems with not" " so many nodes, caching last items speeds up pubsub " "and allows to raise user connection rate. The cost " "is memory usage, as every item is stored in memory.")}}, {max_item_expire_node, #{value => "timeout() | infinity", note => "added in 21.12", desc => ?T("Specify the maximum item epiry time. Default value " "is: 'infinity'.")}}, {max_items_node, #{value => "non_neg_integer() | infinity", desc => ?T("Define the maximum number of items that can be " "stored in a node. Default value is: '1000'.")}}, {max_nodes_discoitems, #{value => "pos_integer() | infinity", desc => ?T("The maximum number of nodes to return in a " "discoitem response. The default value is: '100'.")}}, {max_subscriptions_node, #{value => "MaxSubs", desc => ?T("Define the maximum number of subscriptions managed " "by a node. " "Default value is no limitation: 'undefined'.")}}, {name, #{value => ?T("Name"), desc => ?T("The value of the service name. This name is only visible " "in some clients that support " "https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. " "The default is 'vCard User Search'.")}}, {nodetree, #{value => "Nodetree", desc => [?T("To specify which nodetree to use. If not defined, the " "default pubsub nodetree is used: 'tree'. Only one " "nodetree can be used per host, and is shared by all " "node plugins."), ?T("- 'tree' nodetree store node configuration and " "relations on the database. 'flat' nodes are stored " "without any relationship, and 'hometree' nodes can " "have child nodes."), ?T("- 'virtual' nodetree does not store nodes on database. " "This saves resources on systems with tons of nodes. " "If using the 'virtual' nodetree, you can only enable " "those node plugins: '[flat, pep]' or '[flat]'; any " "other plugins configuration will not work. Also, all " "nodes will have the default configuration, and this " "can not be changed. Using 'virtual' nodetree requires " "to start from a clean database, it will not work if " "you used the default 'tree' nodetree before.")]}}, {pep_mapping, #{value => "List of Key:Value", desc => ?T("This allows to define a list of key-value to choose " "defined node plugins on given PEP namespace. " "The following example will use 'node_tune' instead of " "'node_pep' for every PEP node with the tune namespace:"), example => ["modules:", " ...", " mod_pubsub:", " pep_mapping:", " http://jabber.org/protocol/tune: tune", " ..."] }}, {plugins, #{value => "[Plugin, ...]", desc => [?T("To specify which pubsub node plugins to use. " "The first one in the list is used by default. " "If this option is not defined, the default plugins " "list is: '[flat]'. PubSub clients can define which " "plugin to use when creating a node: " "add 'type=\'plugin-name\'' attribute " "to the 'create' stanza element."), ?T("- 'flat' plugin handles the default behaviour and " "follows standard XEP-0060 implementation."), ?T("- 'pep' plugin adds extention to handle Personal " "Eventing Protocol (XEP-0163) to the PubSub engine. " "Adding pep allows to handle PEP automatically.")]}}, {vcard, #{value => ?T("vCard"), desc => ?T("A custom vCard of the server that will be displayed by " "some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML " "representation of vCard. Since the representation has " "no attributes, the mapping is straightforward."), example => [{?T("The following XML representation of vCard:"), ["", " PubSub Service", " ", " ", " Elm Street", " ", ""]}, {?T("will be translated to:"), ["vcard:", " fn: PubSub Service", " adr:", " -", " work: true", " street: Elm Street"]}]}} ], example => [{?T("Example of configuration that uses flat nodes as default, " "and allows use of flat, hometree and pep nodes:"), ["modules:", " ...", " mod_pubsub:", " access_createnode: pubsub_createnode", " max_subscriptions_node: 100", " default_node_config:", " notification_type: normal", " notify_retract: false", " max_items: 4", " plugins:", " - flat", " - pep", " ..."]}, {?T("Using relational database requires using mod_pubsub with " "db_type 'sql'. Only flat, hometree and pep plugins supports " "SQL. The following example shows previous configuration " "with SQL usage:"), ["modules:", " ...", " mod_pubsub:", " db_type: sql", " access_createnode: pubsub_createnode", " ignore_pep_from_offline: true", " last_item_cache: false", " plugins:", " - flat", " - pep", " ..."]} ]}. ejabberd-21.12/src/ejabberd_http.erl0000644000232200023220000007401214154362354017726 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_http.erl %%% Author : Alexey Shchepin %%% Purpose : %%% Created : 27 Feb 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_http). -behaviour(ejabberd_listener). -author('alexey@process-one.net'). %% External exports -export([start/3, start_link/3, accept/1, receive_headers/1, recv_file/2, listen_opt_type/1, listen_options/0, apply_custom_headers/2]). -export([init/3]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include_lib("kernel/include/file.hrl"). -record(state, {sockmod, socket, request_method, request_version, request_path, request_auth, request_keepalive, request_content_length = 0, request_lang = <<"en">>, %% XXX bard: request handlers are configured in %% ejabberd.cfg under the HTTP service. For example, %% to have the module test_web handle requests with %% paths starting with "/test/module": %% %% {5280, ejabberd_http, [http_bind, web_admin, %% {request_handlers, [{["test", "module"], mod_test_web}]}]} %% request_handlers = [], request_host, request_port, request_tp, request_headers = [], end_of_request = false, options = [], default_host, custom_headers, trail = <<>>, addr_re, sock_peer_name = none }). -define(XHTML_DOCTYPE, <<"\n\n">>). -define(HTML_DOCTYPE, <<"\n" "">>). -define(RECV_BUF, 65536). -define(SEND_BUF, 65536). -define(MAX_POST_SIZE, 20971520). %% 20Mb start(SockMod, Socket, Opts) -> {ok, proc_lib:spawn(ejabberd_http, init, [SockMod, Socket, Opts])}. start_link(SockMod, Socket, Opts) -> {ok, proc_lib:spawn_link(ejabberd_http, init, [SockMod, Socket, Opts])}. init(SockMod, Socket, Opts) -> TLSEnabled = proplists:get_bool(tls, Opts), TLSOpts1 = lists:filter(fun ({ciphers, _}) -> true; ({dhfile, _}) -> true; ({cafile, _}) -> true; ({protocol_options, _}) -> true; (_) -> false end, Opts), TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of false -> [compression_none | TLSOpts1]; true -> TLSOpts1 end, TLSOpts3 = case ejabberd_pkix:get_certfile( ejabberd_config:get_myname()) of error -> TLSOpts2; {ok, CertFile} -> [{certfile, CertFile}|TLSOpts2] end, TLSOpts = [verify_none | TLSOpts3], {SockMod1, Socket1} = if TLSEnabled -> inet:setopts(Socket, [{recbuf, ?RECV_BUF}]), {ok, TLSSocket} = fast_tls:tcp_to_tls(Socket, TLSOpts), {fast_tls, TLSSocket}; true -> {SockMod, Socket} end, SockPeer = proplists:get_value(sock_peer_name, Opts, none), RequestHandlers = proplists:get_value(request_handlers, Opts, []), ?DEBUG("S: ~p~n", [RequestHandlers]), {ok, RE} = re:compile(<<"^(?:\\[(.*?)\\]|(.*?))(?::(\\d+))?$">>), CustomHeaders = proplists:get_value(custom_headers, Opts, []), State = #state{sockmod = SockMod1, socket = Socket1, custom_headers = CustomHeaders, options = Opts, request_handlers = RequestHandlers, sock_peer_name = SockPeer, addr_re = RE}, try receive_headers(State) of V -> V catch {error, _} -> State end. accept(_Pid) -> ok. send_text(_State, none) -> ok; send_text(State, Text) -> case (State#state.sockmod):send(State#state.socket, Text) of ok -> ok; {error, timeout} -> ?INFO_MSG("Timeout on ~p:send", [State#state.sockmod]), exit(normal); Error -> ?DEBUG("Error in ~p:send: ~p", [State#state.sockmod, Error]), exit(normal) end. send_file(State, Fd, Size, FileName) -> try case State#state.sockmod of gen_tcp -> case file:sendfile(Fd, State#state.socket, 0, Size, []) of {ok, _} -> ok end; _ -> case file:read(Fd, ?SEND_BUF) of {ok, Data} -> send_text(State, Data), send_file(State, Fd, Size, FileName); eof -> ok end end catch _:{case_clause, {error, Why}} -> if Why /= closed -> ?WARNING_MSG("Failed to read ~ts: ~ts", [FileName, file_format_error(Why)]), exit(normal); true -> ok end end. receive_headers(#state{trail = Trail} = State) -> SockMod = State#state.sockmod, Socket = State#state.socket, Data = SockMod:recv(Socket, 0, 300000), case Data of {error, closed} when State#state.request_method == undefined -> % socket closed without receiving anything in it ok; {error, Error} -> ?DEBUG("Error when retrieving http headers ~p: ~p", [State#state.sockmod, Error]), ok; {ok, D} -> parse_headers(State#state{trail = <>}) end. parse_headers(#state{trail = <<>>} = State) -> receive_headers(State); parse_headers(#state{request_method = Method, trail = Data} = State) -> PktType = case Method of undefined -> http_bin; _ -> httph_bin end, case erlang:decode_packet(PktType, Data, []) of {ok, Pkt, Rest} -> NewState = process_header(State#state{trail = Rest}, {ok, Pkt}), case NewState#state.end_of_request of true -> ok; _ -> parse_headers(NewState) end; {more, _} -> receive_headers(State#state{trail = Data}); _ -> ok end. process_header(State, Data) -> SockMod = State#state.sockmod, Socket = State#state.socket, case Data of {ok, {http_request, Method, Uri, Version}} -> KeepAlive = case Version of {1, 1} -> true; _ -> false end, Path = case Uri of {absoluteURI, _Scheme, _Host, _Port, P} -> {abs_path, P}; {abs_path, P} -> {abs_path, P}; _ -> Uri end, State#state{request_method = Method, request_version = Version, request_path = Path, request_keepalive = KeepAlive}; {ok, {http_header, _, 'Connection' = Name, _, Conn}} -> KeepAlive1 = case misc:tolower(Conn) of <<"keep-alive">> -> true; <<"close">> -> false; _ -> State#state.request_keepalive end, State#state{request_keepalive = KeepAlive1, request_headers = add_header(Name, Conn, State)}; {ok, {http_header, _, 'Authorization' = Name, _, Auth}} -> State#state{request_auth = parse_auth(Auth), request_headers = add_header(Name, Auth, State)}; {ok, {http_header, _, 'Content-Length' = Name, _, SLen}} -> case catch binary_to_integer(SLen) of Len when is_integer(Len) -> State#state{request_content_length = Len, request_headers = add_header(Name, SLen, State)}; _ -> State end; {ok, {http_header, _, 'Accept-Language' = Name, _, Langs}} -> State#state{request_lang = parse_lang(Langs), request_headers = add_header(Name, Langs, State)}; {ok, {http_header, _, 'Host' = Name, _, Value}} -> {Host, Port, TP} = get_transfer_protocol(State#state.addr_re, SockMod, Value), State#state{request_host = Host, request_port = Port, request_tp = TP, request_headers = add_header(Name, Value, State)}; {ok, {http_header, _, Name, _, Value}} when is_binary(Name) -> State#state{request_headers = add_header(normalize_header_name(Name), Value, State)}; {ok, {http_header, _, Name, _, Value}} -> State#state{request_headers = add_header(Name, Value, State)}; {ok, http_eoh} when State#state.request_host == undefined; State#state.request_host == error -> {State1, Out} = process_request(State), send_text(State1, Out), process_header(State, {ok, {http_error, <<>>}}); {ok, http_eoh} -> ?DEBUG("(~w) http query: ~w ~p~n", [State#state.socket, State#state.request_method, element(2, State#state.request_path)]), {State3, Out} = process_request(State), send_text(State3, Out), case State3#state.request_keepalive of true -> #state{sockmod = SockMod, socket = Socket, trail = State3#state.trail, options = State#state.options, default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re}; _ -> #state{end_of_request = true, trail = State3#state.trail, options = State#state.options, default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re} end; _ -> #state{end_of_request = true, options = State#state.options, default_host = State#state.default_host, custom_headers = State#state.custom_headers, request_handlers = State#state.request_handlers, addr_re = State#state.addr_re} end. add_header(Name, Value, State)-> [{Name, Value} | State#state.request_headers]. get_transfer_protocol(RE, SockMod, HostPort) -> {Proto, DefPort} = case SockMod of gen_tcp -> {http, 80}; fast_tls -> {https, 443} end, {Host, Port} = case re:run(HostPort, RE, [{capture,[1,2,3],binary}]) of nomatch -> {error, DefPort}; {match, [<<>>, H, <<>>]} -> {jid:nameprep(H), DefPort}; {match, [H, <<>>, <<>>]} -> {jid:nameprep(H), DefPort}; {match, [<<>>, H, PortStr]} -> {jid:nameprep(H), binary_to_integer(PortStr)}; {match, [H, <<>>, PortStr]} -> {jid:nameprep(H), binary_to_integer(PortStr)} end, {Host, Port, Proto}. %% XXX bard: search through request handlers looking for one that %% matches the requested URL path, and pass control to it. If none is %% found, answer with HTTP 404. process([], _) -> ejabberd_web:error(not_found); process(Handlers, Request) -> {HandlerPathPrefix, HandlerModule, HandlerOpts, HandlersLeft} = case Handlers of [{Pfx, Mod} | Tail] -> {Pfx, Mod, [], Tail}; [{Pfx, Mod, Opts} | Tail] -> {Pfx, Mod, Opts, Tail} end, case (lists:prefix(HandlerPathPrefix, Request#request.path) or (HandlerPathPrefix==Request#request.path)) of true -> ?DEBUG("~p matches ~p", [Request#request.path, HandlerPathPrefix]), %% LocalPath is the path "local to the handler", i.e. if %% the handler was registered to handle "/test/" and the %% requested path is "/test/foo/bar", the local path is %% ["foo", "bar"] LocalPath = lists:nthtail(length(HandlerPathPrefix), Request#request.path), R = case erlang:function_exported(HandlerModule, socket_handoff, 3) of true -> HandlerModule:socket_handoff( LocalPath, Request, HandlerOpts); false -> HandlerModule:process(LocalPath, Request) end, ejabberd_hooks:run(http_request_debug, [{LocalPath, Request}]), R; false -> process(HandlersLeft, Request) end. extract_path_query(#state{request_method = Method, request_path = {abs_path, Path}} = State) when Method =:= 'GET' orelse Method =:= 'HEAD' orelse Method =:= 'DELETE' orelse Method =:= 'OPTIONS' -> case catch url_decode_q_split_normalize(Path) of {'EXIT', Error} -> ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]), {State, false}; {LPath, Query} -> LQuery = case catch parse_urlencoded(Query) of {'EXIT', _Reason} -> []; LQ -> LQ end, {State, {LPath, LQuery, <<"">>, Path}} end; extract_path_query(#state{request_method = Method, request_path = {abs_path, Path}, request_content_length = Len, trail = Trail, sockmod = _SockMod, socket = _Socket} = State) when (Method =:= 'POST' orelse Method =:= 'PUT') andalso Len>0 -> case catch url_decode_q_split_normalize(Path) of {'EXIT', Error} -> ?DEBUG("Error decoding URL '~p': ~p", [Path, Error]), {State, false}; {LPath, _Query} -> case Method of 'PUT' -> {State, {LPath, [], Trail, Path}}; 'POST' -> case recv_data(State) of {ok, Data} -> LQuery = case catch parse_urlencoded(Data) of {'EXIT', _Reason} -> []; LQ -> LQ end, {State, {LPath, LQuery, Data, Path}}; error -> {State, false} end end end; extract_path_query(State) -> {State, false}. process_request(#state{request_host = undefined, custom_headers = CustomHeaders} = State) -> {State, make_text_output(State, 400, CustomHeaders, <<"Missing Host header">>)}; process_request(#state{request_host = error, custom_headers = CustomHeaders} = State) -> {State, make_text_output(State, 400, CustomHeaders, <<"Malformed Host header">>)}; process_request(#state{request_method = Method, request_auth = Auth, request_lang = Lang, request_version = Version, sockmod = SockMod, socket = Socket, sock_peer_name = SockPeer, options = Options, request_host = Host, request_port = Port, request_tp = TP, request_content_length = Length, request_headers = RequestHeaders, request_handlers = RequestHandlers, custom_headers = CustomHeaders} = State) -> case proplists:get_value(<<"Expect">>, RequestHeaders, <<>>) of <<"100-", _/binary>> when Version == {1, 1} -> send_text(State, <<"HTTP/1.1 100 Continue\r\n\r\n">>); _ -> ok end, case extract_path_query(State) of {State2, false} -> {State2, make_bad_request(State)}; {State2, {LPath, LQuery, Data, RawPath}} -> PeerName = case SockPeer of none -> case SockMod of gen_tcp -> inet:peername(Socket); _ -> SockMod:peername(Socket) end; {_, Peer} -> {ok, Peer} end, IPHere = case PeerName of {ok, V} -> V; {error, _} = E -> throw(E) end, XFF = proplists:get_value('X-Forwarded-For', RequestHeaders, []), IP = analyze_ip_xff(IPHere, XFF), Request = #request{method = Method, path = LPath, raw_path = RawPath, q = LQuery, auth = Auth, length = Length, sockmod = SockMod, socket = Socket, data = Data, lang = Lang, host = Host, port = Port, tp = TP, opts = Options, headers = RequestHeaders, ip = IP}, RequestHandlers1 = ejabberd_hooks:run_fold( http_request_handlers, RequestHandlers, [Host, Request]), Res = case process(RequestHandlers1, Request) of El when is_record(El, xmlel) -> make_xhtml_output(State, 200, CustomHeaders, El); {Status, Headers, El} when is_record(El, xmlel) -> make_xhtml_output(State, Status, apply_custom_headers(Headers, CustomHeaders), El); Output when is_binary(Output) or is_list(Output) -> make_text_output(State, 200, CustomHeaders, Output); {Status, Headers, Output} when is_binary(Output) or is_list(Output) -> make_text_output(State, Status, apply_custom_headers(Headers, CustomHeaders), Output); {Status, Headers, {file, FileName}} -> make_file_output(State, Status, Headers, FileName); {Status, Reason, Headers, Output} when is_binary(Output) or is_list(Output) -> make_text_output(State, Status, Reason, apply_custom_headers(Headers, CustomHeaders), Output); _ -> none end, {State2#state{trail = <<>>}, Res} end. make_bad_request(State) -> make_xhtml_output(State, 400, State#state.custom_headers, ejabberd_web:make_xhtml([#xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}])). analyze_ip_xff(IP, []) -> IP; analyze_ip_xff({IPLast, Port}, XFF) -> [ClientIP | ProxiesIPs] = str:tokens(XFF, <<", ">>) ++ [misc:ip_to_list(IPLast)], TrustedProxies = ejabberd_option:trusted_proxies(), IPClient = case is_ipchain_trusted(ProxiesIPs, TrustedProxies) of true -> {ok, IPFirst} = inet_parse:address( binary_to_list(ClientIP)), ?DEBUG("The IP ~w was replaced with ~w due to " "header X-Forwarded-For: ~ts", [IPLast, IPFirst, XFF]), IPFirst; false -> IPLast end, {IPClient, Port}. is_ipchain_trusted([], _) -> false; is_ipchain_trusted(_UserIPs, all) -> true; is_ipchain_trusted(UserIPs, Masks) -> lists:all( fun(IP) -> case inet:parse_address(binary_to_list(IP)) of {ok, IP2} -> lists:any( fun({Mask, MaskLen}) -> misc:match_ip_mask(IP2, Mask, MaskLen) end, Masks); _ -> false end end, UserIPs). recv_data(#state{request_content_length = Len}) when Len >= ?MAX_POST_SIZE -> error; recv_data(#state{request_content_length = Len, trail = Trail, sockmod = SockMod, socket = Socket}) -> NewLen = Len - byte_size(Trail), if NewLen > 0 -> case SockMod:recv(Socket, NewLen, 60000) of {ok, Data} -> {ok, <>}; {error, _} -> error end; true -> {ok, Trail} end. recv_file(#request{length = Len, data = Trail, sockmod = SockMod, socket = Socket}, Path) -> case file:open(Path, [write, exclusive, raw]) of {ok, Fd} -> Res = case file:write(Fd, Trail) of ok -> NewLen = max(0, Len - byte_size(Trail)), do_recv_file(NewLen, SockMod, Socket, Fd); {error, _} = Err -> Err end, file:close(Fd), case Res of ok -> ok; {error, _} -> file:delete(Path) end, Res; {error, _} = Err -> Err end. do_recv_file(0, _SockMod, _Socket, _Fd) -> ok; do_recv_file(Len, SockMod, Socket, Fd) -> ChunkLen = min(Len, ?RECV_BUF), case SockMod:recv(Socket, ChunkLen, timer:seconds(30)) of {ok, Data} -> case file:write(Fd, Data) of ok -> do_recv_file(Len-size(Data), SockMod, Socket, Fd); {error, _} = Err -> Err end; {error, _} -> {error, closed} end. make_headers(State, Status, Reason, Headers, Data) -> Len = if is_integer(Data) -> Data; true -> iolist_size(Data) end, Headers1 = [{<<"Content-Length">>, integer_to_binary(Len)} | Headers], Headers2 = case lists:keyfind(<<"Content-Type">>, 1, Headers) of {_, _} -> Headers1; false -> [{<<"Content-Type">>, <<"text/html; charset=utf-8">>} | Headers1] end, HeadersOut = case {State#state.request_version, State#state.request_keepalive} of {{1, 1}, true} -> Headers2; {_, true} -> [{<<"Connection">>, <<"keep-alive">>} | Headers2]; {_, false} -> [{<<"Connection">>, <<"close">>} | Headers2] end, Version = case State#state.request_version of {1, 1} -> <<"HTTP/1.1 ">>; _ -> <<"HTTP/1.0 ">> end, H = [[Attr, <<": ">>, Val, <<"\r\n">>] || {Attr, Val} <- HeadersOut], NewReason = case Reason of <<"">> -> code_to_phrase(Status); _ -> Reason end, SL = [Version, integer_to_binary(Status), <<" ">>, NewReason, <<"\r\n">>], [SL, H, <<"\r\n">>]. make_xhtml_output(State, Status, Headers, XHTML) -> Data = case State#state.request_method of 'HEAD' -> <<"">>; _ -> DocType = case lists:member(html, Headers) of true -> ?HTML_DOCTYPE; false -> ?XHTML_DOCTYPE end, iolist_to_binary([DocType, fxml:element_to_binary(XHTML)]) end, EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Data), [EncodedHdrs, Data]. make_text_output(State, Status, Headers, Text) -> make_text_output(State, Status, <<"">>, Headers, Text). make_text_output(State, Status, Reason, Headers, Text) -> Data = iolist_to_binary(Text), Data2 = case State#state.request_method of 'HEAD' -> <<"">>; _ -> Data end, EncodedHdrs = make_headers(State, Status, Reason, Headers, Data2), [EncodedHdrs, Data2]. make_file_output(State, Status, Headers, FileName) -> case file:read_file_info(FileName) of {ok, #file_info{size = Size}} when State#state.request_method == 'HEAD' -> make_headers(State, Status, <<"">>, Headers, Size); {ok, #file_info{size = Size}} -> case file:open(FileName, [raw, read]) of {ok, Fd} -> EncodedHdrs = make_headers(State, Status, <<"">>, Headers, Size), send_text(State, EncodedHdrs), send_file(State, Fd, Size, FileName), file:close(Fd), none; {error, Why} -> Reason = file_format_error(Why), ?ERROR_MSG("Failed to open ~ts: ~ts", [FileName, Reason]), make_text_output(State, 404, Reason, [], <<>>) end; {error, Why} -> Reason = file_format_error(Why), ?ERROR_MSG("Failed to read info of ~ts: ~ts", [FileName, Reason]), make_text_output(State, 404, Reason, [], <<>>) end. parse_lang(Langs) -> case str:tokens(Langs, <<",; ">>) of [First | _] -> First; [] -> <<"en">> end. file_format_error(Reason) -> case file:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); Text -> Text end. url_decode_q_split_normalize(Path) -> {NPath, Query} = url_decode_q_split(Path), LPath = normalize_path([NPE || NPE <- str:tokens(path_decode(NPath), <<"/">>)]), {LPath, Query}. % Code below is taken (with some modifications) from the yaws webserver, which % is distributed under the following license: % % This software (the yaws webserver) is free software. % Parts of this software is Copyright (c) Claes Wikstrom % Any use or misuse of the source code is hereby freely allowed. % % 1. Redistributions of source code must retain the above copyright % notice as well as this list of conditions. % % 2. Redistributions in binary form must reproduce the above copyright % notice as well as this list of conditions. %% @doc Split the URL and return {Path, QueryPart} url_decode_q_split(Path) -> url_decode_q_split(Path, <<>>). url_decode_q_split(<<$?, T/binary>>, Acc) -> %% Don't decode the query string here, that is parsed separately. {path_norm_reverse(Acc), T}; url_decode_q_split(<>, Acc) when H /= 0 -> url_decode_q_split(T, <>); url_decode_q_split(<<>>, Ack) -> {path_norm_reverse(Ack), <<>>}. %% @doc Decode a part of the URL and return string() path_decode(Path) -> path_decode(Path, <<>>). path_decode(<<$%, Hi, Lo, Tail/binary>>, Acc) -> Hex = list_to_integer([Hi, Lo], 16), if Hex == 0 -> exit(badurl); true -> ok end, path_decode(Tail, <>); path_decode(<>, Acc) when H /= 0 -> path_decode(T, <>); path_decode(<<>>, Acc) -> Acc. path_norm_reverse(<<"/", T/binary>>) -> start_dir(0, <<"/">>, T); path_norm_reverse(T) -> start_dir(0, <<"">>, T). start_dir(N, Path, <<"..">>) -> rest_dir(N, Path, <<"">>); start_dir(N, Path, <<"/", T/binary>>) -> start_dir(N, Path, T); start_dir(N, Path, <<"./", T/binary>>) -> start_dir(N, Path, T); start_dir(N, Path, <<"../", T/binary>>) -> start_dir(N + 1, Path, T); start_dir(N, Path, T) -> rest_dir(N, Path, T). rest_dir(_N, Path, <<>>) -> case Path of <<>> -> <<"/">>; _ -> Path end; rest_dir(0, Path, <<$/, T/binary>>) -> start_dir(0, <<$/, Path/binary>>, T); rest_dir(N, Path, <<$/, T/binary>>) -> start_dir(N - 1, Path, T); rest_dir(0, Path, <>) -> rest_dir(0, <>, T); rest_dir(N, Path, <<_H, T/binary>>) -> rest_dir(N, Path, T). code_to_phrase(100) -> <<"Continue">>; code_to_phrase(101) -> <<"Switching Protocols ">>; code_to_phrase(200) -> <<"OK">>; code_to_phrase(201) -> <<"Created">>; code_to_phrase(202) -> <<"Accepted">>; code_to_phrase(203) -> <<"Non-Authoritative Information">>; code_to_phrase(204) -> <<"No Content">>; code_to_phrase(205) -> <<"Reset Content">>; code_to_phrase(206) -> <<"Partial Content">>; code_to_phrase(300) -> <<"Multiple Choices">>; code_to_phrase(301) -> <<"Moved Permanently">>; code_to_phrase(302) -> <<"Found">>; code_to_phrase(303) -> <<"See Other">>; code_to_phrase(304) -> <<"Not Modified">>; code_to_phrase(305) -> <<"Use Proxy">>; code_to_phrase(306) -> <<"(Unused)">>; code_to_phrase(307) -> <<"Temporary Redirect">>; code_to_phrase(400) -> <<"Bad Request">>; code_to_phrase(401) -> <<"Unauthorized">>; code_to_phrase(402) -> <<"Payment Required">>; code_to_phrase(403) -> <<"Forbidden">>; code_to_phrase(404) -> <<"Not Found">>; code_to_phrase(405) -> <<"Method Not Allowed">>; code_to_phrase(406) -> <<"Not Acceptable">>; code_to_phrase(407) -> <<"Proxy Authentication Required">>; code_to_phrase(408) -> <<"Request Timeout">>; code_to_phrase(409) -> <<"Conflict">>; code_to_phrase(410) -> <<"Gone">>; code_to_phrase(411) -> <<"Length Required">>; code_to_phrase(412) -> <<"Precondition Failed">>; code_to_phrase(413) -> <<"Request Entity Too Large">>; code_to_phrase(414) -> <<"Request-URI Too Long">>; code_to_phrase(415) -> <<"Unsupported Media Type">>; code_to_phrase(416) -> <<"Requested Range Not Satisfiable">>; code_to_phrase(417) -> <<"Expectation Failed">>; code_to_phrase(500) -> <<"Internal Server Error">>; code_to_phrase(501) -> <<"Not Implemented">>; code_to_phrase(502) -> <<"Bad Gateway">>; code_to_phrase(503) -> <<"Service Unavailable">>; code_to_phrase(504) -> <<"Gateway Timeout">>; code_to_phrase(505) -> <<"HTTP Version Not Supported">>. -spec parse_auth(binary()) -> {binary(), binary()} | {oauth, binary(), []} | invalid. parse_auth(<<"Basic ", Auth64/binary>>) -> try base64:decode(Auth64) of Auth -> case binary:split(Auth, <<":">>) of [User, Pass] -> PassUtf8 = unicode:characters_to_binary(Pass, utf8), {User, PassUtf8}; _ -> invalid end catch _:_ -> invalid end; parse_auth(<<"Bearer ", SToken/binary>>) -> Token = str:strip(SToken), {oauth, Token, []}; parse_auth(<<_/binary>>) -> invalid. parse_urlencoded(S) -> parse_urlencoded(S, nokey, <<>>, key). parse_urlencoded(<<$%, Hi, Lo, Tail/binary>>, Last, Cur, State) -> Hex = list_to_integer([Hi, Lo], 16), parse_urlencoded(Tail, Last, <>, State); parse_urlencoded(<<$&, Tail/binary>>, _Last, Cur, key) -> [{Cur, <<"">>} | parse_urlencoded(Tail, nokey, <<>>, key)]; %% cont keymode parse_urlencoded(<<$&, Tail/binary>>, Last, Cur, value) -> V = {Last, Cur}, [V | parse_urlencoded(Tail, nokey, <<>>, key)]; parse_urlencoded(<<$+, Tail/binary>>, Last, Cur, State) -> parse_urlencoded(Tail, Last, <>, State); parse_urlencoded(<<$=, Tail/binary>>, _Last, Cur, key) -> parse_urlencoded(Tail, Cur, <<>>, value); %% change mode parse_urlencoded(<>, Last, Cur, State) -> parse_urlencoded(Tail, Last, <>, State); parse_urlencoded(<<>>, Last, Cur, _State) -> [{Last, Cur}]; parse_urlencoded(undefined, _, _, _) -> []. apply_custom_headers(Headers, CustomHeaders) -> {Doctype, Headers2} = case Headers -- [html] of Headers -> {[], Headers}; Other -> {[html], Other} end, M = maps:merge(maps:from_list(Headers2), maps:from_list(CustomHeaders)), Doctype ++ maps:to_list(M). % The following code is mostly taken from yaws_ssl.erl toupper(C) when C >= $a andalso C =< $z -> C - 32; toupper(C) -> C. tolower(C) when C >= $A andalso C =< $Z -> C + 32; tolower(C) -> C. normalize_header_name(Name) -> normalize_header_name(Name, [], true). normalize_header_name(<<"">>, Acc, _) -> iolist_to_binary(Acc); normalize_header_name(<<"-", Rest/binary>>, Acc, _) -> normalize_header_name(Rest, [Acc, "-"], true); normalize_header_name(<>, Acc, true) -> normalize_header_name(Rest, [Acc, toupper(C)], false); normalize_header_name(<>, Acc, false) -> normalize_header_name(Rest, [Acc, tolower(C)], false). normalize_path(Path) -> normalize_path(Path, []). normalize_path([], Norm) -> lists:reverse(Norm); normalize_path([<<"..">>|Path], Norm) -> normalize_path(Path, Norm); normalize_path([_Parent, <<"..">>|Path], Norm) -> normalize_path(Path, Norm); normalize_path([Part | Path], Norm) -> normalize_path(Path, [Part|Norm]). listen_opt_type(tag) -> econf:binary(); listen_opt_type(request_handlers) -> econf:map( econf:and_then( econf:binary(), fun(Path) -> str:tokens(Path, <<"/">>) end), econf:beam([[{socket_handoff, 3}, {process, 2}]])); listen_opt_type(default_host) -> econf:domain(); listen_opt_type(custom_headers) -> econf:map( econf:binary(), econf:and_then( econf:binary(), fun(V) -> misc:expand_keyword(<<"@VERSION@">>, V, ejabberd_option:version()) end)). listen_options() -> [{ciphers, undefined}, {dhfile, undefined}, {cafile, undefined}, {protocol_options, undefined}, {tls, false}, {tls_compression, false}, {request_handlers, []}, {tag, <<>>}, {default_host, undefined}, {custom_headers, []}]. ejabberd-21.12/src/mod_conversejs.erl0000644000232200023220000001340714154362354020152 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_conversejs.erl %%% Author : Alexey Shchepin %%% Purpose : Serve simple page for Converse.js XMPP web browser client %%% Created : 8 Nov 2021 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_conversejs). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process/2, depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). -include("ejabberd_web_admin.hrl"). start(_Host, _Opts) -> ok. stop(_Host) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. process([], #request{method = 'GET'}) -> Host = ejabberd_config:get_myname(), Domain = gen_mod:get_module_opt(Host, ?MODULE, default_domain), Script = gen_mod:get_module_opt(Host, ?MODULE, conversejs_script), CSS = gen_mod:get_module_opt(Host, ?MODULE, conversejs_css), Init = [{<<"discover_connection_methods">>, false}, {<<"jid">>, Domain}, {<<"default_domain">>, Domain}, {<<"domain_placeholder">>, Domain}, {<<"view_mode">>, <<"fullscreen">>}], Init2 = case gen_mod:get_module_opt(Host, ?MODULE, websocket_url) of undefined -> Init; WSURL -> [{<<"websocket_url">>, WSURL} | Init] end, Init3 = case gen_mod:get_module_opt(Host, ?MODULE, bosh_service_url) of undefined -> Init2; BoshURL -> [{<<"bosh_service_url">>, BoshURL} | Init2] end, {200, [html], [<<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>, <<"">>]}; process(_, _) -> ejabberd_web:error(not_found). mod_opt_type(bosh_service_url) -> econf:either(undefined, econf:binary()); mod_opt_type(websocket_url) -> econf:either(undefined, econf:binary()); mod_opt_type(conversejs_script) -> econf:binary(); mod_opt_type(conversejs_css) -> econf:binary(); mod_opt_type(default_domain) -> econf:binary(). mod_options(_) -> [{bosh_service_url, undefined}, {websocket_url, undefined}, {default_domain, ejabberd_config:get_myname()}, {conversejs_script, <<"https://cdn.conversejs.org/dist/converse.min.js">>}, {conversejs_css, <<"https://cdn.conversejs.org/dist/converse.min.css">>}]. mod_doc() -> #{desc => [?T("This module serves a simple page for the " "https://conversejs.org/[Converse] XMPP web browser client."), "", ?T("This module is available since ejabberd 21.12."), "", ?T("To use this module, in addition to adding it to the 'modules' " "section, you must also enable it in 'listen' -> 'ejabberd_http' -> " "http://../listen-options/#request-handlers[request_handlers]."), "", ?T("You must also setup either the option 'websocket_url' or 'bosh_service_url'."), "", ?T("By default, the options 'conversejs_css' and 'conversejs_script'" " point to the public Converse.js client. Alternatively, you can" " host the client locally using _`mod_http_fileserver`_.") ], example => ["listen:", " -", " port: 5280", " module: ejabberd_http", " request_handlers:", " /websocket: ejabberd_http_ws", " /conversejs: mod_conversejs", "", "modules:", " mod_conversejs:", " websocket_url: \"ws://example.org:5280/websocket\""], opts => [{websocket_url, #{value => ?T("WebsocketURL"), desc => ?T("A websocket URL to which Converse.js can connect to.")}}, {bosh_service_url, #{value => ?T("BoshURL"), desc => ?T("BOSH service URL to which Converse.js can connect to.")}}, {default_domain, #{value => ?T("Domain"), desc => ?T("Specify a domain to act as the default for user JIDs. " "The default value is the first domain defined in the " "ejabberd configuration file.")}}, {conversejs_script, #{value => ?T("URL"), desc => ?T("Converse.js main script URL.")}}, {conversejs_css, #{value => ?T("URL"), desc => ?T("Converse.js CSS URL.")}}] }. ejabberd-21.12/src/mod_block_strangers.erl0000644000232200023220000002555214154362354021157 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_block_strangers.erl %%% Author : Alexey Shchepin %%% Purpose : Block packets from non-subscribers %%% Created : 25 Dec 2016 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_block_strangers). -author('alexey@process-one.net'). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, mod_doc/0, depends/2, mod_opt_type/1, mod_options/1]). -export([filter_packet/1, filter_offline_msg/1, filter_subscription/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -define(SETS, gb_sets). -type c2s_state() :: ejabberd_c2s:state(). %%%=================================================================== %%% Callbacks and hooks %%%=================================================================== start(Host, _Opts) -> ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, filter_packet, 25), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, filter_subscription, 25), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, filter_offline_msg, 25). stop(Host) -> ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, filter_packet, 25), ejabberd_hooks:delete(roster_in_subscription, Host, ?MODULE, filter_subscription, 25), ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, filter_offline_msg, 25). reload(_Host, _NewOpts, _OldOpts) -> ok. -spec filter_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()} | {stop, {drop, c2s_state()}}. filter_packet({#message{from = From} = Msg, State} = Acc) -> LFrom = jid:tolower(From), LBFrom = jid:remove_resource(LFrom), #{pres_a := PresA} = State, case (?SETS):is_element(LFrom, PresA) orelse (?SETS):is_element(LBFrom, PresA) orelse sets_bare_member(LBFrom, PresA) of false -> case check_message(Msg) of allow -> Acc; deny -> {stop, {drop, State}} end; true -> Acc end; filter_packet(Acc) -> Acc. -spec filter_offline_msg({_, message()}) -> {_, message()} | {stop, {drop, message()}}. filter_offline_msg({_Action, #message{} = Msg} = Acc) -> case check_message(Msg) of allow -> Acc; deny -> {stop, {drop, Msg}} end. -spec filter_subscription(boolean(), presence()) -> boolean() | {stop, false}. filter_subscription(Acc, #presence{meta = #{captcha := passed}}) -> Acc; filter_subscription(Acc, #presence{from = From, to = To, lang = Lang, id = SID, type = subscribe} = Pres) -> LServer = To#jid.lserver, case mod_block_strangers_opt:drop(LServer) andalso mod_block_strangers_opt:captcha(LServer) andalso need_check(Pres) of true -> case check_subscription(From, To) of false -> BFrom = jid:remove_resource(From), BTo = jid:remove_resource(To), Limiter = jid:tolower(BFrom), case ejabberd_captcha:create_captcha( SID, BTo, BFrom, Lang, Limiter, fun(Res) -> handle_captcha_result(Res, Pres) end) of {ok, ID, Body, CaptchaEls} -> Msg = #message{from = BTo, to = From, id = ID, body = Body, sub_els = CaptchaEls}, case mod_block_strangers_opt:log(LServer) of true -> ?INFO_MSG("Challenge subscription request " "from stranger ~ts to ~ts with " "CAPTCHA", [jid:encode(From), jid:encode(To)]); false -> ok end, ejabberd_router:route(Msg); {error, limit} -> ErrText = ?T("Too many CAPTCHA requests"), Err = xmpp:err_resource_constraint(ErrText, Lang), ejabberd_router:route_error(Pres, Err); _ -> ErrText = ?T("Unable to generate a CAPTCHA"), Err = xmpp:err_internal_server_error(ErrText, Lang), ejabberd_router:route_error(Pres, Err) end, {stop, false}; true -> Acc end; false -> Acc end; filter_subscription(Acc, _) -> Acc. -spec handle_captcha_result(captcha_succeed | captcha_failed, presence()) -> ok. handle_captcha_result(captcha_succeed, Pres) -> Pres1 = xmpp:put_meta(Pres, captcha, passed), ejabberd_router:route(Pres1); handle_captcha_result(captcha_failed, #presence{lang = Lang} = Pres) -> Txt = ?T("The CAPTCHA verification has failed"), ejabberd_router:route_error(Pres, xmpp:err_not_allowed(Txt, Lang)). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec check_message(message()) -> allow | deny. check_message(#message{from = From, to = To, lang = Lang} = Msg) -> LServer = To#jid.lserver, case need_check(Msg) of true -> case check_subscription(From, To) of false -> Drop = mod_block_strangers_opt:drop(LServer), Log = mod_block_strangers_opt:log(LServer), if Log -> ?INFO_MSG("~ts message from stranger ~ts to ~ts", [if Drop -> "Rejecting"; true -> "Allow" end, jid:encode(From), jid:encode(To)]); true -> ok end, if Drop -> Txt = ?T("Messages from strangers are rejected"), Err = xmpp:err_policy_violation(Txt, Lang), Msg1 = maybe_adjust_from(Msg), ejabberd_router:route_error(Msg1, Err), deny; true -> allow end; true -> allow end; false -> allow end. -spec maybe_adjust_from(message()) -> message(). maybe_adjust_from(#message{type = groupchat, from = From} = Msg) -> Msg#message{from = jid:remove_resource(From)}; maybe_adjust_from(#message{} = Msg) -> Msg. -spec need_check(presence() | message()) -> boolean(). need_check(Pkt) -> To = xmpp:get_to(Pkt), From = xmpp:get_from(Pkt), IsSelf = To#jid.luser == From#jid.luser andalso To#jid.lserver == From#jid.lserver, LServer = To#jid.lserver, IsEmpty = case Pkt of #message{body = [], subject = []} -> true; _ -> false end, IsError = (error == xmpp:get_type(Pkt)), AllowLocalUsers = mod_block_strangers_opt:allow_local_users(LServer), Access = mod_block_strangers_opt:access(LServer), not (IsSelf orelse IsEmpty orelse IsError orelse acl:match_rule(LServer, Access, From) == allow orelse ((AllowLocalUsers orelse From#jid.luser == <<"">>) andalso ejabberd_router:is_my_host(From#jid.lserver))). -spec check_subscription(jid(), jid()) -> boolean(). check_subscription(From, To) -> LocalServer = To#jid.lserver, {RemoteUser, RemoteServer, _} = jid:tolower(From), case mod_roster:is_subscribed(From, To) of false when RemoteUser == <<"">> -> false; false -> %% Check if the contact's server is in the roster mod_block_strangers_opt:allow_transports(LocalServer) andalso mod_roster:is_subscribed(jid:make(RemoteServer), To); true -> true end. -spec sets_bare_member(ljid(), ?SETS:set()) -> boolean(). sets_bare_member({U, S, <<"">>} = LBJID, Set) -> case ?SETS:next(?SETS:iterator_from(LBJID, Set)) of {{U, S, _}, _} -> true; _ -> false end. depends(_Host, _Opts) -> []. mod_opt_type(access) -> econf:acl(); mod_opt_type(drop) -> econf:bool(); mod_opt_type(log) -> econf:bool(); mod_opt_type(captcha) -> econf:bool(); mod_opt_type(allow_local_users) -> econf:bool(); mod_opt_type(allow_transports) -> econf:bool(). mod_options(_) -> [{access, none}, {drop, true}, {log, false}, {captcha, false}, {allow_local_users, true}, {allow_transports, true}]. mod_doc() -> #{desc => ?T("This module allows to block/log messages coming from an " "unknown entity. If a writing entity is not in your roster, " "you can let this module drop and/or log the message. " "By default you'll just not receive message from that entity. " "Enable this module if you want to drop SPAM messages."), opts => [{access, #{value => ?T("AccessName"), desc => ?T("The option is supposed to be used when 'allow_local_users' " "and 'allow_transports' are not enough. It's an ACL where " "'deny' means the message will be rejected (or a CAPTCHA " "would be generated for a presence, if configured), and " "'allow' means the sender is whitelisted and the stanza " "will pass through. The default value is 'none', which " "means nothing is whitelisted.")}}, {drop, #{value => "true | false", desc => ?T("This option specifies if strangers messages should " "be dropped or not. The default value is 'true'.")}}, {log, #{value => "true | false", desc => ?T("This option specifies if strangers' messages should " "be logged (as info message) in ejabberd.log. " "The default value is 'false'.")}}, {allow_local_users, #{value => "true | false", desc => ?T("This option specifies if strangers from the same " "local host should be accepted or not. " "The default value is 'true'.")}}, {allow_transports, #{value => "true | false", desc => ?T("If set to 'true' and some server's JID is in user's " "roster, then messages from any user of this server " "are accepted even if no subscription present. " "The default value is 'true'.")}}, {captcha, #{value => "true | false", desc => ?T("Whether to generate CAPTCHA or not in response to " "messages from strangers. See also section " "https://docs.ejabberd.im/admin/configuration/#captcha" "[CAPTCHA] of the Configuration Guide. " "The default value is 'false'.")}}]}. ejabberd-21.12/src/ext_mod.erl0000644000232200023220000006353514154362354016600 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ext_mod.erl %%% Author : Christophe Romain %%% Purpose : external modules management %%% Created : 19 Feb 2015 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2006-2021 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(ext_mod). -behaviour(gen_server). -author("Christophe Romain "). -export([start_link/0, update/0, check/1, available_command/0, available/0, available/1, installed_command/0, installed/0, installed/1, install/1, uninstall/1, upgrade/0, upgrade/1, add_paths/0, add_sources/1, add_sources/2, del_sources/1, modules_dir/0, config_dir/0, get_commands_spec/0]). -export([modules_configs/0, module_ebin_dir/1]). -export([compile_erlang_file/2, compile_elixir_file/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -define(REPOS, "https://github.com/processone/ejabberd-contrib"). -record(state, {}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> process_flag(trap_exit, true), add_paths(), application:start(inets), inets:start(httpc, [{profile, ext_mod}]), ejabberd_commands:register_commands(get_commands_spec()), {ok, #state{}}. add_paths() -> [code:add_patha(module_ebin_dir(Module)) || {Module, _} <- installed()]. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_commands:unregister_commands(get_commands_spec()). code_change(_OldVsn, State, _Extra) -> {ok, State}. %% -- ejabberd commands get_commands_spec() -> [#ejabberd_commands{name = modules_update_specs, tags = [modules], desc = "Update the module source code from Git", longdesc = "A connection to Internet is required", module = ?MODULE, function = update, args = [], result = {res, rescode}}, #ejabberd_commands{name = modules_available, tags = [modules], desc = "List the contributed modules available to install", module = ?MODULE, function = available_command, result_desc = "List of tuples with module name and description", result_example = [{mod_cron, "Execute scheduled commands"}, {mod_rest, "ReST frontend"}], args = [], result = {modules, {list, {module, {tuple, [{name, atom}, {summary, string}]}}}}}, #ejabberd_commands{name = modules_installed, tags = [modules], desc = "List the contributed modules already installed", module = ?MODULE, function = installed_command, result_desc = "List of tuples with module name and description", result_example = [{mod_cron, "Execute scheduled commands"}, {mod_rest, "ReST frontend"}], args = [], result = {modules, {list, {module, {tuple, [{name, atom}, {summary, string}]}}}}}, #ejabberd_commands{name = module_install, tags = [modules], desc = "Compile, install and start an available contributed module", module = ?MODULE, function = install, args_desc = ["Module name"], args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_uninstall, tags = [modules], desc = "Uninstall a contributed module", module = ?MODULE, function = uninstall, args_desc = ["Module name"], args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_upgrade, tags = [modules], desc = "Upgrade the running code of an installed module", longdesc = "In practice, this uninstalls and installs the module", module = ?MODULE, function = upgrade, args_desc = ["Module name"], args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}}, #ejabberd_commands{name = module_check, tags = [modules], desc = "Check the contributed module repository compliance", module = ?MODULE, function = check, args_desc = ["Module name"], args_example = [<<"mod_rest">>], args = [{module, binary}], result = {res, rescode}} ]. %% -- public modules functions update() -> Contrib = maps:put(?REPOS, [], maps:new()), Jungles = lists:foldl(fun({Package, Spec}, Acc) -> Repo = proplists:get_value(url, Spec, ""), Mods = maps:get(Repo, Acc, []), maps:put(Repo, [Package|Mods], Acc) end, Contrib, modules_spec(sources_dir(), "*/*")), Repos = maps:fold(fun(Repo, _Mods, Acc) -> Update = add_sources(Repo), ?INFO_MSG("Update packages from repo ~ts: ~p", [Repo, Update]), case Update of ok -> Acc; Error -> [{repository, Repo, Error}|Acc] end end, [], Jungles), Res = lists:foldl(fun({Package, Spec}, Acc) -> Repo = proplists:get_value(url, Spec, ""), Update = add_sources(Package, Repo), ?INFO_MSG("Update package ~ts: ~p", [Package, Update]), case Update of ok -> Acc; Error -> [{Package, Repo, Error}|Acc] end end, Repos, modules_spec(sources_dir(), "*")), case Res of [] -> ok; [Error|_] -> Error end. available() -> Jungle = modules_spec(sources_dir(), "*/*"), Standalone = modules_spec(sources_dir(), "*"), lists:keysort(1, lists:foldl(fun({Key, Val}, Acc) -> lists:keystore(Key, 1, Acc, {Key, Val}) end, Jungle, Standalone)). available(Module) when is_atom(Module) -> available(misc:atom_to_binary(Module)); available(Package) when is_binary(Package) -> Available = [misc:atom_to_binary(K) || K<-proplists:get_keys(available())], lists:member(Package, Available). available_command() -> [short_spec(Item) || Item <- available()]. installed() -> modules_spec(modules_dir(), "*"). installed(Module) when is_atom(Module) -> installed(misc:atom_to_binary(Module)); installed(Package) when is_binary(Package) -> Installed = [misc:atom_to_binary(K) || K<-proplists:get_keys(installed())], lists:member(Package, Installed). installed_command() -> [short_spec(Item) || Item <- installed()]. install(Module) when is_atom(Module) -> install(misc:atom_to_binary(Module)); install(Package) when is_binary(Package) -> Spec = [S || {Mod, S} <- available(), misc:atom_to_binary(Mod)==Package], case {Spec, installed(Package), is_contrib_allowed()} of {_, _, false} -> {error, not_allowed}; {[], _, _} -> {error, not_available}; {_, true, _} -> {error, conflict}; {[Attrs], _, _} -> Module = misc:binary_to_atom(Package), case compile_and_install(Module, Attrs) of ok -> code:add_patha(module_ebin_dir(Module)), ejabberd_config:reload(), case erlang:function_exported(Module, post_install, 0) of true -> Module:post_install(); _ -> ok end; Error -> delete_path(module_lib_dir(Module)), Error end end. uninstall(Module) when is_atom(Module) -> uninstall(misc:atom_to_binary(Module)); uninstall(Package) when is_binary(Package) -> case installed(Package) of true -> Module = misc:binary_to_atom(Package), case erlang:function_exported(Module, pre_uninstall, 0) of true -> Module:pre_uninstall(); _ -> ok end, [catch gen_mod:stop_module(Host, Module) || Host <- ejabberd_option:hosts()], code:purge(Module), code:delete(Module), code:del_path(module_ebin_dir(Module)), delete_path(module_lib_dir(Module)), ejabberd_config:reload(); false -> {error, not_installed} end. upgrade() -> [{Package, upgrade(Package)} || {Package, _Spec} <- installed()]. upgrade(Module) when is_atom(Module) -> upgrade(misc:atom_to_binary(Module)); upgrade(Package) when is_binary(Package) -> uninstall(Package), install(Package). add_sources(Path) when is_list(Path) -> add_sources(iolist_to_binary(module_name(Path)), Path). add_sources(_, "") -> {error, no_url}; add_sources(Module, Path) when is_atom(Module), is_list(Path) -> add_sources(misc:atom_to_binary(Module), Path); add_sources(Package, Path) when is_binary(Package), is_list(Path) -> DestDir = sources_dir(), RepDir = filename:join(DestDir, module_name(Path)), delete_path(RepDir), case filelib:ensure_dir(RepDir) of ok -> case {string:left(Path, 4), string:right(Path, 2)} of {"http", "ip"} -> extract(zip, geturl(Path), DestDir); {"http", "gz"} -> extract(tar, geturl(Path), DestDir); {"http", _} -> extract_url(Path, DestDir); {"git@", _} -> extract_github_master(Path, DestDir); {_, "ip"} -> extract(zip, Path, DestDir); {_, "gz"} -> extract(tar, Path, DestDir); _ -> {error, unsupported_source} end; Error -> Error end. del_sources(Module) when is_atom(Module) -> del_sources(misc:atom_to_binary(Module)); del_sources(Package) when is_binary(Package) -> case uninstall(Package) of ok -> SrcDir = module_src_dir(misc:binary_to_atom(Package)), delete_path(SrcDir); Error -> Error end. check(Module) when is_atom(Module) -> check(misc:atom_to_binary(Module)); check(Package) when is_binary(Package) -> case {available(Package), installed(Package)} of {false, _} -> {error, not_available}; {_, false} -> Status = install(Package), uninstall(Package), case Status of ok -> check_sources(misc:binary_to_atom(Package)); Error -> Error end; _ -> check_sources(misc:binary_to_atom(Package)) end. %% -- archives and variables functions geturl(Url) -> case getenv("PROXY_SERVER", "", ":") of [H, Port] -> httpc:set_options([{proxy, {{H, list_to_integer(Port)}, []}}], ext_mod); [H] -> httpc:set_options([{proxy, {{H, 8080}, []}}], ext_mod); _ -> ok end, User = case getenv("PROXY_USER", "", ":") of [U, Pass] -> [{proxy_auth, {U, Pass}}]; _ -> [] end, case httpc:request(get, {Url, []}, User, [{body_format, binary}], ext_mod) of {ok, {{_, 200, _}, Headers, Response}} -> {ok, Headers, Response}; {ok, {{_, Code, _}, _Headers, Response}} -> {error, {Code, Response}}; {error, Reason} -> {error, Reason} end. getenv(Env) -> getenv(Env, ""). getenv(Env, Default) -> case os:getenv(Env) of false -> Default; "" -> Default; Value -> Value end. getenv(Env, Default, Separator) -> string:tokens(getenv(Env, Default), Separator). extract(zip, {ok, _, Body}, DestDir) -> extract(zip, iolist_to_binary(Body), DestDir); extract(tar, {ok, _, Body}, DestDir) -> extract(tar, {binary, iolist_to_binary(Body)}, DestDir); extract(_, {error, Reason}, _) -> {error, Reason}; extract(zip, Zip, DestDir) -> case zip:extract(Zip, [{cwd, DestDir}]) of {ok, _} -> ok; Error -> Error end; extract(tar, Tar, DestDir) -> erl_tar:extract(Tar, [compressed, {cwd, DestDir}]). extract_url(Path, DestDir) -> hd([extract_github_master(Path, DestDir) || string:str(Path, "github") > 0] ++[{error, unsupported_source}]). extract_github_master(Repos, DestDir) -> Base = case string:tokens(Repos, ":") of ["git@github.com", T1] -> "https://github.com/"++T1; _ -> Repos end, Url = case lists:reverse(Base) of [$t,$i,$g,$.|T2] -> lists:reverse(T2); _ -> Base end, case extract(zip, geturl(Url++"/archive/master.zip"), DestDir) of ok -> RepDir = filename:join(DestDir, module_name(Repos)), file:rename(RepDir++"-master", RepDir); Error -> Error end. copy(From, To) -> case filelib:is_dir(From) of true -> Copy = fun(F) -> SubFrom = filename:join(From, F), SubTo = filename:join(To, F), copy(SubFrom, SubTo) end, lists:foldl(fun(ok, ok) -> ok; (ok, Error) -> Error; (Error, _) -> Error end, ok, [Copy(filename:basename(X)) || X<-filelib:wildcard(From++"/*")]); false -> filelib:ensure_dir(To), case file:copy(From, To) of {ok, _} -> ok; Error -> Error end end. delete_path(Path) -> case filelib:is_dir(Path) of true -> [delete_path(SubPath) || SubPath <- filelib:wildcard(Path++"/*")], file:del_dir(Path); false -> file:delete(Path) end. modules_dir() -> DefaultDir = filename:join(getenv("HOME"), ".ejabberd-modules"), getenv("CONTRIB_MODULES_PATH", DefaultDir). sources_dir() -> filename:join(modules_dir(), "sources"). config_dir() -> DefaultDir = filename:join(modules_dir(), "conf"), getenv("CONTRIB_MODULES_CONF_DIR", DefaultDir). -spec modules_configs() -> [binary()]. modules_configs() -> Fs = [{filename:rootname(filename:basename(F)), F} || F <- filelib:wildcard(config_dir() ++ "/*.{yml,yaml}") ++ filelib:wildcard(modules_dir() ++ "/*/conf/*.{yml,yaml}")], [unicode:characters_to_binary(proplists:get_value(F, Fs)) || F <- proplists:get_keys(Fs)]. module_lib_dir(Package) -> filename:join(modules_dir(), Package). module_ebin_dir(Package) -> filename:join(module_lib_dir(Package), "ebin"). module_src_dir(Package) -> Rep = module_name(Package), SrcDir = sources_dir(), Standalone = filelib:wildcard(Rep, SrcDir), Jungle = filelib:wildcard("*/"++Rep, SrcDir), case Standalone++Jungle of [RepDir|_] -> filename:join(SrcDir, RepDir); _ -> filename:join(SrcDir, Rep) end. module_name(Id) -> filename:basename(filename:rootname(Id)). module(Id) -> misc:binary_to_atom(iolist_to_binary(module_name(Id))). module_spec(Spec) -> [{path, filename:dirname(Spec)} | case consult(Spec) of {ok, Meta} -> Meta; _ -> [] end]. modules_spec(Dir, Path) -> Wildcard = filename:join(Path, "*.spec"), lists:sort( [{module(Match), module_spec(filename:join(Dir, Match))} || Match <- filelib:wildcard(Wildcard, Dir)]). short_spec({Module, Attrs}) when is_atom(Module), is_list(Attrs) -> {Module, proplists:get_value(summary, Attrs, "")}. is_contrib_allowed() -> ejabberd_option:allow_contrib_modules(). %% -- build functions check_sources(Module) -> SrcDir = module_src_dir(Module), SpecFile = filename:flatten([Module, ".spec"]), {ok, Dir} = file:get_cwd(), file:set_cwd(SrcDir), HaveSrc = case filelib:is_dir("src") or filelib:is_dir("lib") of true -> []; false -> [{missing, "src (Erlang) or lib (Elixir) sources directory"}] end, DirCheck = lists:foldl( fun({Type, Name}, Acc) -> case filelib:Type(Name) of true -> Acc; false -> [{missing, Name}|Acc] end end, HaveSrc, [{is_file, "README.txt"}, {is_file, "COPYING"}, {is_file, SpecFile}]), SpecCheck = case consult(SpecFile) of {ok, Spec} -> lists:foldl( fun(Key, Acc) -> case lists:keysearch(Key, 1, Spec) of false -> [{missing_meta, Key}|Acc]; {value, {Key, [_NoEmpty|_]}} -> Acc; {value, {Key, Val}} -> [{invalid_meta, {Key, Val}}|Acc] end end, [], [author, summary, home, url]); {error, Error} -> [{invalid_spec, Error}] end, file:set_cwd(Dir), Result = DirCheck ++ SpecCheck, case Result of [] -> ok; _ -> {error, Result} end. compile_and_install(Module, Spec) -> SrcDir = module_src_dir(Module), LibDir = module_lib_dir(Module), case filelib:is_dir(SrcDir) of true -> case compile_deps(SrcDir) of ok -> case compile(SrcDir) of ok -> install(Module, Spec, SrcDir, LibDir); Error -> Error end; Error -> Error end; false -> Path = proplists:get_value(url, Spec, ""), case add_sources(Module, Path) of ok -> compile_and_install(Module, Spec); Error -> Error end end. compile_deps(LibDir) -> Deps = filename:join(LibDir, "deps"), case filelib:is_dir(Deps) of true -> ok; % assume deps are included false -> fetch_rebar_deps(LibDir) end, Rs = [compile(Dep) || Dep <- filelib:wildcard(filename:join(Deps, "*"))], compile_result(Rs). compile(LibDir) -> Bin = filename:join(LibDir, "ebin"), Inc = filename:join(LibDir, "include"), Lib = filename:join(LibDir, "lib"), Src = filename:join(LibDir, "src"), Options = [{outdir, Bin}, {i, Inc} | compile_options()], filelib:ensure_dir(filename:join(Bin, ".")), [copy(App, Bin) || App <- filelib:wildcard(Src++"/*.app")], Er = [compile_erlang_file(Bin, File, Options) || File <- filelib:wildcard(Src++"/*.erl")], Ex = [compile_elixir_file(Bin, File) || File <- filelib:wildcard(Lib ++ "/*.ex")], compile_result(Er++Ex). compile_result(Results) -> case lists:dropwhile( fun({ok, _}) -> true; (_) -> false end, Results) of [] -> ok; [Error|_] -> Error end. maybe_define_lager_macro() -> case list_to_integer(erlang:system_info(otp_release)) < 22 of true -> [{d, 'LAGER'}]; false -> [] end. compile_options() -> [verbose, report_errors, report_warnings, debug_info, ?ALL_DEFS] ++ maybe_define_lager_macro() ++ [{i, filename:join(app_dir(App), "include")} || App <- [fast_xml, xmpp, p1_utils, ejabberd]] ++ [{i, filename:join(mod_dir(Mod), "include")} || Mod <- installed()]. app_dir(App) -> case code:lib_dir(App) of {error, bad_name} -> case code:which(App) of Beam when is_list(Beam) -> filename:dirname(filename:dirname(Beam)); _ -> "." end; Dir -> Dir end. mod_dir({Package, Spec}) -> Default = filename:join(modules_dir(), Package), proplists:get_value(path, Spec, Default). compile_erlang_file(Dest, File) -> compile_erlang_file(Dest, File, compile_options()). compile_erlang_file(Dest, File, ErlOptions) -> Options = [{outdir, Dest} | ErlOptions], case compile:file(File, Options) of {ok, Module} -> {ok, Module}; {ok, Module, _} -> {ok, Module}; {ok, Module, _, _} -> {ok, Module}; error -> {error, {compilation_failed, File}}; {error, E, W} -> {error, {compilation_failed, File, E, W}} end. -ifdef(ELIXIR_ENABLED). compile_elixir_file(Dest, File) when is_list(Dest) and is_list(File) -> compile_elixir_file(list_to_binary(Dest), list_to_binary(File)); compile_elixir_file(Dest, File) -> try 'Elixir.Kernel.ParallelCompiler':files_to_path([File], Dest, []) of [Module] -> {ok, Module} catch _ -> {error, {compilation_failed, File}} end. -else. compile_elixir_file(_, File) -> {error, {compilation_failed, File}}. -endif. install(Module, Spec, SrcDir, LibDir) -> {ok, CurDir} = file:get_cwd(), file:set_cwd(SrcDir), Files1 = [{File, copy(File, filename:join(LibDir, File))} || File <- filelib:wildcard("{ebin,priv,conf,include}/**")], Files2 = [{File, copy(File, filename:join(LibDir, filename:join(lists:nthtail(2,filename:split(File)))))} || File <- filelib:wildcard("deps/*/{ebin,priv}/**")], Errors = lists:dropwhile(fun({_, ok}) -> true; (_) -> false end, Files1++Files2), inform_module_configuration(Module, LibDir, Files1), Result = case Errors of [{F, {error, E}}|_] -> {error, {F, E}}; [] -> SpecPath = proplists:get_value(path, Spec), SpecFile = filename:flatten([Module, ".spec"]), copy(filename:join(SpecPath, SpecFile), filename:join(LibDir, SpecFile)) end, file:set_cwd(CurDir), Result. inform_module_configuration(Module, LibDir, Files1) -> Res = lists:filter(fun({[$c, $o, $n, $f |_], ok}) -> true; (_) -> false end, Files1), case Res of [{ConfigPath, ok}] -> FullConfigPath = filename:join(LibDir, ConfigPath), io:format("Module ~p has been installed and started.~n" "It's configured in the file:~n ~s~n" "Configure the module in that file, or remove it~n" "and configure in your main ejabberd.yml~n", [Module, FullConfigPath]); [] -> io:format("Module ~p has been installed.~n" "Now you can configure it in your ejabberd.yml~n", [Module]) end. %% -- minimalist rebar spec parser, only support git fetch_rebar_deps(SrcDir) -> case rebar_deps(filename:join(SrcDir, "rebar.config")) ++ rebar_deps(filename:join(SrcDir, "rebar.config.script")) of [] -> ok; Deps -> {ok, CurDir} = file:get_cwd(), file:set_cwd(SrcDir), filelib:ensure_dir(filename:join("deps", ".")), lists:foreach(fun({_App, Cmd}) -> os:cmd("cd deps; "++Cmd++"; cd ..") end, Deps), file:set_cwd(CurDir) end. rebar_deps(Script) -> case file:script(Script) of {ok, Config} when is_list(Config) -> [rebar_dep(Dep) || Dep <- proplists:get_value(deps, Config, [])]; {ok, {deps, Deps}} -> [rebar_dep(Dep) || Dep <- Deps]; _ -> [] end. rebar_dep({App, _, {git, Url}}) -> {App, "git clone "++Url++" "++filename:basename(App)}; rebar_dep({App, _, {git, Url, {branch, Ref}}}) -> {App, "git clone -n "++Url++" "++filename:basename(App)++ "; (cd "++filename:basename(App)++ "; git checkout -q origin/"++Ref++")"}; rebar_dep({App, _, {git, Url, {tag, Ref}}}) -> {App, "git clone -n "++Url++" "++filename:basename(App)++ "; (cd "++filename:basename(App)++ "; git checkout -q "++Ref++")"}; rebar_dep({App, _, {git, Url, Ref}}) -> {App, "git clone -n "++Url++" "++filename:basename(App)++ "; (cd "++filename:basename(App)++ "; git checkout -q "++Ref++")"}. %% -- YAML spec parser consult(File) -> case fast_yaml:decode_from_file(File, [plain_as_atom]) of {ok, []} -> {ok, []}; {ok, [Doc|_]} -> {ok, [format(Spec) || Spec <- Doc]}; {error, Err} -> {error, fast_yaml:format_error(Err)} end. format({Key, Val}) when is_binary(Val) -> {Key, binary_to_list(Val)}; format({Key, Val}) -> % TODO: improve Yaml parsing {Key, Val}. ejabberd-21.12/src/mod_mqtt_sql.erl0000644000232200023220000001253014154362354017631 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mod_mqtt_sql). -behaviour(mod_mqtt). %% API -export([init/2, publish/6, delete_published/2, lookup_published/2]). -export([list_topics/1]). %% Unsupported backend API -export([init/0]). -export([subscribe/4, unsubscribe/2, find_subscriber/2]). -export([open_session/1, close_session/1, lookup_session/1, get_sessions/2]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init() -> ?ERROR_MSG("Backend 'sql' is only supported for db_type", []), {error, db_failure}. init(_Host, _Opts) -> ok. publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> PayloadFormat = encode_pfi(maps:get(payload_format_indicator, Props, binary)), ResponseTopic = maps:get(response_topic, Props, <<"">>), CorrelationData = maps:get(correlation_data, Props, <<"">>), ContentType = maps:get(content_type, Props, <<"">>), UserProps = encode_props(maps:get(user_property, Props, [])), case ?SQL_UPSERT(LServer, "mqtt_pub", ["!topic=%(Topic)s", "!server_host=%(LServer)s", "username=%(U)s", "resource=%(R)s", "payload=%(Payload)s", "qos=%(QoS)d", "payload_format=%(PayloadFormat)d", "response_topic=%(ResponseTopic)s", "correlation_data=%(CorrelationData)s", "content_type=%(ContentType)s", "user_properties=%(UserProps)s", "expiry=%(ExpiryTime)d"]) of ok -> ok; _Err -> {error, db_failure} end. delete_published({_, LServer, _}, Topic) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from mqtt_pub where " "topic=%(Topic)s and %(LServer)H")) of {updated, _} -> ok; _Err -> {error, db_failure} end. lookup_published({_, LServer, _}, Topic) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(payload)s, @(qos)d, @(payload_format)d, " "@(content_type)s, @(response_topic)s, " "@(correlation_data)s, @(user_properties)s, @(expiry)d " "from mqtt_pub where topic=%(Topic)s and %(LServer)H")) of {selected, [{Payload, QoS, PayloadFormat, ContentType, ResponseTopic, CorrelationData, EncProps, Expiry}]} -> try decode_props(EncProps) of UserProps -> try decode_pfi(PayloadFormat) of PFI -> Props = #{payload_format_indicator => PFI, content_type => ContentType, response_topic => ResponseTopic, correlation_data => CorrelationData, user_property => UserProps}, {ok, {Payload, QoS, Props, Expiry}} catch _:badarg -> ?ERROR_MSG("Malformed value of 'payload_format' column " "for topic '~ts'", [Topic]), {error, db_failure} end catch _:badarg -> ?ERROR_MSG("Malformed value of 'user_properties' column " "for topic '~ts'", [Topic]), {error, db_failure} end; {selected, []} -> {error, notfound}; _ -> {error, db_failure} end. list_topics(LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(topic)s from mqtt_pub where %(LServer)H")) of {selected, Res} -> {ok, [Topic || {Topic} <- Res]}; _ -> {error, db_failure} end. open_session(_) -> erlang:nif_error(unsupported_db). close_session(_) -> erlang:nif_error(unsupported_db). lookup_session(_) -> erlang:nif_error(unsupported_db). get_sessions(_, _) -> erlang:nif_error(unsupported_db). subscribe(_, _, _, _) -> erlang:nif_error(unsupported_db). unsubscribe(_, _) -> erlang:nif_error(unsupported_db). find_subscriber(_, _) -> erlang:nif_error(unsupported_db). %%%=================================================================== %%% Internal functions %%%=================================================================== encode_pfi(binary) -> 0; encode_pfi(utf8) -> 1. decode_pfi(0) -> binary; decode_pfi(1) -> utf8. encode_props([]) -> <<"">>; encode_props(L) -> term_to_binary(L). decode_props(<<"">>) -> []; decode_props(Bin) -> binary_to_term(Bin). ejabberd-21.12/src/ejabberd_shaper.erl0000644000232200023220000002040514154362354020226 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_shaper). -behaviour(gen_server). -export([start_link/0, new/1, update/2, match/3, get_max_rate/1]). -export([reload_from_config/0]). -export([validator/1, shaper_rules_validator/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -type state() :: #{hosts := [binary()]}. -type shaper() :: none | p1_shaper:state(). -type shaper_rate() :: {pos_integer(), pos_integer()} | pos_integer() | infinity. -type shaper_rule() :: {atom() | pos_integer(), [acl:access_rule()]}. -type shaper_rate_rule() :: {shaper_rate(), [acl:access_rule()]}. -export_type([shaper/0, shaper_rule/0, shaper_rate/0]). %%%=================================================================== %%% API %%%=================================================================== -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec match(global | binary(), atom() | [shaper_rule()], jid:jid() | jid:ljid() | inet:ip_address() | acl:match()) -> none | shaper_rate(). match(_, none, _) -> none; match(_, infinity, _) -> infinity; match(Host, Shaper, Match) when is_map(Match) -> Rules = if is_atom(Shaper) -> read_shaper_rules(Shaper, Host); true -> Shaper end, Rate = acl:match_rules(Host, Rules, Match, none), read_shaper(Rate); match(Host, Shaper, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 -> match(Host, Shaper, #{ip => IP}); match(Host, Shaper, JID) -> match(Host, Shaper, #{usr => jid:tolower(JID)}). -spec get_max_rate(none | shaper_rate()) -> none | pos_integer(). get_max_rate({Rate, _}) -> Rate; get_max_rate(Rate) when is_integer(Rate), Rate > 0 -> Rate; get_max_rate(_) -> none. -spec new(none | shaper_rate()) -> shaper(). new({Rate, Burst}) -> p1_shaper:new(Rate, Burst); new(Rate) when is_integer(Rate), Rate > 0 -> p1_shaper:new(Rate); new(_) -> none. -spec update(shaper(), non_neg_integer()) -> {shaper(), non_neg_integer()}. update(none, _Size) -> {none, 0}; update(Shaper1, Size) -> Shaper2 = p1_shaper:update(Shaper1, Size), ?DEBUG("Shaper update:~n~ts =>~n~ts", [p1_shaper:pp(Shaper1), p1_shaper:pp(Shaper2)]), Shaper2. -spec validator(shaper | shaper_rules) -> econf:validator(). validator(shaper) -> econf:options( #{'_' => shaper_validator()}, [{disallowed, reserved()}, {return, map}, unique]); validator(shaper_rules) -> econf:options( #{'_' => shaper_rules_validator()}, [{disallowed, reserved()}, unique]). -spec shaper_rules_validator() -> econf:validator(). shaper_rules_validator() -> fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(shaper_name())(K), (acl:access_validator())(V)}; (N) -> {(shaper_name())(N), [{acl, all}]} end, lists:flatten(L)); (N) -> [{(shaper_name())(N), [{acl, all}]}] end. -spec reload_from_config() -> ok. reload_from_config() -> gen_server:call(?MODULE, reload_from_config, timer:minutes(1)). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> create_tabs(), Hosts = ejabberd_option:hosts(), load_from_config([], Hosts), ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20), {ok, #{hosts => Hosts}}. -spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}. handle_call(reload_from_config, _, #{hosts := OldHosts} = State) -> NewHosts = ejabberd_option:hosts(), load_from_config(OldHosts, NewHosts), {reply, ok, State#{hosts => NewHosts}}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(any(), state()) -> ok. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20). -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== %%%=================================================================== %%% Table management %%%=================================================================== -spec load_from_config([binary()], [binary()]) -> ok. load_from_config(OldHosts, NewHosts) -> ?DEBUG("Loading shaper rules from config", []), Shapers = ejabberd_option:shaper(), ets:insert(shaper, maps:to_list(Shapers)), ets:insert( shaper_rules, lists:flatmap( fun(Host) -> lists:flatmap( fun({Name, List}) -> case resolve_shapers(Name, List, Shapers) of [] -> []; List1 -> [{{Name, Host}, List1}] end end, ejabberd_option:shaper_rules(Host)) end, [global|NewHosts])), lists:foreach( fun(Host) -> ets:match_delete(shaper_rules, {{'_', Host}, '_'}) end, OldHosts -- NewHosts), ?DEBUG("Shaper rules loaded successfully", []). -spec create_tabs() -> ok. create_tabs() -> _ = mnesia:delete_table(shaper), _ = ets:new(shaper, [named_table, {read_concurrency, true}]), _ = ets:new(shaper_rules, [named_table, {read_concurrency, true}]), ok. -spec read_shaper_rules(atom(), global | binary()) -> [shaper_rate_rule()]. read_shaper_rules(Name, Host) -> case ets:lookup(shaper_rules, {Name, Host}) of [{_, Rule}] -> Rule; [] -> [] end. -spec read_shaper(atom() | shaper_rate()) -> none | shaper_rate(). read_shaper(Name) when is_atom(Name), Name /= none, Name /= infinity -> case ets:lookup(shaper, Name) of [{_, Rate}] -> Rate; [] -> none end; read_shaper(Rate) -> Rate. %%%=================================================================== %%% Validators %%%=================================================================== shaper_name() -> econf:either( econf:and_then( econf:atom(), fun(infinite) -> infinity; (unlimited) -> infinity; (A) -> A end), econf:pos_int()). shaper_validator() -> econf:either( econf:and_then( econf:options( #{rate => econf:pos_int(), burst_size => econf:pos_int()}, [unique, {required, [rate]}, {return, map}]), fun(#{rate := Rate} = Map) -> {Rate, maps:get(burst_size, Map, Rate)} end), econf:pos_int(infinity)). %%%=================================================================== %%% Aux %%%=================================================================== reserved() -> [none, infinite, unlimited, infinity]. -spec resolve_shapers(atom(), [shaper_rule()], #{atom() => shaper_rate()}) -> [shaper_rate_rule()]. resolve_shapers(ShaperRule, Rules, Shapers) -> lists:filtermap( fun({Name, Rule}) when is_atom(Name), Name /= none, Name /= infinity -> try {true, {maps:get(Name, Shapers), Rule}} catch _:{badkey, _} -> ?WARNING_MSG( "Shaper rule '~ts' refers to unknown shaper: ~ts", [ShaperRule, Name]), false end; (_) -> true end, Rules). ejabberd-21.12/src/mod_admin_update_sql.erl0000644000232200023220000003440414154362354021302 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_admin_update_sql.erl %%% Author : Alexey Shchepin %%% Purpose : Convert SQL DB to the new format %%% Created : 9 Aug 2017 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_admin_update_sql). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, mod_options/1, get_commands_spec/0, depends/2, mod_doc/0]). % Commands API -export([update_sql/0]). -include("logger.hrl"). -include("ejabberd_commands.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). %%% %%% gen_mod %%% start(_Host, _Opts) -> ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(_Host) -> ejabberd_commands:unregister_commands(get_commands_spec()). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. %%% %%% Register commands %%% get_commands_spec() -> [#ejabberd_commands{name = update_sql, tags = [sql], desc = "Convert PostgreSQL DB to the new format", module = ?MODULE, function = update_sql, args = [], args_example = [], args_desc = [], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"} ]. update_sql() -> lists:foreach( fun(Host) -> case ejabberd_sql_sup:is_started(Host) of false -> ok; true -> update_sql(Host) end end, ejabberd_option:hosts()). -record(state, {host :: binary(), dbtype :: mysql | pgsql | sqlite | mssql | odbc, escape}). update_sql(Host) -> LHost = jid:nameprep(Host), DBType = ejabberd_option:sql_type(LHost), IsSupported = case DBType of pgsql -> true; _ -> false end, if not IsSupported -> io:format("Converting ~p DB is not supported~n", [DBType]), error; true -> Escape = case DBType of mssql -> fun ejabberd_sql:standard_escape/1; sqlite -> fun ejabberd_sql:standard_escape/1; _ -> fun ejabberd_sql:escape/1 end, State = #state{host = LHost, dbtype = DBType, escape = Escape}, update_tables(State) end. update_tables(State) -> add_sh_column(State, "users"), drop_pkey(State, "users"), add_pkey(State, "users", ["server_host", "username"]), drop_sh_default(State, "users"), add_sh_column(State, "last"), drop_pkey(State, "last"), add_pkey(State, "last", ["server_host", "username"]), drop_sh_default(State, "last"), add_sh_column(State, "rosterusers"), drop_index(State, "i_rosteru_user_jid"), drop_index(State, "i_rosteru_username"), drop_index(State, "i_rosteru_jid"), create_unique_index(State, "rosterusers", "i_rosteru_sh_user_jid", ["server_host", "username", "jid"]), create_index(State, "rosterusers", "i_rosteru_sh_username", ["server_host", "username"]), create_index(State, "rosterusers", "i_rosteru_sh_jid", ["server_host", "jid"]), drop_sh_default(State, "rosterusers"), add_sh_column(State, "rostergroups"), drop_index(State, "pk_rosterg_user_jid"), create_index(State, "rostergroups", "i_rosterg_sh_user_jid", ["server_host", "username", "jid"]), drop_sh_default(State, "rostergroups"), add_sh_column(State, "sr_group"), add_pkey(State, "sr_group", ["server_host", "name"]), create_unique_index(State, "sr_group", "i_sr_group_sh_name", ["server_host", "name"]), drop_sh_default(State, "sr_group"), add_sh_column(State, "sr_user"), drop_index(State, "i_sr_user_jid_grp"), drop_index(State, "i_sr_user_jid"), drop_index(State, "i_sr_user_grp"), add_pkey(State, "sr_user", ["server_host", "jid", "grp"]), create_unique_index(State, "sr_user", "i_sr_user_sh_jid_grp", ["server_host", "jid", "grp"]), create_index(State, "sr_user", "i_sr_user_sh_jid", ["server_host", "jid"]), create_index(State, "sr_user", "i_sr_user_sh_grp", ["server_host", "grp"]), drop_sh_default(State, "sr_user"), add_sh_column(State, "spool"), drop_index(State, "i_despool"), create_index(State, "spool", "i_spool_sh_username", ["server_host", "username"]), drop_sh_default(State, "spool"), add_sh_column(State, "archive"), drop_index(State, "i_username"), drop_index(State, "i_username_timestamp"), drop_index(State, "i_timestamp"), drop_index(State, "i_peer"), drop_index(State, "i_bare_peer"), create_index(State, "archive", "i_archive_sh_username_timestamp", ["server_host", "username", "timestamp"]), create_index(State, "archive", "i_archive_sh_timestamp", ["server_host", "timestamp"]), create_index(State, "archive", "i_archive_sh_peer", ["server_host", "peer"]), create_index(State, "archive", "i_archive_sh_bare_peer", ["server_host", "bare_peer"]), drop_sh_default(State, "archive"), add_sh_column(State, "archive_prefs"), drop_pkey(State, "archive_prefs"), add_pkey(State, "archive_prefs", ["server_host", "username"]), drop_sh_default(State, "archive_prefs"), add_sh_column(State, "vcard"), drop_pkey(State, "vcard"), add_pkey(State, "vcard", ["server_host", "username"]), drop_sh_default(State, "vcard"), add_sh_column(State, "vcard_search"), drop_pkey(State, "vcard_search"), drop_index(State, "i_vcard_search_lfn"), drop_index(State, "i_vcard_search_lfamily"), drop_index(State, "i_vcard_search_lgiven"), drop_index(State, "i_vcard_search_lmiddle"), drop_index(State, "i_vcard_search_lnickname"), drop_index(State, "i_vcard_search_lbday"), drop_index(State, "i_vcard_search_lctry"), drop_index(State, "i_vcard_search_llocality"), drop_index(State, "i_vcard_search_lemail"), drop_index(State, "i_vcard_search_lorgname"), drop_index(State, "i_vcard_search_lorgunit"), add_pkey(State, "vcard_search", ["server_host", "username"]), create_index(State, "vcard_search", "i_vcard_search_sh_lfn", ["server_host", "lfn"]), create_index(State, "vcard_search", "i_vcard_search_sh_lfamily", ["server_host", "lfamily"]), create_index(State, "vcard_search", "i_vcard_search_sh_lgiven", ["server_host", "lgiven"]), create_index(State, "vcard_search", "i_vcard_search_sh_lmiddle", ["server_host", "lmiddle"]), create_index(State, "vcard_search", "i_vcard_search_sh_lnickname", ["server_host", "lnickname"]), create_index(State, "vcard_search", "i_vcard_search_sh_lbday", ["server_host", "lbday"]), create_index(State, "vcard_search", "i_vcard_search_sh_lctry", ["server_host", "lctry"]), create_index(State, "vcard_search", "i_vcard_search_sh_llocality", ["server_host", "llocality"]), create_index(State, "vcard_search", "i_vcard_search_sh_lemail", ["server_host", "lemail"]), create_index(State, "vcard_search", "i_vcard_search_sh_lorgname", ["server_host", "lorgname"]), create_index(State, "vcard_search", "i_vcard_search_sh_lorgunit", ["server_host", "lorgunit"]), drop_sh_default(State, "vcard_search"), add_sh_column(State, "privacy_default_list"), drop_pkey(State, "privacy_default_list"), add_pkey(State, "privacy_default_list", ["server_host", "username"]), drop_sh_default(State, "privacy_default_list"), add_sh_column(State, "privacy_list"), drop_index(State, "i_privacy_list_username"), drop_index(State, "i_privacy_list_username_name"), create_index(State, "privacy_list", "i_privacy_list_sh_username", ["server_host", "username"]), create_unique_index(State, "privacy_list", "i_privacy_list_sh_username_name", ["server_host", "username", "name"]), drop_sh_default(State, "privacy_list"), add_sh_column(State, "private_storage"), drop_index(State, "i_private_storage_username"), drop_index(State, "i_private_storage_username_namespace"), add_pkey(State, "private_storage", ["server_host", "username", "namespace"]), create_index(State, "private_storage", "i_private_storage_sh_username", ["server_host", "username"]), drop_sh_default(State, "private_storage"), add_sh_column(State, "roster_version"), drop_pkey(State, "roster_version"), add_pkey(State, "roster_version", ["server_host", "username"]), drop_sh_default(State, "roster_version"), add_sh_column(State, "muc_room"), drop_sh_default(State, "muc_room"), add_sh_column(State, "muc_registered"), drop_sh_default(State, "muc_registered"), add_sh_column(State, "muc_online_room"), drop_sh_default(State, "muc_online_room"), add_sh_column(State, "muc_online_users"), drop_sh_default(State, "muc_online_users"), add_sh_column(State, "motd"), drop_pkey(State, "motd"), add_pkey(State, "motd", ["server_host", "username"]), drop_sh_default(State, "motd"), add_sh_column(State, "sm"), drop_index(State, "i_sm_sid"), drop_index(State, "i_sm_username"), add_pkey(State, "sm", ["usec", "pid"]), create_index(State, "sm", "i_sm_sh_username", ["server_host", "username"]), drop_sh_default(State, "sm"), add_sh_column(State, "carboncopy"), drop_index(State, "i_carboncopy_ur"), drop_index(State, "i_carboncopy_user"), add_pkey(State, "carboncopy", ["server_host", "username", "resource"]), create_index(State, "carboncopy", "i_carboncopy_sh_user", ["server_host", "username"]), drop_sh_default(State, "carboncopy"), add_sh_column(State, "push_session"), drop_index(State, "i_push_usn"), drop_index(State, "i_push_ut"), add_pkey(State, "push_session", ["server_host", "username", "timestamp"]), create_index(State, "push_session", "i_push_session_susn", ["server_host", "username", "service", "node"]), drop_sh_default(State, "push_session"), ok. add_sh_column(#state{dbtype = pgsql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", (State#state.escape)(State#state.host), "';"]); add_sh_column(#state{dbtype = mysql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " ADD COLUMN server_host text NOT NULL DEFAULT '", (State#state.escape)(State#state.host), "';"]). drop_pkey(#state{dbtype = pgsql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " DROP CONSTRAINT ", Table, "_pkey;"]); drop_pkey(#state{dbtype = mysql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " DROP PRIMARY KEY;"]). add_pkey(#state{dbtype = pgsql} = State, Table, Cols) -> SCols = string:join(Cols, ", "), sql_query( State#state.host, ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]); add_pkey(#state{dbtype = mysql} = State, Table, Cols) -> SCols = string:join(Cols, ", "), sql_query( State#state.host, ["ALTER TABLE ", Table, " ADD PRIMARY KEY (", SCols, ");"]). drop_sh_default(#state{dbtype = pgsql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]); drop_sh_default(#state{dbtype = mysql} = State, Table) -> sql_query( State#state.host, ["ALTER TABLE ", Table, " ALTER COLUMN server_host DROP DEFAULT;"]). drop_index(#state{dbtype = pgsql} = State, Index) -> sql_query( State#state.host, ["DROP INDEX ", Index, ";"]); drop_index(#state{dbtype = mysql} = State, Index) -> sql_query( State#state.host, ["DROP INDEX ", Index, ";"]). create_unique_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> SCols = string:join(Cols, ", "), sql_query( State#state.host, ["CREATE UNIQUE INDEX ", Index, " ON ", Table, " USING btree (", SCols, ");"]); create_unique_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> Cols2 = [C ++ "(75)" || C <- Cols], SCols = string:join(Cols2, ", "), sql_query( State#state.host, ["CREATE UNIQUE INDEX ", Index, " ON ", Table, "(", SCols, ");"]). create_index(#state{dbtype = pgsql} = State, Table, Index, Cols) -> SCols = string:join(Cols, ", "), sql_query( State#state.host, ["CREATE INDEX ", Index, " ON ", Table, " USING btree (", SCols, ");"]); create_index(#state{dbtype = mysql} = State, Table, Index, Cols) -> Cols2 = [C ++ "(75)" || C <- Cols], SCols = string:join(Cols2, ", "), sql_query( State#state.host, ["CREATE INDEX ", Index, " ON ", Table, "(", SCols, ");"]). sql_query(Host, Query) -> io:format("executing \"~ts\" on ~ts~n", [Query, Host]), case ejabberd_sql:sql_query(Host, Query) of {error, Error} -> io:format("error: ~p~n", [Error]), ok; _ -> ok end. mod_options(_) -> []. mod_doc() -> #{desc => ?T("This module can be used to update existing SQL database " "from the default to the new schema. Check the section " "http://../database/#default-and-new-schemas[Default and New Schemas] for details. " "Please note that only PostgreSQL is supported. " "When the module is loaded use _`update_sql`_ API.")}. ejabberd-21.12/src/mod_announce.erl0000644000232200023220000010434214154362354017576 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_announce.erl %%% Author : Alexey Shchepin %%% Purpose : Manage announce messages %%% Created : 11 Aug 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% Implements a small subset of XEP-0133: Service Administration %%% Version 1.1 (2005-08-19) -module(mod_announce). -author('alexey@process-one.net'). -behaviour(gen_server). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, export/1, import_info/0, import_start/2, import/5, announce/1, send_motd/1, disco_identity/5, disco_features/5, disco_items/5, depends/2, send_announcement_to_all/3, announce_commands/4, mod_doc/0, announce_items/4, mod_opt_type/1, mod_options/1, clean_cache/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([announce_all/1, announce_all_hosts_all/1, announce_online/1, announce_all_hosts_online/1, announce_motd/1, announce_all_hosts_motd/1, announce_motd_update/1, announce_all_hosts_motd_update/1, announce_motd_delete/1, announce_all_hosts_motd_delete/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_announce.hrl"). -include("translate.hrl"). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback set_motd_users(binary(), [{binary(), binary(), binary()}]) -> ok | {error, any()}. -callback set_motd(binary(), xmlel()) -> ok | {error, any()}. -callback delete_motd(binary()) -> ok | {error, any()}. -callback get_motd(binary()) -> {ok, xmlel()} | error | {error, any()}. -callback is_motd_user(binary(), binary()) -> {ok, boolean()} | {error, any()}. -callback set_motd_user(binary(), binary()) -> ok | {error, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). -record(state, {host :: binary()}). -define(NS_ADMINL(Sub), [<<"http:">>, <<"jabber.org">>, <<"protocol">>, <<"admin">>, <>]). -define(MOTD_CACHE, motd_cache). tokenize(Node) -> str:tokens(Node, <<"/#">>). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). depends(_Host, _Opts) -> [{mod_adhoc, hard}]. %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(local_send_to_resource_hook, Host, ?MODULE, announce, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 50), ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, announce_items, 50), ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, send_motd, 50), {ok, #state{host = Host}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({F, #message{from = From, to = To} = Pkt}, State) when is_atom(F) -> LServer = To#jid.lserver, Host = case F of announce_all -> LServer; announce_all_hosts_all -> global; announce_online -> LServer; announce_all_hosts_online -> global; announce_motd -> LServer; announce_all_hosts_motd -> global; announce_motd_update -> LServer; announce_all_hosts_motd_update -> global; announce_motd_delete -> LServer; announce_all_hosts_motd_delete -> global end, Access = get_access(Host), case acl:match_rule(Host, Access, From) of deny -> route_forbidden_error(Pkt); allow -> ?MODULE:F(Pkt) end, {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, announce_commands, 50), ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, announce_items, 50), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 50), ejabberd_hooks:delete(local_send_to_resource_hook, Host, ?MODULE, announce, 50), ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, send_motd, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. %% Announcing via messages to a custom resource -spec announce(stanza()) -> ok | stop. announce(#message{to = #jid{luser = <<>>} = To} = Packet) -> Proc = gen_mod:get_module_proc(To#jid.lserver, ?MODULE), Res = case To#jid.lresource of <<"announce/all">> -> gen_server:cast(Proc, {announce_all, Packet}); <<"announce/all-hosts/all">> -> gen_server:cast(Proc, {announce_all_hosts_all, Packet}); <<"announce/online">> -> gen_server:cast(Proc, {announce_online, Packet}); <<"announce/all-hosts/online">> -> gen_server:cast(Proc, {announce_all_hosts_online, Packet}); <<"announce/motd">> -> gen_server:cast(Proc, {announce_motd, Packet}); <<"announce/all-hosts/motd">> -> gen_server:cast(Proc, {announce_all_hosts_motd, Packet}); <<"announce/motd/update">> -> gen_server:cast(Proc, {announce_motd_update, Packet}); <<"announce/all-hosts/motd/update">> -> gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet}); <<"announce/motd/delete">> -> gen_server:cast(Proc, {announce_motd_delete, Packet}); <<"announce/all-hosts/motd/delete">> -> gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet}); _ -> undefined end, case Res of ok -> stop; _ -> ok end; announce(_Packet) -> ok. %%------------------------------------------------------------------------- %% Announcing via ad-hoc commands -define(INFO_COMMAND(Lang, Node), [#identity{category = <<"automation">>, type = <<"command-node">>, name = get_title(Lang, Node)}]). disco_identity(Acc, _From, _To, Node, Lang) -> LNode = tokenize(Node), case LNode of ?NS_ADMINL("announce") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("announce-allhosts") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("announce-all") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("announce-all-allhosts") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("set-motd") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("set-motd-allhosts") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("edit-motd") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("edit-motd-allhosts") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("delete-motd") -> ?INFO_COMMAND(Lang, Node); ?NS_ADMINL("delete-motd-allhosts") -> ?INFO_COMMAND(Lang, Node); _ -> Acc end. %%------------------------------------------------------------------------- -define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Feats} end). disco_features(Acc, From, #jid{lserver = LServer} = _To, <<"announce">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Access1 = get_access(LServer), Access2 = get_access(global), case {acl:match_rule(LServer, Access1, From), acl:match_rule(global, Access2, From)} of {deny, deny} -> Txt = ?T("Access denied by service policy"), {error, xmpp:err_forbidden(Txt, Lang)}; _ -> {result, []} end end; disco_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Access = get_access(LServer), Allow = acl:match_rule(LServer, Access, From), AccessGlobal = get_access(global), AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> ?INFO_RESULT(AllowGlobal, [?NS_COMMANDS], Lang); _ -> Acc end end. %%------------------------------------------------------------------------- -define(NODE_TO_ITEM(Lang, Server, Node), #disco_item{jid = jid:make(Server), node = Node, name = get_title(Lang, Node)}). -define(ITEMS_RESULT(Allow, Items, Lang), case Allow of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Items} end). disco_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, <<"">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Access1 = get_access(LServer), Access2 = get_access(global), case {acl:match_rule(LServer, Access1, From), acl:match_rule(global, Access2, From)} of {deny, deny} -> Acc; _ -> Items = case Acc of {result, I} -> I; _ -> [] end, Nodes = [?NODE_TO_ITEM(Lang, Server, <<"announce">>)], {result, Items ++ Nodes} end end; disco_items(Acc, From, #jid{lserver = LServer} = To, <<"announce">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> announce_items(Acc, From, To, Lang) end; disco_items(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Access = get_access(LServer), Allow = acl:match_rule(LServer, Access, From), AccessGlobal = get_access(global), AllowGlobal = acl:match_rule(global, AccessGlobal, From), case Node of ?NS_ADMIN_ANNOUNCE -> ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL -> ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_SET_MOTD -> ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_EDIT_MOTD -> ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_DELETE_MOTD -> ?ITEMS_RESULT(Allow, [], Lang); ?NS_ADMIN_ANNOUNCE_ALLHOSTS -> ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS -> ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_SET_MOTD_ALLHOSTS -> ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_EDIT_MOTD_ALLHOSTS -> ?ITEMS_RESULT(AllowGlobal, [], Lang); ?NS_ADMIN_DELETE_MOTD_ALLHOSTS -> ?ITEMS_RESULT(AllowGlobal, [], Lang); _ -> Acc end end. %%------------------------------------------------------------------------- -spec announce_items(empty | {error, stanza_error()} | {result, [disco_item()]}, jid(), jid(), binary()) -> {error, stanza_error()} | {result, [disco_item()]} | empty. announce_items(Acc, From, #jid{lserver = LServer, server = Server} = _To, Lang) -> Access1 = get_access(LServer), Nodes1 = case acl:match_rule(LServer, Access1, From) of allow -> [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALL), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_SET_MOTD), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_EDIT_MOTD), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_DELETE_MOTD)]; deny -> [] end, Access2 = get_access(global), Nodes2 = case acl:match_rule(global, Access2, From) of allow -> [?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALLHOSTS), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_SET_MOTD_ALLHOSTS), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_EDIT_MOTD_ALLHOSTS), ?NODE_TO_ITEM(Lang, Server, ?NS_ADMIN_DELETE_MOTD_ALLHOSTS)]; deny -> [] end, case {Nodes1, Nodes2} of {[], []} -> Acc; _ -> Items = case Acc of {result, I} -> I; _ -> [] end, {result, Items ++ Nodes1 ++ Nodes2} end. %%------------------------------------------------------------------------- commands_result(Allow, From, To, Request) -> case Allow of deny -> Lang = Request#adhoc_command.lang, {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> announce_commands(From, To, Request) end. -spec announce_commands(empty | adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. announce_commands(Acc, From, #jid{lserver = LServer} = To, #adhoc_command{node = Node} = Request) -> LNode = tokenize(Node), F = fun() -> Access = get_access(global), Allow = acl:match_rule(global, Access, From), commands_result(Allow, From, To, Request) end, R = case LNode of ?NS_ADMINL("announce-allhosts") -> F(); ?NS_ADMINL("announce-all-allhosts") -> F(); ?NS_ADMINL("set-motd-allhosts") -> F(); ?NS_ADMINL("edit-motd-allhosts") -> F(); ?NS_ADMINL("delete-motd-allhosts") -> F(); _ -> Access = get_access(LServer), Allow = acl:match_rule(LServer, Access, From), case LNode of ?NS_ADMINL("announce") -> commands_result(Allow, From, To, Request); ?NS_ADMINL("announce-all") -> commands_result(Allow, From, To, Request); ?NS_ADMINL("set-motd") -> commands_result(Allow, From, To, Request); ?NS_ADMINL("edit-motd") -> commands_result(Allow, From, To, Request); ?NS_ADMINL("delete-motd") -> commands_result(Allow, From, To, Request); _ -> unknown end end, case R of unknown -> Acc; _ -> {stop, R} end. %%------------------------------------------------------------------------- announce_commands(From, To, #adhoc_command{lang = Lang, node = Node, sid = SID, xdata = XData, action = Action} = Request) -> if Action == cancel -> %% User cancels request #adhoc_command{status = canceled, lang = Lang, node = Node, sid = SID}; XData == undefined andalso Action == execute -> %% User requests form Form = generate_adhoc_form(Lang, Node, To#jid.lserver), xmpp_util:make_adhoc_response( #adhoc_command{status = executing, lang = Lang, node = Node, sid = SID, xdata = Form}); XData /= undefined andalso (Action == execute orelse Action == complete) -> case handle_adhoc_form(From, To, Request) of ok -> #adhoc_command{lang = Lang, node = Node, sid = SID, status = completed}; {error, _} = Err -> Err end; true -> Txt = ?T("Unexpected action"), {error, xmpp:err_bad_request(Txt, Lang)} end. -define(TVFIELD(Type, Var, Val), #xdata_field{type = Type, var = Var, values = vvaluel(Val)}). vvaluel(Val) -> case Val of <<>> -> []; _ -> [Val] end. generate_adhoc_form(Lang, Node, ServerHost) -> LNode = tokenize(Node), {OldSubject, OldBody} = if (LNode == ?NS_ADMINL("edit-motd")) or (LNode == ?NS_ADMINL("edit-motd-allhosts")) -> get_stored_motd(ServerHost); true -> {<<>>, <<>>} end, Fs = if (LNode == ?NS_ADMINL("delete-motd")) or (LNode == ?NS_ADMINL("delete-motd-allhosts")) -> [#xdata_field{type = boolean, var = <<"confirm">>, label = translate:translate( Lang, ?T("Really delete message of the day?")), values = [<<"true">>]}]; true -> [#xdata_field{type = 'text-single', var = <<"subject">>, label = translate:translate(Lang, ?T("Subject")), values = vvaluel(OldSubject)}, #xdata_field{type = 'text-multi', var = <<"body">>, label = translate:translate(Lang, ?T("Message body")), values = vvaluel(OldBody)}] end, #xdata{type = form, title = get_title(Lang, Node), fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, values = [?NS_ADMIN]}|Fs]}. join_lines([]) -> <<>>; join_lines(Lines) -> join_lines(Lines, []). join_lines([Line|Lines], Acc) -> join_lines(Lines, [<<"\n">>,Line|Acc]); join_lines([], Acc) -> %% Remove last newline iolist_to_binary(lists:reverse(tl(Acc))). handle_adhoc_form(From, #jid{lserver = LServer} = To, #adhoc_command{lang = Lang, node = Node, xdata = XData}) -> Confirm = case xmpp_util:get_xdata_values(<<"confirm">>, XData) of [<<"true">>] -> true; [<<"1">>] -> true; _ -> false end, Subject = join_lines(xmpp_util:get_xdata_values(<<"subject">>, XData)), Body = join_lines(xmpp_util:get_xdata_values(<<"body">>, XData)), Packet = #message{from = From, to = To, type = headline, body = xmpp:mk_text(Body), subject = xmpp:mk_text(Subject)}, Proc = gen_mod:get_module_proc(LServer, ?MODULE), case {Node, Body} of {?NS_ADMIN_DELETE_MOTD, _} -> if Confirm -> gen_server:cast(Proc, {announce_motd_delete, Packet}); true -> ok end; {?NS_ADMIN_DELETE_MOTD_ALLHOSTS, _} -> if Confirm -> gen_server:cast(Proc, {announce_all_hosts_motd_delete, Packet}); true -> ok end; {_, <<>>} -> %% An announce message with no body is definitely an operator error. %% Throw an error and give him/her a chance to send message again. {error, xmpp:err_not_acceptable( ?T("No body provided for announce message"), Lang)}; %% Now send the packet to ?MODULE. %% We don't use direct announce_* functions because it %% leads to large delay in response and queries processing {?NS_ADMIN_ANNOUNCE, _} -> gen_server:cast(Proc, {announce_online, Packet}); {?NS_ADMIN_ANNOUNCE_ALLHOSTS, _} -> gen_server:cast(Proc, {announce_all_hosts_online, Packet}); {?NS_ADMIN_ANNOUNCE_ALL, _} -> gen_server:cast(Proc, {announce_all, Packet}); {?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS, _} -> gen_server:cast(Proc, {announce_all_hosts_all, Packet}); {?NS_ADMIN_SET_MOTD, _} -> gen_server:cast(Proc, {announce_motd, Packet}); {?NS_ADMIN_SET_MOTD_ALLHOSTS, _} -> gen_server:cast(Proc, {announce_all_hosts_motd, Packet}); {?NS_ADMIN_EDIT_MOTD, _} -> gen_server:cast(Proc, {announce_motd_update, Packet}); {?NS_ADMIN_EDIT_MOTD_ALLHOSTS, _} -> gen_server:cast(Proc, {announce_all_hosts_motd_update, Packet}); Junk -> %% This can't happen, as we haven't registered any other %% command nodes. ?ERROR_MSG("Unexpected node/body = ~p", [Junk]), {error, xmpp:err_internal_server_error()} end. get_title(Lang, <<"announce">>) -> translate:translate(Lang, ?T("Announcements")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL) -> translate:translate(Lang, ?T("Send announcement to all users")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALL_ALLHOSTS) -> translate:translate(Lang, ?T("Send announcement to all users on all hosts")); get_title(Lang, ?NS_ADMIN_ANNOUNCE) -> translate:translate(Lang, ?T("Send announcement to all online users")); get_title(Lang, ?NS_ADMIN_ANNOUNCE_ALLHOSTS) -> translate:translate(Lang, ?T("Send announcement to all online users on all hosts")); get_title(Lang, ?NS_ADMIN_SET_MOTD) -> translate:translate(Lang, ?T("Set message of the day and send to online users")); get_title(Lang, ?NS_ADMIN_SET_MOTD_ALLHOSTS) -> translate:translate(Lang, ?T("Set message of the day on all hosts and send to online users")); get_title(Lang, ?NS_ADMIN_EDIT_MOTD) -> translate:translate(Lang, ?T("Update message of the day (don't send)")); get_title(Lang, ?NS_ADMIN_EDIT_MOTD_ALLHOSTS) -> translate:translate(Lang, ?T("Update message of the day on all hosts (don't send)")); get_title(Lang, ?NS_ADMIN_DELETE_MOTD) -> translate:translate(Lang, ?T("Delete message of the day")); get_title(Lang, ?NS_ADMIN_DELETE_MOTD_ALLHOSTS) -> translate:translate(Lang, ?T("Delete message of the day on all hosts")). %%------------------------------------------------------------------------- announce_all(#message{to = To} = Packet) -> Local = jid:make(To#jid.server), lists:foreach( fun({User, Server}) -> Dest = jid:make(User, Server), ejabberd_router:route( xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) end, ejabberd_auth:get_users(To#jid.lserver)). announce_all_hosts_all(#message{to = To} = Packet) -> Local = jid:make(To#jid.server), lists:foreach( fun({User, Server}) -> Dest = jid:make(User, Server), ejabberd_router:route( xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) end, ejabberd_auth:get_users()). announce_online(#message{to = To} = Packet) -> announce_online1(ejabberd_sm:get_vh_session_list(To#jid.lserver), To#jid.server, Packet). announce_all_hosts_online(#message{to = To} = Packet) -> announce_online1(ejabberd_sm:dirty_get_sessions_list(), To#jid.server, Packet). announce_online1(Sessions, Server, Packet) -> Local = jid:make(Server), lists:foreach( fun({U, S, R}) -> Dest = jid:make(U, S, R), ejabberd_router:route(xmpp:set_from_to(Packet, Local, Dest)) end, Sessions). announce_motd(#message{to = To} = Packet) -> announce_motd(To#jid.lserver, Packet). announce_all_hosts_motd(Packet) -> Hosts = ejabberd_option:hosts(), [announce_motd(Host, Packet) || Host <- Hosts]. announce_motd(Host, Packet) -> LServer = jid:nameprep(Host), announce_motd_update(LServer, Packet), Sessions = ejabberd_sm:get_vh_session_list(LServer), announce_online1(Sessions, LServer, Packet), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:set_motd_users(LServer, Sessions). announce_motd_update(#message{to = To} = Packet) -> announce_motd_update(To#jid.lserver, Packet). announce_all_hosts_motd_update(Packet) -> Hosts = ejabberd_option:hosts(), [announce_motd_update(Host, Packet) || Host <- Hosts]. announce_motd_update(LServer, Packet) -> Mod = gen_mod:db_mod(LServer, ?MODULE), delete_motd(Mod, LServer), set_motd(Mod, LServer, xmpp:encode(Packet)). announce_motd_delete(#message{to = To}) -> LServer = To#jid.lserver, Mod = gen_mod:db_mod(LServer, ?MODULE), delete_motd(Mod, LServer). announce_all_hosts_motd_delete(_Packet) -> lists:foreach( fun(Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), delete_motd(Mod, Host) end, ejabberd_option:hosts()). -spec send_motd({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. send_motd({_, #{pres_last := _}} = Acc) -> %% This is just a presence update, nothing to do Acc; send_motd({#presence{type = available}, #{jid := #jid{luser = LUser, lserver = LServer} = JID}} = Acc) when LUser /= <<>> -> Mod = gen_mod:db_mod(LServer, ?MODULE), case get_motd(Mod, LServer) of {ok, Packet} -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of Msg -> case is_motd_user(Mod, LUser, LServer) of false -> Local = jid:make(LServer), ejabberd_router:route( xmpp:set_from_to(Msg, Local, JID)), set_motd_user(Mod, LUser, LServer); true -> ok end catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode motd packet ~p: ~ts", [Packet, xmpp:format_error(Why)]) end; _ -> ok end, Acc; send_motd(Acc) -> Acc. -spec get_motd(module(), binary()) -> {ok, xmlel()} | error | {error, any()}. get_motd(Mod, LServer) -> case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?MOTD_CACHE, {<<"">>, LServer}, fun() -> Mod:get_motd(LServer) end); false -> Mod:get_motd(LServer) end. -spec set_motd(module(), binary(), xmlel()) -> any(). set_motd(Mod, LServer, XML) -> case use_cache(Mod, LServer) of true -> ets_cache:update( ?MOTD_CACHE, {<<"">>, LServer}, {ok, XML}, fun() -> Mod:set_motd(LServer, XML) end, cache_nodes(Mod, LServer)); false -> Mod:set_motd(LServer, XML) end. -spec is_motd_user(module(), binary(), binary()) -> boolean(). is_motd_user(Mod, LUser, LServer) -> Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?MOTD_CACHE, {LUser, LServer}, fun() -> Mod:is_motd_user(LUser, LServer) end); false -> Mod:is_motd_user(LUser, LServer) end, case Res of {ok, Bool} -> Bool; _ -> false end. -spec set_motd_user(module(), binary(), binary()) -> any(). set_motd_user(Mod, LUser, LServer) -> case use_cache(Mod, LServer) of true -> ets_cache:update( ?MOTD_CACHE, {LUser, LServer}, {ok, true}, fun() -> Mod:set_motd_user(LUser, LServer) end, cache_nodes(Mod, LServer)); false -> Mod:set_motd_user(LUser, LServer) end. -spec delete_motd(module(), binary()) -> ok | {error, any()}. delete_motd(Mod, LServer) -> case Mod:delete_motd(LServer) of ok -> case use_cache(Mod, LServer) of true -> ejabberd_cluster:eval_everywhere( ?MODULE, clean_cache, [LServer]); false -> ok end; Err -> Err end. get_stored_motd(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case get_motd(Mod, LServer) of {ok, Packet} -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(Packet, ?NS_CLIENT, CodecOpts) of #message{body = Body, subject = Subject} -> {xmpp:get_text(Subject), xmpp:get_text(Body)} catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode motd packet ~p: ~ts", [Packet, xmpp:format_error(Why)]) end; _ -> {<<>>, <<>>} end. %% This function is similar to others, but doesn't perform any ACL verification send_announcement_to_all(Host, SubjectS, BodyS) -> Packet = #message{type = headline, body = xmpp:mk_text(BodyS), subject = xmpp:mk_text(SubjectS)}, Sessions = ejabberd_sm:dirty_get_sessions_list(), Local = jid:make(Host), lists:foreach( fun({U, S, R}) -> Dest = jid:make(U, S, R), ejabberd_router:route( xmpp:set_from_to(add_store_hint(Packet), Local, Dest)) end, Sessions). -spec get_access(global | binary()) -> atom(). get_access(Host) -> mod_announce_opt:access(Host). -spec add_store_hint(stanza()) -> stanza(). add_store_hint(El) -> xmpp:set_subtag(El, #hint{type = store}). -spec route_forbidden_error(stanza()) -> ok. route_forbidden_error(Packet) -> Lang = xmpp:get_lang(Packet), Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), ejabberd_router:route_error(Packet, Err). -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?MOTD_CACHE, CacheOpts); false -> ets_cache:delete(?MOTD_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_announce_opt:cache_size(Opts), CacheMissed = mod_announce_opt:cache_missed(Opts), LifeTime = mod_announce_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_announce_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec clean_cache(binary()) -> non_neg_integer(). clean_cache(LServer) -> ets_cache:filter( ?MOTD_CACHE, fun({_, S}, _) -> S /= LServer end). %%------------------------------------------------------------------------- export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"motd">>, 3}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, DBType, Tab, List) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, List). mod_opt_type(access) -> econf:acl(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{access, none}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module enables configured users to broadcast " "announcements and to set the message of the day (MOTD). " "Configured users can perform these actions with an XMPP " "client either using Ad-hoc Commands or sending messages " "to specific JIDs."), "", ?T("Note that this module can be resource intensive on large " "deployments as it may broadcast a lot of messages. This module " "should be disabled for instances of ejabberd with hundreds of " "thousands users."), "", ?T("The Ad-hoc Commands are listed in the Server Discovery. " "For this feature to work, _`mod_adhoc`_ must be enabled."), "", ?T("The specific JIDs where messages can be sent are listed below. " "The first JID in each entry will apply only to the specified " "virtual host example.org, while the JID between brackets " "will apply to all virtual hosts in ejabberd:"), "", "- example.org/announce/all (example.org/announce/all-hosts/all)::", ?T("The message is sent to all registered users. If the user is " "online and connected to several resources, only the resource " "with the highest priority will receive the message. " "If the registered user is not connected, the message will be " "stored offline in assumption that offline storage (see _`mod_offline`_) " "is enabled."), "- example.org/announce/online (example.org/announce/all-hosts/online)::", ?T("The message is sent to all connected users. If the user is " "online and connected to several resources, all resources will " "receive the message."), "- example.org/announce/motd (example.org/announce/all-hosts/motd)::", ?T("The message is set as the message of the day (MOTD) and is sent " "to users when they login. In addition the message is sent to all " "connected users (similar to announce/online)."), "- example.org/announce/motd/update (example.org/announce/all-hosts/motd/update)::", ?T("The message is set as message of the day (MOTD) and is sent to users " "when they login. The message is not sent to any currently connected user."), "- example.org/announce/motd/delete (example.org/announce/all-hosts/motd/delete)::", ?T("Any message sent to this JID removes the existing message of the day (MOTD).")], opts => [{access, #{value => ?T("AccessName"), desc => ?T("This option specifies who is allowed to send announcements " "and to set the message of the day. The default value is 'none' " "(i.e. nobody is able to send such messages).")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. ejabberd-21.12/src/ejabberd_xmlrpc.erl0000644000232200023220000003321014154362354020247 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_xmlrpc.erl %%% Author : Badlop %%% Purpose : XML-RPC server that frontends ejabberd commands %%% Created : 21 Aug 2007 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% TODO: Remove support for plaintext password %%% TODO: commands strings should be strings without ~n -module(ejabberd_xmlrpc). -behaviour(ejabberd_listener). -author('badlop@process-one.net'). -export([start/3, start_link/3, handler/2, process/2, accept/1, listen_options/0]). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("mod_roster.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -record(state, {auth = noauth :: noauth | map(), get_auth = true :: boolean(), ip :: inet:ip_address()}). %% ----------------------------- %% Listener interface %% ----------------------------- start(SockMod, Socket, Opts) -> Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts], ejabberd_http:start(SockMod, Socket, Opts1). start_link(SockMod, Socket, Opts) -> Opts1 = [{request_handlers, [{[], ?MODULE}]}|Opts], ejabberd_http:start_link(SockMod, Socket, Opts1). accept(Pid) -> ejabberd_http:accept(Pid). %% ----------------------------- %% HTTP interface %% ----------------------------- process(_, #request{method = 'POST', data = Data, ip = {IP, _}}) -> GetAuth = true, State = #state{get_auth = GetAuth, ip = IP}, case fxml_stream:parse_element(Data) of {error, _} -> {400, [], #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"Malformed XML">>}]}}; El -> case fxmlrpc:decode(El) of {error, _} = Err -> ?ERROR_MSG("XML-RPC request ~ts failed with reason: ~p", [Data, Err]), {400, [], #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"Malformed Request">>}]}}; {ok, RPC} -> ?DEBUG("Got XML-RPC request: ~p", [RPC]), {false, Result} = handler(State, RPC), XML = fxml:element_to_binary(fxmlrpc:encode(Result)), {200, [{<<"Content-Type">>, <<"text/xml">>}], <<"", XML/binary>>} end end; process(_, _) -> {400, [], #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}}. %% ----------------------------- %% Access verification %% ----------------------------- -spec extract_auth([{user | server | token | password, binary()}]) -> map() | {error, not_found | expired | invalid_auth}. extract_auth(AuthList) -> ?DEBUG("AUTHLIST ~p", [AuthList]), try get_attrs([user, server, token], AuthList) of [U0, S0, T] -> U = jid:nodeprep(U0), S = jid:nameprep(S0), case ejabberd_oauth:check_token(T) of {ok, {U, S}, Scope} -> #{usr => {U, S, <<"">>}, oauth_scope => Scope, caller_server => S}; {false, Reason} -> {error, Reason}; _ -> {error, not_found} end catch exit:{attribute_not_found, _, _} -> try get_attrs([user, server, password], AuthList) of [U0, S0, P] -> U = jid:nodeprep(U0), S = jid:nameprep(S0), case ejabberd_auth:check_password(U, <<"">>, S, P) of true -> #{usr => {U, S, <<"">>}, caller_server => S}; false -> {error, invalid_auth} end catch exit:{attribute_not_found, Attr, _} -> throw({error, missing_auth_arguments, Attr}) end end. %% ----------------------------- %% Handlers %% ----------------------------- handler(#state{get_auth = true, auth = noauth, ip = IP} = State, {call, Method, [{struct, AuthList} | Arguments] = AllArgs}) -> try extract_auth(AuthList) of {error, invalid_auth} -> build_fault_response(-118, "Invalid authentication data", []); {error, not_found} -> build_fault_response(-118, "Invalid oauth token", []); {error, expired} -> build_fault_response(-118, "Invalid oauth token", []); Auth -> handler(State#state{get_auth = false, auth = Auth#{ip => IP, caller_module => ?MODULE}}, {call, Method, Arguments}) catch {error, missing_auth_arguments, _Attr} -> handler(State#state{get_auth = false, auth = #{ip => IP, caller_module => ?MODULE}}, {call, Method, AllArgs}) end; %% ............................. %% Debug handler(_State, {call, echothis, [A]}) -> {false, {response, [A]}}; handler(_State, {call, echothisnew, [{struct, [{sentence, A}]}]}) -> {false, {response, [{struct, [{repeated, A}]}]}}; handler(_State, {call, multhis, [{struct, [{a, A}, {b, B}]}]}) -> {false, {response, [A * B]}}; handler(_State, {call, multhisnew, [{struct, [{a, A}, {b, B}]}]}) -> {false, {response, [{struct, [{mu, A * B}]}]}}; %% ............................. %% ejabberd commands handler(State, {call, Command, []}) -> handler(State, {call, Command, [{struct, []}]}); handler(State, {call, Command, [{struct, AttrL}]}) -> {ArgsF, ArgsR, ResultF} = ejabberd_commands:get_command_format(Command, State#state.auth), try_do_command(State#state.auth, Command, AttrL, ArgsF, ArgsR, ResultF); handler(_State, Payload) -> build_fault_response(-112, "Unknown call: ~p", [Payload]). %% ----------------------------- %% Command %% ----------------------------- try_do_command(Auth, Command, AttrL, ArgsF, ArgsR, ResultF) -> try do_command(Auth, Command, AttrL, ArgsF, ArgsR, ResultF) of {command_result, ResultFormatted} -> {false, {response, [ResultFormatted]}} catch exit:{duplicated_attribute, ExitAt, ExitAtL} -> build_fault_response(-114, "Attribute '~p' duplicated:~n~p", [ExitAt, ExitAtL]); exit:{attribute_not_found, ExitAt, ExitAtL} -> build_fault_response(-116, "Required attribute '~p' not found:~n~p", [ExitAt, ExitAtL]); exit:{additional_unused_args, ExitAtL} -> build_fault_response(-120, "The call provided additional unused " "arguments:~n~p", [ExitAtL]); exit:{invalid_arg_type, Arg, Type} -> build_fault_response(-122, "Parameter '~p' can't be coerced to type '~p'", [Arg, Type]); Why -> build_fault_response(-118, "A problem '~p' occurred executing the " "command ~p with arguments~n~p", [Why, Command, AttrL]) end. build_fault_response(Code, ParseString, ParseArgs) -> FaultString = "Error " ++ integer_to_list(Code) ++ "\n" ++ lists:flatten(io_lib:format(ParseString, ParseArgs)), ?WARNING_MSG(FaultString, []), {false, {response, {fault, Code, list_to_binary(FaultString)}}}. do_command(Auth, Command, AttrL, ArgsF, ArgsR, ResultF) -> ArgsFormatted = format_args(rename_old_args(AttrL, ArgsR), ArgsF), Result = ejabberd_commands:execute_command2(Command, ArgsFormatted, Auth), ResultFormatted = format_result(Result, ResultF), {command_result, ResultFormatted}. rename_old_args(Args, []) -> Args; rename_old_args(Args, [{OldName, NewName} | ArgsR]) -> Args2 = case lists:keytake(OldName, 1, Args) of {value, {OldName, Value}, ArgsTail} -> [{NewName, Value} | ArgsTail]; false -> Args end, rename_old_args(Args2, ArgsR). %%----------------------------- %% Format arguments %%----------------------------- get_attrs(Attribute_names, L) -> [get_attr(A, L) || A <- Attribute_names]. get_attr(A, L) -> case lists:keysearch(A, 1, L) of {value, {A, Value}} -> Value; false -> exit({attribute_not_found, A, L}) end. get_elem_delete(A, L) -> case proplists:get_all_values(A, L) of [Value] -> {Value, proplists:delete(A, L)}; [_, _ | _] -> exit({duplicated_attribute, A, L}); [] -> exit({attribute_not_found, A, L}) end. format_args(Args, ArgsFormat) -> {ArgsRemaining, R} = lists:foldl(fun ({ArgName, ArgFormat}, {Args1, Res}) -> {ArgValue, Args2} = get_elem_delete(ArgName, Args1), Formatted = format_arg(ArgValue, ArgFormat), {Args2, Res ++ [Formatted]} end, {Args, []}, ArgsFormat), case ArgsRemaining of [] -> R; L when is_list(L) -> exit({additional_unused_args, L}) end. format_arg({array, Elements}, {list, {ElementDefName, ElementDefFormat}}) when is_list(Elements) -> lists:map(fun ({struct, [{ElementName, ElementValue}]}) when ElementDefName == ElementName -> format_arg(ElementValue, ElementDefFormat) end, Elements); format_arg({array, [{struct, Elements}]}, {list, {ElementDefName, ElementDefFormat}}) when is_list(Elements) -> lists:map(fun ({ElementName, ElementValue}) -> true = ElementDefName == ElementName, format_arg(ElementValue, ElementDefFormat) end, Elements); format_arg({array, [{struct, Elements}]}, {tuple, ElementsDef}) when is_list(Elements) -> FormattedList = format_args(Elements, ElementsDef), list_to_tuple(FormattedList); format_arg({array, Elements}, {list, ElementsDef}) when is_list(Elements) and is_atom(ElementsDef) -> [format_arg(Element, ElementsDef) || Element <- Elements]; format_arg(Arg, integer) when is_integer(Arg) -> Arg; format_arg(Arg, binary) when is_list(Arg) -> process_unicode_codepoints(Arg); format_arg(Arg, binary) when is_binary(Arg) -> Arg; format_arg(Arg, string) when is_list(Arg) -> Arg; format_arg(Arg, string) when is_binary(Arg) -> binary_to_list(Arg); format_arg(undefined, binary) -> <<>>; format_arg(undefined, string) -> ""; format_arg(Arg, Format) -> ?ERROR_MSG("Don't know how to format Arg ~p for format ~p", [Arg, Format]), exit({invalid_arg_type, Arg, Format}). process_unicode_codepoints(Str) -> iolist_to_binary(lists:map(fun(X) when X > 255 -> unicode:characters_to_binary([X]); (Y) -> Y end, Str)). %% ----------------------------- %% Result %% ----------------------------- format_result({error, Error}, _) when is_list(Error) -> throw({error, lists:flatten(Error)}); format_result({error, Error}, _) -> throw({error, Error}); format_result({error, _Type, _Code, Error}, _) when is_list(Error) -> throw({error, lists:flatten(Error)}); format_result({error, _Type, _Code, Error}, _) -> throw({error, Error}); format_result(String, string) -> lists:flatten(String); format_result(Atom, {Name, atom}) -> {struct, [{Name, iolist_to_binary(atom_to_list(Atom))}]}; format_result(Int, {Name, integer}) -> {struct, [{Name, Int}]}; format_result([A|_]=String, {Name, string}) when is_list(String) and is_integer(A) -> {struct, [{Name, lists:flatten(String)}]}; format_result(Binary, {Name, string}) when is_binary(Binary) -> {struct, [{Name, binary_to_list(Binary)}]}; format_result(Atom, {Name, string}) when is_atom(Atom) -> {struct, [{Name, atom_to_list(Atom)}]}; format_result(Integer, {Name, string}) when is_integer(Integer) -> {struct, [{Name, integer_to_list(Integer)}]}; format_result(Other, {Name, string}) -> {struct, [{Name, io_lib:format("~p", [Other])}]}; format_result(String, {Name, binary}) when is_list(String) -> {struct, [{Name, lists:flatten(String)}]}; format_result(Binary, {Name, binary}) when is_binary(Binary) -> {struct, [{Name, binary_to_list(Binary)}]}; format_result(Code, {Name, rescode}) -> {struct, [{Name, make_status(Code)}]}; format_result({Code, Text}, {Name, restuple}) -> {struct, [{Name, make_status(Code)}, {text, io_lib:format("~s", [Text])}]}; format_result(Elements, {Name, {list, ElementsDef}}) -> FormattedList = lists:map(fun (Element) -> format_result(Element, ElementsDef) end, Elements), {struct, [{Name, {array, FormattedList}}]}; format_result(ElementsTuple, {Name, {tuple, ElementsDef}}) -> ElementsList = tuple_to_list(ElementsTuple), ElementsAndDef = lists:zip(ElementsList, ElementsDef), FormattedList = lists:map(fun ({Element, ElementDef}) -> format_result(Element, ElementDef) end, ElementsAndDef), {struct, [{Name, {array, FormattedList}}]}; format_result(404, {Name, _}) -> {struct, [{Name, make_status(not_found)}]}. make_status(ok) -> 0; make_status(true) -> 0; make_status(false) -> 1; make_status(error) -> 1; make_status(_) -> 1. listen_options() -> ?WARNING_MSG("It is deprecated defining ejabberd_xmlrpc as a listen module " "in the ejabberd configuration. Support for that configuration" " method may be removed in a future ejabberd release. You are " "encouraged to define ejabberd_xmlrpc inside request_handlers " "option of ejabberd_http listen module. See the ejabberd " "documentation for details: https://docs.ejabberd.im/admin/" "configuration/listen/#ejabberd-xmlrpc", []), []. ejabberd-21.12/src/ejabberd_sm_mnesia.erl0000644000232200023220000001175014154362354020722 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_sm_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sm_mnesia). -behaviour(gen_server). -behaviour(ejabberd_sm). %% API -export([init/0, use_cache/1, set_session/1, delete_session/1, get_sessions/0, get_sessions/1, get_sessions/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("ejabberd_sm.hrl"). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec use_cache(binary()) -> boolean(). use_cache(_LServer) -> false. -spec set_session(#session{}) -> ok. set_session(Session) -> mnesia:dirty_write(Session). -spec delete_session(#session{}) -> ok. delete_session(#session{sid = SID}) -> mnesia:dirty_delete(session, SID). -spec get_sessions() -> [#session{}]. get_sessions() -> ets:tab2list(session). -spec get_sessions(binary()) -> [#session{}]. get_sessions(LServer) -> mnesia:dirty_select(session, [{#session{usr = '$1', _ = '_'}, [{'==', {element, 2, '$1'}, LServer}], ['$_']}]). -spec get_sessions(binary(), binary()) -> {ok, [#session{}]}. get_sessions(LUser, LServer) -> {ok, mnesia:dirty_index_read(session, {LUser, LServer}, #session.us)}. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> update_tables(), ejabberd_mnesia:create(?MODULE, session, [{ram_copies, [node()]}, {attributes, record_info(fields, session)}, {index, [usr,us]}]), ejabberd_mnesia:create(?MODULE, session_counter, [{ram_copies, [node()]}, {attributes, record_info(fields, session_counter)}]), mnesia:subscribe(system), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> Sessions = ets:select( session, ets:fun2ms( fun(#session{sid = {_, Pid}} = S) when node(Pid) == Node -> S end)), lists:foreach( fun(S) -> mnesia:dirty_delete_object(S) end, Sessions), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== update_tables() -> case catch mnesia:table_info(session, attributes) of [ur, user, node] -> mnesia:delete_table(session); [ur, user, pid] -> mnesia:delete_table(session); [usr, us, pid] -> mnesia:delete_table(session); [usr, us, sid, priority, info] -> mnesia:delete_table(session); [sid, usr, us, priority] -> mnesia:delete_table(session); [sid, usr, us, priority, info] -> ok; {'EXIT', _} -> ok end, case lists:member(presence, mnesia:system_info(tables)) of true -> mnesia:delete_table(presence); false -> ok end, case lists:member(local_session, mnesia:system_info(tables)) of true -> mnesia:delete_table(local_session); false -> ok end. ejabberd-21.12/src/mod_offline.erl0000644000232200023220000012760214154362354017416 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_offline.erl %%% Author : Alexey Shchepin %%% Purpose : Store and manage offline messages. %%% Created : 5 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_offline). -author('alexey@process-one.net'). -protocol({xep, 13, '1.2'}). -protocol({xep, 22, '1.4'}). -protocol({xep, 23, '1.3'}). -protocol({xep, 160, '1.0'}). -protocol({xep, 334, '0.2'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, store_packet/1, store_offline_msg/1, c2s_self_presence/1, get_sm_features/5, get_sm_identity/5, get_sm_items/5, get_info/5, handle_offline_query/1, remove_expired_messages/1, remove_old_messages/2, remove_user/2, import_info/0, import_start/2, import/5, export/1, get_queue_length/2, count_offline_messages/2, get_offline_els/2, find_x_expire/2, c2s_handle_info/2, c2s_copy_session/2, webadmin_page/3, webadmin_user/4, webadmin_user_parse_query/5]). -export([mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). -deprecated({get_queue_length,2}). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("mod_offline.hrl"). -include("translate.hrl"). -define(OFFLINE_TABLE_LOCK_THRESHOLD, 1000). %% default value for the maximum number of user messages -define(MAX_USER_MESSAGES, infinity). -define(SPOOL_COUNTER_CACHE, offline_msg_counter_cache). -type c2s_state() :: ejabberd_c2s:state(). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(#offline_msg{}) -> ok. -callback store_message(#offline_msg{}) -> ok | {error, any()}. -callback pop_messages(binary(), binary()) -> {ok, [#offline_msg{}]} | {error, any()}. -callback remove_expired_messages(binary()) -> {atomic, any()}. -callback remove_old_messages(non_neg_integer(), binary()) -> {atomic, any()}. -callback remove_user(binary(), binary()) -> any(). -callback read_message_headers(binary(), binary()) -> [{non_neg_integer(), jid(), jid(), undefined | erlang:timestamp(), xmlel()}] | error. -callback read_message(binary(), binary(), non_neg_integer()) -> {ok, #offline_msg{}} | error. -callback remove_message(binary(), binary(), non_neg_integer()) -> ok | {error, any()}. -callback read_all_messages(binary(), binary()) -> [#offline_msg{}]. -callback remove_all_messages(binary(), binary()) -> {atomic, any()}. -callback count_messages(binary(), binary()) -> {ets_cache:tag(), non_neg_integer()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([remove_expired_messages/1, remove_old_messages/2, use_cache/1, cache_nodes/1]). depends(_Host, _Opts) -> []. start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, store_packet, 50), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:add(disco_info, Host, ?MODULE, get_info, 50), ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), ejabberd_hooks:add(webadmin_user_parse_query, Host, ?MODULE, webadmin_user_parse_query, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE, ?MODULE, handle_offline_query). stop(Host) -> ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, store_packet, 50), ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:delete(disco_info, Host, ?MODULE, get_info, 50), ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, c2s_handle_info, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), ejabberd_hooks:delete(webadmin_user_parse_query, Host, ?MODULE, webadmin_user_parse_query, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_FLEX_OFFLINE). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), init_cache(NewMod, Host, NewOpts), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end. init_cache(Mod, Host, Opts) -> CacheOpts = [{max_size, mod_offline_opt:cache_size(Opts)}, {life_time, mod_offline_opt:cache_life_time(Opts)}, {cache_missed, false}], case use_cache(Mod, Host) of true -> ets_cache:new(?SPOOL_COUNTER_CACHE, CacheOpts); false -> ets_cache:delete(?SPOOL_COUNTER_CACHE) end. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_offline_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec flush_cache(module(), binary(), binary()) -> ok. flush_cache(Mod, User, Server) -> case use_cache(Mod, Server) of true -> ets_cache:delete(?SPOOL_COUNTER_CACHE, {User, Server}, cache_nodes(Mod, Server)); false -> ok end. -spec store_offline_msg(#offline_msg{}) -> ok | {error, full | any()}. store_offline_msg(#offline_msg{us = {User, Server}, packet = Pkt} = Msg) -> UseMam = use_mam_for_user(User, Server), Mod = gen_mod:db_mod(Server, ?MODULE), case UseMam andalso xmpp:get_meta(Pkt, mam_archived, false) of true -> case count_offline_messages(User, Server) of 0 -> store_message_in_db(Mod, Msg); _ -> case use_cache(Mod, Server) of true -> ets_cache:incr( ?SPOOL_COUNTER_CACHE, {User, Server}, 1, cache_nodes(Mod, Server)); false -> ok end end; false -> case get_max_user_messages(User, Server) of infinity -> store_message_in_db(Mod, Msg); Limit -> Num = count_offline_messages(User, Server), if Num < Limit -> store_message_in_db(Mod, Msg); true -> {error, full} end end end. get_max_user_messages(User, Server) -> Access = mod_offline_opt:access_max_user_messages(Server), case ejabberd_shaper:match(Server, Access, jid:make(User, Server)) of Max when is_integer(Max) -> Max; infinity -> infinity; _ -> ?MAX_USER_MESSAGES end. get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> Feats = case Acc of {result, I} -> I; _ -> [] end, {result, Feats ++ [?NS_FEATURE_MSGOFFLINE, ?NS_FLEX_OFFLINE]}; get_sm_features(_Acc, _From, _To, ?NS_FEATURE_MSGOFFLINE, _Lang) -> %% override all lesser features... {result, []}; get_sm_features(_Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, _Lang) -> {result, [?NS_FLEX_OFFLINE]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. get_sm_identity(Acc, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, _Lang) -> [#identity{category = <<"automation">>, type = <<"message-list">>}|Acc]; get_sm_identity(Acc, _From, _To, _Node, _Lang) -> Acc. get_sm_items(_Acc, #jid{luser = U, lserver = S} = JID, #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, _Lang) -> ejabberd_sm:route(JID, {resend_offline, false}), Mod = gen_mod:db_mod(S, ?MODULE), Hdrs = case Mod:read_message_headers(U, S) of L when is_list(L) -> L; _ -> [] end, BareJID = jid:remove_resource(JID), {result, lists:map( fun({Seq, From, _To, _TS, _El}) -> Node = integer_to_binary(Seq), #disco_item{jid = BareJID, node = Node, name = jid:encode(From)} end, Hdrs)}; get_sm_items(Acc, _From, _To, _Node, _Lang) -> Acc. -spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. get_info(_Acc, #jid{luser = U, lserver = S} = JID, #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, Lang) -> ejabberd_sm:route(JID, {resend_offline, false}), [#xdata{type = result, fields = flex_offline:encode( [{number_of_messages, count_offline_messages(U, S)}], Lang)}]; get_info(Acc, _From, _To, _Node, _Lang) -> Acc. -spec c2s_handle_info(c2s_state(), term()) -> c2s_state(). c2s_handle_info(State, {resend_offline, Flag}) -> {stop, State#{resend_offline => Flag}}; c2s_handle_info(State, _) -> State. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{resend_offline := Flag}) -> State#{resend_offline => Flag}; c2s_copy_session(State, _) -> State. -spec handle_offline_query(iq()) -> iq(). handle_offline_query(#iq{from = #jid{luser = U1, lserver = S1}, to = #jid{luser = U2, lserver = S2}, lang = Lang, sub_els = [#offline{}]} = IQ) when {U1, S1} /= {U2, S2} -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); handle_offline_query(#iq{from = #jid{luser = U, lserver = S} = From, to = #jid{luser = U, lserver = S} = _To, type = Type, lang = Lang, sub_els = [#offline{} = Offline]} = IQ) -> case {Type, Offline} of {get, #offline{fetch = true, items = [], purge = false}} -> %% TODO: report database errors handle_offline_fetch(From), xmpp:make_iq_result(IQ); {get, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> case handle_offline_items_view(From, Items) of true -> xmpp:make_iq_result(IQ); false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) end; {set, #offline{fetch = false, items = [], purge = true}} -> case delete_all_msgs(U, S) of {atomic, ok} -> xmpp:make_iq_result(IQ); _Err -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; {set, #offline{fetch = false, items = [_|_] = Items, purge = false}} -> case handle_offline_items_remove(From, Items) of true -> xmpp:make_iq_result(IQ); false -> xmpp:make_error(IQ, xmpp:err_item_not_found()) end; _ -> xmpp:make_error(IQ, xmpp:err_bad_request()) end; handle_offline_query(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec handle_offline_items_view(jid(), [offline_item()]) -> boolean(). handle_offline_items_view(JID, Items) -> {U, S, R} = jid:tolower(JID), case use_mam_for_user(U, S) of true -> false; _ -> lists:foldl( fun(#offline_item{node = Node, action = view}, Acc) -> case fetch_msg_by_node(JID, Node) of {ok, OfflineMsg} -> case offline_msg_to_route(S, OfflineMsg) of {route, El} -> NewEl = set_offline_tag(El, Node), case ejabberd_sm:get_session_pid(U, S, R) of Pid when is_pid(Pid) -> ejabberd_c2s:route(Pid, {route, NewEl}); none -> ok end, Acc or true; error -> Acc or false end; error -> Acc or false end end, false, Items) end. -spec handle_offline_items_remove(jid(), [offline_item()]) -> boolean(). handle_offline_items_remove(JID, Items) -> {U, S, _R} = jid:tolower(JID), case use_mam_for_user(U, S) of true -> false; _ -> lists:foldl( fun(#offline_item{node = Node, action = remove}, Acc) -> Acc or remove_msg_by_node(JID, Node) end, false, Items) end. -spec set_offline_tag(message(), binary()) -> message(). set_offline_tag(Msg, Node) -> xmpp:set_subtag(Msg, #offline{items = [#offline_item{node = Node}]}). -spec handle_offline_fetch(jid()) -> ok. handle_offline_fetch(#jid{luser = U, lserver = S} = JID) -> ejabberd_sm:route(JID, {resend_offline, false}), lists:foreach( fun({Node, El}) -> El1 = set_offline_tag(El, Node), ejabberd_router:route(El1) end, read_messages(U, S)). -spec fetch_msg_by_node(jid(), binary()) -> error | {ok, #offline_msg{}}. fetch_msg_by_node(To, Seq) -> case catch binary_to_integer(Seq) of I when is_integer(I), I >= 0 -> LUser = To#jid.luser, LServer = To#jid.lserver, Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:read_message(LUser, LServer, I); _ -> error end. -spec remove_msg_by_node(jid(), binary()) -> boolean(). remove_msg_by_node(To, Seq) -> case catch binary_to_integer(Seq) of I when is_integer(I), I>= 0 -> LUser = To#jid.luser, LServer = To#jid.lserver, Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_message(LUser, LServer, I), flush_cache(Mod, LUser, LServer), true; _ -> false end. -spec need_to_store(binary(), message()) -> boolean(). need_to_store(_LServer, #message{type = error}) -> false; need_to_store(LServer, #message{type = Type} = Packet) -> case xmpp:has_subtag(Packet, #offline{}) of false -> case misc:unwrap_mucsub_message(Packet) of #message{type = groupchat} = Msg -> need_to_store(LServer, Msg#message{type = chat}); #message{} = Msg -> need_to_store(LServer, Msg); _ -> case check_store_hint(Packet) of store -> true; no_store -> false; none -> Store = case Type of groupchat -> mod_offline_opt:store_groupchat(LServer); headline -> false; _ -> true end, case {Store, mod_offline_opt:store_empty_body(LServer)} of {false, _} -> false; {_, true} -> true; {_, false} -> Packet#message.body /= []; {_, unless_chat_state} -> not misc:is_standalone_chat_state(Packet) end end end; true -> false end. -spec store_packet({any(), message()}) -> {any(), message()}. store_packet({_Action, #message{from = From, to = To} = Packet} = Acc) -> case need_to_store(To#jid.lserver, Packet) of true -> case check_event(Packet) of true -> #jid{luser = LUser, lserver = LServer} = To, TimeStamp = erlang:timestamp(), Expire = find_x_expire(TimeStamp, Packet), OffMsg = #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp, expire = Expire, from = From, to = To, packet = Packet}, case store_offline_msg(OffMsg) of ok -> {offlined, Packet}; {error, Reason} -> discard_warn_sender(Packet, Reason), stop end; _ -> maybe_update_cache(To, Packet), Acc end; false -> maybe_update_cache(To, Packet), Acc end. -spec maybe_update_cache(jid(), message()) -> ok. maybe_update_cache(#jid{lserver = Server, luser = User}, Packet) -> case xmpp:get_meta(Packet, mam_archived, false) of true -> Mod = gen_mod:db_mod(Server, ?MODULE), case use_mam_for_user(User, Server) andalso use_cache(Mod, Server) of true -> ets_cache:incr( ?SPOOL_COUNTER_CACHE, {User, Server}, 1, cache_nodes(Mod, Server)); _ -> ok end; _ -> ok end. -spec check_store_hint(message()) -> store | no_store | none. check_store_hint(Packet) -> case has_store_hint(Packet) of true -> store; false -> case has_no_store_hint(Packet) of true -> no_store; false -> none end end. -spec has_store_hint(message()) -> boolean(). has_store_hint(Packet) -> xmpp:has_subtag(Packet, #hint{type = 'store'}). -spec has_no_store_hint(message()) -> boolean(). has_no_store_hint(Packet) -> xmpp:has_subtag(Packet, #hint{type = 'no-store'}) orelse xmpp:has_subtag(Packet, #hint{type = 'no-storage'}). %% Check if the packet has any content about XEP-0022 -spec check_event(message()) -> boolean(). check_event(#message{from = From, to = To, id = ID, type = Type} = Msg) -> case xmpp:get_subtag(Msg, #xevent{}) of false -> true; #xevent{id = undefined, offline = false} -> true; #xevent{id = undefined, offline = true} -> NewMsg = #message{from = To, to = From, id = ID, type = Type, sub_els = [#xevent{id = ID, offline = true}]}, ejabberd_router:route(NewMsg), true; % Don't store composing events #xevent{id = V, composing = true} when V /= undefined -> false; % Nor composing stopped events #xevent{id = V, composing = false, delivered = false, displayed = false, offline = false} when V /= undefined -> false; % But store other received notifications #xevent{id = V} when V /= undefined -> true; _ -> false end. -spec find_x_expire(erlang:timestamp(), message()) -> erlang:timestamp() | never. find_x_expire(TimeStamp, Msg) -> case xmpp:get_subtag(Msg, #expire{seconds = 0}) of #expire{seconds = Int} -> {MegaSecs, Secs, MicroSecs} = TimeStamp, S = MegaSecs * 1000000 + Secs + Int, MegaSecs1 = S div 1000000, Secs1 = S rem 1000000, {MegaSecs1, Secs1, MicroSecs}; false -> never end. c2s_self_presence({_Pres, #{resend_offline := false}} = Acc) -> Acc; c2s_self_presence({#presence{type = available} = NewPres, State} = Acc) -> NewPrio = get_priority_from_presence(NewPres), LastPrio = case maps:get(pres_last, State, undefined) of undefined -> -1; LastPres -> get_priority_from_presence(LastPres) end, if LastPrio < 0 andalso NewPrio >= 0 -> route_offline_messages(State); true -> ok end, Acc; c2s_self_presence(Acc) -> Acc. -spec route_offline_messages(c2s_state()) -> ok. route_offline_messages(#{jid := #jid{luser = LUser, lserver = LServer}} = State) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Msgs = case Mod:pop_messages(LUser, LServer) of {ok, OffMsgs} -> case use_mam_for_user(LUser, LServer) of true -> flush_cache(Mod, LUser, LServer), lists:map( fun({_, #message{from = From, to = To} = Msg}) -> #offline_msg{from = From, to = To, us = {LUser, LServer}, packet = Msg} end, read_mam_messages(LUser, LServer, OffMsgs)); _ -> flush_cache(Mod, LUser, LServer), OffMsgs end; _ -> [] end, lists:foreach( fun(OffMsg) -> route_offline_message(State, OffMsg) end, Msgs). -spec route_offline_message(c2s_state(), #offline_msg{}) -> ok. route_offline_message(#{lserver := LServer} = State, #offline_msg{expire = Expire} = OffMsg) -> case offline_msg_to_route(LServer, OffMsg) of error -> ok; {route, Msg} -> case is_message_expired(Expire, Msg) of true -> ok; false -> case privacy_check_packet(State, Msg, in) of allow -> ejabberd_router:route(Msg); deny -> ok end end end. -spec is_message_expired(erlang:timestamp() | never, message()) -> boolean(). is_message_expired(Expire, Msg) -> TS = erlang:timestamp(), Expire1 = case Expire of undefined -> find_x_expire(TS, Msg); _ -> Expire end, Expire1 /= never andalso Expire1 =< TS. -spec privacy_check_packet(c2s_state(), stanza(), in | out) -> allow | deny. privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> ejabberd_hooks:run_fold(privacy_check_packet, LServer, allow, [State, Pkt, Dir]). remove_expired_messages(Server) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, remove_expired_messages, 1) of true -> Ret = Mod:remove_expired_messages(LServer), ets_cache:clear(?SPOOL_COUNTER_CACHE), Ret; false -> erlang:error(not_implemented) end. remove_old_messages(Days, Server) -> LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, remove_old_messages, 2) of true -> Ret = Mod:remove_old_messages(Days, LServer), ets_cache:clear(?SPOOL_COUNTER_CACHE), Ret; false -> erlang:error(not_implemented) end. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), flush_cache(Mod, LUser, LServer). %% Helper functions: -spec check_if_message_should_be_bounced(message()) -> boolean(). check_if_message_should_be_bounced(Packet) -> case Packet of #message{type = groupchat, to = #jid{lserver = LServer}} -> mod_offline_opt:bounce_groupchat(LServer); #message{to = #jid{lserver = LServer}} -> case misc:is_mucsub_message(Packet) of true -> mod_offline_opt:bounce_groupchat(LServer); _ -> true end; _ -> true end. %% Warn senders that their messages have been discarded: -spec discard_warn_sender(message(), full | any()) -> ok. discard_warn_sender(Packet, Reason) -> case check_if_message_should_be_bounced(Packet) of true -> Lang = xmpp:get_lang(Packet), Err = case Reason of full -> ErrText = ?T("Your contact offline message queue is " "full. The message has been discarded."), xmpp:err_resource_constraint(ErrText, Lang); _ -> ErrText = ?T("Database failure"), xmpp:err_internal_server_error(ErrText, Lang) end, ejabberd_router:route_error(Packet, Err); _ -> ok end. webadmin_page(_, Host, #request{us = _US, path = [<<"user">>, U, <<"queue">>], q = Query, lang = Lang} = _Request) -> Res = user_queue(U, Host, Query, Lang), {stop, Res}; webadmin_page(Acc, _, _) -> Acc. get_offline_els(LUser, LServer) -> [Packet || {_Seq, Packet} <- read_messages(LUser, LServer)]. -spec offline_msg_to_route(binary(), #offline_msg{}) -> {route, message()} | error. offline_msg_to_route(LServer, #offline_msg{from = From, to = To} = R) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(R#offline_msg.packet, ?NS_CLIENT, CodecOpts) of Pkt -> Pkt1 = xmpp:set_from_to(Pkt, From, To), Pkt2 = add_delay_info(Pkt1, LServer, R#offline_msg.timestamp), {route, Pkt2} catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode packet ~p of user ~ts: ~ts", [R#offline_msg.packet, jid:encode(To), xmpp:format_error(Why)]), error end. -spec read_messages(binary(), binary()) -> [{binary(), message()}]. read_messages(LUser, LServer) -> Res = case read_db_messages(LUser, LServer) of error -> []; L when is_list(L) -> L end, case use_mam_for_user(LUser, LServer) of true -> read_mam_messages(LUser, LServer, Res); _ -> Res end. -spec read_db_messages(binary(), binary()) -> [{binary(), message()}] | error. read_db_messages(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), CodecOpts = ejabberd_config:codec_options(), case Mod:read_message_headers(LUser, LServer) of error -> error; L -> lists:flatmap( fun({Seq, From, To, TS, El}) -> Node = integer_to_binary(Seq), try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of Pkt -> Node = integer_to_binary(Seq), Pkt1 = add_delay_info(Pkt, LServer, TS), Pkt2 = xmpp:set_from_to(Pkt1, From, To), [{Node, Pkt2}] catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode packet ~p " "of user ~ts: ~ts", [El, jid:encode(To), xmpp:format_error(Why)]), [] end end, L) end. -spec parse_marker_messages(binary(), [#offline_msg{} | {any(), message()}]) -> {integer() | none, [message()]}. parse_marker_messages(LServer, ReadMsgs) -> {Timestamp, ExtraMsgs} = lists:foldl( fun({_Node, #message{id = <<"ActivityMarker">>, body = [], type = error} = Msg}, {T, E}) -> case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of #delay{stamp = Time} -> if T == none orelse T > Time -> {Time, E}; true -> {T, E} end end; (#offline_msg{from = From, to = To, timestamp = TS, packet = Pkt}, {T, E}) -> try xmpp:decode(Pkt) of #message{id = <<"ActivityMarker">>, body = [], type = error} = Msg -> TS2 = case TS of undefined -> case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of #delay{stamp = TS0} -> TS0; _ -> erlang:timestamp() end; _ -> TS end, if T == none orelse T > TS2 -> {TS2, E}; true -> {T, E} end; Decoded -> Pkt1 = add_delay_info(Decoded, LServer, TS), {T, [xmpp:set_from_to(Pkt1, From, To) | E]} catch _:{xmpp_codec, _Why} -> {T, E} end; ({_Node, Msg}, {T, E}) -> {T, [Msg | E]} end, {none, []}, ReadMsgs), Start = case {Timestamp, ExtraMsgs} of {none, [First|_]} -> case xmpp:get_subtag(First, #delay{stamp = {0,0,0}}) of #delay{stamp = {Mega, Sec, Micro}} -> {Mega, Sec, Micro+1}; _ -> none end; {none, _} -> none; _ -> Timestamp end, {Start, ExtraMsgs}. -spec read_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}]) -> [{integer(), message()}]. read_mam_messages(LUser, LServer, ReadMsgs) -> {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), AllMsgs = case Start of none -> ExtraMsgs; _ -> MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of Number when is_integer(Number) -> max(0, Number - length(ExtraMsgs)); infinity -> undefined end, JID = jid:make(LUser, LServer, <<>>), {MamMsgs, _, _} = mod_mam:select(LServer, JID, JID, [{start, Start}], #rsm_set{max = MaxOfflineMsgs, before = <<"9999999999999999">>}, chat, only_messages), MamMsgs2 = lists:map( fun({_, _, #forwarded{sub_els = [MM | _], delay = #delay{stamp = MMT}}}) -> add_delay_info(MM, LServer, MMT) end, MamMsgs), ExtraMsgs ++ MamMsgs2 end, AllMsgs2 = lists:sort( fun(A, B) -> DA = case xmpp:get_subtag(A, #stanza_id{by = #jid{}}) of #stanza_id{id = IDA} -> IDA; _ -> case xmpp:get_subtag(A, #delay{stamp = {0,0,0}}) of #delay{stamp = STA} -> integer_to_binary(misc:now_to_usec(STA)); _ -> <<"unknown">> end end, DB = case xmpp:get_subtag(B, #stanza_id{by = #jid{}}) of #stanza_id{id = IDB} -> IDB; _ -> case xmpp:get_subtag(B, #delay{stamp = {0,0,0}}) of #delay{stamp = STB} -> integer_to_binary(misc:now_to_usec(STB)); _ -> <<"unknown">> end end, DA < DB end, AllMsgs), {AllMsgs3, _} = lists:mapfoldl( fun(Msg, Counter) -> {{Counter, Msg}, Counter + 1} end, 1, AllMsgs2), AllMsgs3. -spec count_mam_messages(binary(), binary(), [#offline_msg{} | {any(), message()}] | error) -> {cache, integer()} | {nocache, integer()}. count_mam_messages(_LUser, _LServer, error) -> {nocache, 0}; count_mam_messages(LUser, LServer, ReadMsgs) -> {Start, ExtraMsgs} = parse_marker_messages(LServer, ReadMsgs), case Start of none -> {cache, length(ExtraMsgs)}; _ -> MaxOfflineMsgs = case get_max_user_messages(LUser, LServer) of Number when is_integer(Number) -> Number - length(ExtraMsgs); infinity -> undefined end, JID = jid:make(LUser, LServer, <<>>), {_, _, Count} = mod_mam:select(LServer, JID, JID, [{start, Start}], #rsm_set{max = MaxOfflineMsgs, before = <<"9999999999999999">>}, chat, only_count), {cache, Count + length(ExtraMsgs)} end. format_user_queue(Hdrs) -> lists:map( fun({Seq, From, To, TS, El}) -> ID = integer_to_binary(Seq), FPacket = ejabberd_web_admin:pretty_print_xml(El), SFrom = jid:encode(From), STo = jid:encode(To), Time = case TS of undefined -> Stamp = fxml:get_path_s(El, [{elem, <<"delay">>}, {attr, <<"stamp">>}]), try xmpp_util:decode_timestamp(Stamp) of {_, _, _} = Now -> format_time(Now) catch _:_ -> <<"">> end; {_, _, _} = Now -> format_time(Now) end, ?XE(<<"tr">>, [?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], [?INPUT(<<"checkbox">>, <<"selected">>, ID)]), ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], Time), ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], SFrom), ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], STo), ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], [?XC(<<"pre">>, FPacket)])]) end, Hdrs). format_time(Now) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]). user_queue(User, Server, Query, Lang) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, Mod = gen_mod:db_mod(LServer, ?MODULE), user_queue_parse_query(LUser, LServer, Query), HdrsAll = case Mod:read_message_headers(LUser, LServer) of error -> []; L -> L end, Hdrs = get_messages_subset(User, Server, HdrsAll), FMsgs = format_user_queue(Hdrs), PageTitle = str:translate_and_format(Lang, ?T("~ts's Offline Messages Queue"), [us_to_list(US)]), (?H1GL(PageTitle, <<"modules/#mod-offline">>, <<"mod_offline">>)) ++ [?XREST(?T("Submitted"))] ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?X(<<"td">>), ?XCT(<<"td">>, ?T("Time")), ?XCT(<<"td">>, ?T("From")), ?XCT(<<"td">>, ?T("To")), ?XCT(<<"td">>, ?T("Packet"))])]), ?XE(<<"tbody">>, if FMsgs == [] -> [?XE(<<"tr">>, [?XAC(<<"td">>, [{<<"colspan">>, <<"4">>}], <<" ">>)])]; true -> FMsgs end)]), ?BR, ?INPUTTD(<<"submit">>, <<"delete">>, ?T("Delete Selected"))])]. user_queue_parse_query(LUser, LServer, Query) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case lists:keysearch(<<"delete">>, 1, Query) of {value, _} -> case user_queue_parse_query(LUser, LServer, Query, Mod, false) of true -> flush_cache(Mod, LUser, LServer); false -> ok end; _ -> ok end. user_queue_parse_query(LUser, LServer, Query, Mod, Acc) -> case lists:keytake(<<"selected">>, 1, Query) of {value, {_, Seq}, Query2} -> NewAcc = case catch binary_to_integer(Seq) of I when is_integer(I), I>=0 -> Mod:remove_message(LUser, LServer, I), true; _ -> Acc end, user_queue_parse_query(LUser, LServer, Query2, Mod, NewAcc); false -> Acc end. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). get_queue_length(LUser, LServer) -> count_offline_messages(LUser, LServer). get_messages_subset(User, Host, MsgsAll) -> MaxOfflineMsgs = case get_max_user_messages(User, Host) of Number when is_integer(Number) -> Number; _ -> 100 end, Length = length(MsgsAll), get_messages_subset2(MaxOfflineMsgs, Length, MsgsAll). get_messages_subset2(Max, Length, MsgsAll) when Length =< Max * 2 -> MsgsAll; get_messages_subset2(Max, Length, MsgsAll) -> FirstN = Max, {MsgsFirstN, Msgs2} = lists:split(FirstN, MsgsAll), MsgsLastN = lists:nthtail(Length - FirstN - FirstN, Msgs2), NoJID = jid:make(<<"...">>, <<"...">>), Seq = <<"0">>, IntermediateMsg = #xmlel{name = <<"...">>, attrs = [], children = []}, MsgsFirstN ++ [{Seq, NoJID, NoJID, IntermediateMsg}] ++ MsgsLastN. webadmin_user(Acc, User, Server, Lang) -> QueueLen = count_offline_messages(jid:nodeprep(User), jid:nameprep(Server)), FQueueLen = ?C(integer_to_binary(QueueLen)), FQueueView = ?AC(<<"queue/">>, ?T("View Queue")), Acc ++ [?XCT(<<"h3">>, ?T("Offline Messages:")), FQueueLen, ?C(<<" | ">>), FQueueView, ?C(<<" | ">>), ?INPUTTD(<<"submit">>, <<"removealloffline">>, ?T("Remove All Offline Messages"))]. -spec delete_all_msgs(binary(), binary()) -> {atomic, any()}. delete_all_msgs(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Ret = Mod:remove_all_messages(LUser, LServer), flush_cache(Mod, LUser, LServer), Ret. webadmin_user_parse_query(_, <<"removealloffline">>, User, Server, _Query) -> case delete_all_msgs(User, Server) of {atomic, ok} -> ?INFO_MSG("Removed all offline messages for ~ts@~ts", [User, Server]), {stop, ok}; Err -> ?ERROR_MSG("Failed to remove offline messages: ~p", [Err]), {stop, error} end; webadmin_user_parse_query(Acc, _Action, _User, _Server, _Query) -> Acc. %% Returns as integer the number of offline messages for a given user -spec count_offline_messages(binary(), binary()) -> non_neg_integer(). count_offline_messages(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_mam_for_user(User, Server) of true -> case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?SPOOL_COUNTER_CACHE, {LUser, LServer}, fun() -> Res = read_db_messages(LUser, LServer), count_mam_messages(LUser, LServer, Res) end); false -> Res = read_db_messages(LUser, LServer), ets_cache:untag(count_mam_messages(LUser, LServer, Res)) end; _ -> case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?SPOOL_COUNTER_CACHE, {LUser, LServer}, fun() -> Mod:count_messages(LUser, LServer) end); false -> ets_cache:untag(Mod:count_messages(LUser, LServer)) end end. -spec store_message_in_db(module(), #offline_msg{}) -> ok | {error, any()}. store_message_in_db(Mod, #offline_msg{us = {User, Server}} = Msg) -> case Mod:store_message(Msg) of ok -> case use_cache(Mod, Server) of true -> ets_cache:incr( ?SPOOL_COUNTER_CACHE, {User, Server}, 1, cache_nodes(Mod, Server)); false -> ok end; Err -> Err end. -spec add_delay_info(message(), binary(), undefined | erlang:timestamp()) -> message(). add_delay_info(Packet, LServer, TS) -> NewTS = case TS of undefined -> erlang:timestamp(); _ -> TS end, Packet1 = xmpp:put_meta(Packet, from_offline, true), misc:add_delay_info(Packet1, jid:make(LServer), NewTS, <<"Offline storage">>). -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of undefined -> 0; _ -> Prio end. export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"spool">>, 4}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, []). import(LServer, {sql, _}, DBType, <<"spool">>, [LUser, XML, _Seq, _TimeStamp]) -> El = fxml_stream:parse_element(XML), #message{from = From, to = To} = Msg = xmpp:decode(El, ?NS_CLIENT, [ignore_els]), TS = case xmpp:get_subtag(Msg, #delay{stamp = {0,0,0}}) of #delay{stamp = {MegaSecs, Secs, _}} -> {MegaSecs, Secs, 0}; false -> erlang:timestamp() end, US = {LUser, LServer}, Expire = find_x_expire(TS, Msg), OffMsg = #offline_msg{us = US, packet = El, from = From, to = To, timestamp = TS, expire = Expire}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(OffMsg). use_mam_for_user(_User, Server) -> mod_offline_opt:use_mam_for_storage(Server). mod_opt_type(access_max_user_messages) -> econf:shaper(); mod_opt_type(store_groupchat) -> econf:bool(); mod_opt_type(bounce_groupchat) -> econf:bool(); mod_opt_type(use_mam_for_storage) -> econf:bool(); mod_opt_type(store_empty_body) -> econf:either( unless_chat_state, econf:bool()); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {access_max_user_messages, max_user_offline_messages}, {store_empty_body, unless_chat_state}, {use_mam_for_storage, false}, {bounce_groupchat, false}, {store_groupchat, false}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0160.html" "[XEP-0160: Best Practices for Handling Offline Messages] " "and https://xmpp.org/extensions/xep-0013.html" "[XEP-0013: Flexible Offline Message Retrieval]. " "This means that all messages sent to an offline user " "will be stored on the server until that user comes online " "again. Thus it is very similar to how email works. A user " "is considered offline if no session presence priority > 0 " "are currently open."), "", ?T("NOTE: 'ejabberdctl' has a command to " "delete expired messages (see chapter " "https://docs.ejabberd.im/admin/guide/managing" "[Managing an ejabberd server] in online documentation.")], opts => [{access_max_user_messages, #{value => ?T("AccessName"), desc => ?T("This option defines which access rule will be " "enforced to limit the maximum number of offline " "messages that a user can have (quota). When a user " "has too many offline messages, any new messages that " "they receive are discarded, and a " "error is returned to the sender. The default value is " "'max_user_offline_messages'.")}}, {store_empty_body, #{value => "true | false | unless_chat_state", desc => ?T("Whether or not to store messages that lack a " "element. The default value is 'unless_chat_state', " "which tells ejabberd to store messages even if they " "lack the element, unless they only contain a " "chat state notification (as defined in " "https://xmpp.org/extensions/xep-0085.html" "[XEP-0085: Chat State Notifications].")}}, {store_groupchat, #{value => "true | false", desc => ?T("Whether or not to store groupchat messages. " "The default value is 'false'.")}}, {use_mam_for_storage, #{value => "true | false", desc => ?T("This is an experimental option. Enabling this option " "will make 'mod_offline' not use the former spool " "table for storing MucSub offline messages, but will " "use the archive table instead. This use of the archive " "table is cleaner and it makes it possible for clients " "to slowly drop the former offline use case and rely on " "message archive instead. It also further reduces the " "storage required when you enabled MucSub. Enabling this " "option has a known drawback for the moment: most of " "flexible message retrieval queries don't work (those that " "allow retrieval/deletion of messages by id), but this " "specification is not widely used. The default value " "is 'false' to keep former behaviour as default and " "ensure this option is disabled.")}}, {bounce_groupchat, #{value => "true | false", desc => ?T("This option is use the disable an optimisation that " "avoids bouncing error messages when groupchat messages " "could not be stored as offline. It will reduce chat " "room load, without any drawback in standard use cases. " "You may change default value only if you have a custom " "module which uses offline hook after 'mod_offline'. This " "option can be useful for both standard MUC and MucSub, " "but the bounce is much more likely to happen in the context " "of MucSub, so it is even more important to have it on " "large MucSub services. The default value is 'false', meaning " "the optimisation is enabled.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => [{?T("This example allows power users to have as much as 5000 " "offline messages, administrators up to 2000, and all the " "other users up to 100:"), ["acl:", " admin:", " user:", " - admin1@localhost", " - admin2@example.org", " poweruser:", " user:", " - bob@example.org", " - jane@example.org", "", "shaper_rules:", " max_user_offline_messages:", " - 5000: poweruser", " - 2000: admin", " - 100", "", "modules:", " ...", " mod_offline:", " access_max_user_messages: max_user_offline_messages", " ..." ]}]}. ejabberd-21.12/src/node_flat_sql.erl0000644000232200023220000011267414154362354017752 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : node_flat_sql.erl %%% Author : Christophe Romain %%% Purpose : Standard PubSub node plugin with ODBC backend %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the default PubSub plugin. %%%

It is used as a default for all unknown PubSub node type. It can serve %%% as a developer basis and reference to build its own custom pubsub node %%% types.

%%%

PubSub plugin nodes are using the {@link gen_node} behaviour.

-module(node_flat_sql). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_sql_pt.hrl"). -include("translate.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/2, remove_extra_items/3, remove_expired_items/2, get_entity_affiliations/2, get_node_affiliations/1, get_affiliation/2, set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, get_subscriptions/2, set_subscriptions/4, get_pending_nodes/2, get_states/1, get_state/2, set_state/1, get_items/7, get_items/3, get_item/7, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1, get_entity_subscriptions_for_send_last/2, get_last_items/3, get_only_item/2]). -export([decode_jid/1, encode_jid/1, encode_jid_like/1, decode_affiliation/1, decode_subscriptions/1, encode_affiliation/1, encode_subscriptions/1, encode_host/1, encode_host_like/1]). init(_Host, _ServerHost, _Opts) -> %%pubsub_subscription_sql:init(Host, ServerHost, Opts), ok. terminate(_Host, _ServerHost) -> ok. options() -> [{sql, true}, {rsm, true} | node_flat:options()]. features() -> [<<"rsm">> | node_flat:features()]. create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). create_node(Nidx, Owner) -> {_U, _S, _R} = OwnerKey = jid:tolower(jid:remove_resource(Owner)), J = encode_jid(OwnerKey), A = encode_affiliation(owner), S = encode_subscriptions([]), ejabberd_sql:sql_query_t( ?SQL("insert into pubsub_state(" "nodeid, jid, affiliation, subscriptions) " "values (%(Nidx)d, %(J)s, %(A)s, %(S)s)")), {result, {default, broadcast}}. delete_node(Nodes) -> Reply = lists:map( fun(#pubsub_node{id = Nidx} = PubsubNode) -> Subscriptions = case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(subscriptions)s " "from pubsub_state where nodeid=%(Nidx)d")) of {selected, RItems} -> [{decode_jid(SJID), decode_subscriptions(Subs)} || {SJID, Subs} <- RItems]; _ -> [] end, {PubsubNode, Subscriptions} end, Nodes), {result, {default, broadcast, Reply}}. subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, _Options) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = lists:member(Affiliation, [member, publisher, owner]), PendingSubscription = lists:any(fun ({pending, _}) -> true; (_) -> false end, Subscriptions), Owner = Affiliation == owner, if not Authorized -> {error, mod_pubsub:extended_error( xmpp:err_bad_request(), mod_pubsub:err_invalid_jid())}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; PendingSubscription -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_pending_subscription())}; (AccessModel == presence) and (not PresenceSubscription) and (not Owner) -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and (not RosterGroup) and (not Owner) -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and (not Whitelisted) and (not Owner) -> {error, mod_pubsub:extended_error( xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; %%ForbiddenAnonymous -> %% % Requesting entity is anonymous %% {error, ?ERR_FORBIDDEN}; true -> %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Subscriber, Nidx, Options), {NewSub, SubId} = case Subscriptions of [{subscribed, Id}|_] -> {subscribed, Id}; [] -> Id = pubsub_subscription_sql:make_subid(), Sub = case AccessModel of authorize -> pending; _ -> subscribed end, update_subscription(Nidx, SubKey, [{Sub, Id} | Subscriptions]), {Sub, Id} end, case {NewSub, SendLast} of {subscribed, never} -> {result, {default, subscribed, SubId}}; {subscribed, _} -> {result, {default, subscribed, SubId, send_last}}; {_, _} -> {result, {default, pending, SubId}} end end. unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> SubKey = jid:tolower(Subscriber), GenKey = jid:remove_resource(SubKey), Authorized = jid:tolower(jid:remove_resource(Sender)) == GenKey, {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, SubKey), SubIdExists = case SubId of <<>> -> false; Binary when is_binary(Binary) -> true; _ -> false end, if %% Requesting entity is prohibited from unsubscribing entity not Authorized -> {error, xmpp:err_forbidden()}; %% Entity did not specify SubId %%SubId == "", ?? -> %% {error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %% Invalid subscription identifier %%InvalidSubId -> %% {error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; %% Requesting entity is not a subscriber Subscriptions == [] -> {error, mod_pubsub:extended_error( xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())}; %% Subid supplied, so use that. SubIdExists -> Sub = first_in_list(fun ({_, S}) when S == SubId -> true; (_) -> false end, Subscriptions), case Sub of {value, S} -> delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions), {result, default}; false -> {error, mod_pubsub:extended_error( xmpp:err_unexpected_request(), mod_pubsub:err_not_subscribed())} end; %% Asking to remove all subscriptions to the given node SubId == all -> [delete_subscription(SubKey, Nidx, S, Affiliation, Subscriptions) || S <- Subscriptions], {result, default}; %% No subid supplied, but there's only one matching subscription length(Subscriptions) == 1 -> delete_subscription(SubKey, Nidx, hd(Subscriptions), Affiliation, Subscriptions), {result, default}; %% No subid and more than one possible subscription match. true -> {error, mod_pubsub:extended_error( xmpp:err_bad_request(), mod_pubsub:err_subid_required())} end. delete_subscription(SubKey, Nidx, {Subscription, SubId}, Affiliation, Subscriptions) -> NewSubs = Subscriptions -- [{Subscription, SubId}], %%pubsub_subscription_sql:unsubscribe_node(SubKey, Nidx, SubId), case {Affiliation, NewSubs} of {none, []} -> del_state(Nidx, SubKey); _ -> update_subscription(Nidx, SubKey, NewSubs) end. publish_item(Nidx, Publisher, PublishModel, MaxItems, ItemId, Payload, _PubOpts) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Subscribed = case PublishModel of subscribers -> node_flat:is_subscribed(Subscriptions); _ -> undefined end, if not ((PublishModel == open) or (PublishModel == publishers) and ((Affiliation == owner) or (Affiliation == publisher) or (Affiliation == publish_only)) or (Subscribed == true)) -> {error, xmpp:err_forbidden()}; true -> if MaxItems > 0; MaxItems == unlimited -> Now = erlang:timestamp(), case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}} = OldItem} -> set_item(OldItem#pubsub_item{ modification = {Now, SubKey}, payload = Payload}), {result, {default, broadcast, []}}; {result, _} -> {error, xmpp:err_forbidden()}; _ -> OldIds = maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId), set_item(#pubsub_item{ itemid = {ItemId, Nidx}, creation = {Now, GenKey}, modification = {Now, SubKey}, payload = Payload}), {result, {default, broadcast, OldIds}} end; true -> {result, {default, broadcast, []}} end end. remove_extra_items(Nidx, MaxItems) -> remove_extra_items(Nidx, MaxItems, itemids(Nidx)). remove_extra_items(_Nidx, unlimited, ItemIds) -> {result, {ItemIds, []}}; remove_extra_items(Nidx, MaxItems, ItemIds) -> NewItems = lists:sublist(ItemIds, MaxItems), OldItems = lists:nthtail(length(NewItems), ItemIds), del_items(Nidx, OldItems), {result, {NewItems, OldItems}}. remove_expired_items(_Nidx, infinity) -> {result, []}; remove_expired_items(Nidx, Seconds) -> ExpT = encode_now( misc:usec_to_now( erlang:system_time(microsecond) - (Seconds * 1000000))), case ejabberd_sql:sql_query_t( ?SQL("select @(itemid)s from pubsub_item where nodeid=%(Nidx)d " "and creation < %(ExpT)s")) of {selected, RItems} -> ItemIds = [ItemId || {ItemId} <- RItems], del_items(Nidx, ItemIds), {result, ItemIds}; _ -> {result, []} end. delete_item(Nidx, Publisher, PublishModel, ItemId) -> SubKey = jid:tolower(Publisher), GenKey = jid:remove_resource(SubKey), {result, Affiliation} = get_affiliation(Nidx, GenKey), Allowed = Affiliation == publisher orelse Affiliation == owner orelse (PublishModel == open andalso case get_item(Nidx, ItemId) of {result, #pubsub_item{creation = {_, GenKey}}} -> true; _ -> false end), if not Allowed -> {error, xmpp:err_forbidden()}; true -> Items = itemids(Nidx, GenKey), case lists:member(ItemId, Items) of true -> case del_item(Nidx, ItemId) of {updated, 1} -> {result, {default, broadcast}}; _ -> {error, xmpp:err_item_not_found()} end; false -> case Affiliation of owner -> case del_item(Nidx, ItemId) of {updated, 1} -> {result, {default, broadcast}}; _ -> {error, xmpp:err_item_not_found()} end; _ -> {error, xmpp:err_forbidden()} end end end. purge_node(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), GenState = get_state(Nidx, GenKey), case GenState of #pubsub_state{affiliation = owner} -> {result, States} = get_states(Nidx), lists:foreach(fun (#pubsub_state{items = []}) -> ok; (#pubsub_state{items = Items}) -> del_items(Nidx, Items) end, States), {result, {default, broadcast}}; _ -> {error, xmpp:err_forbidden()} end. get_entity_affiliations(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), H = encode_host(Host), J = encode_jid(GenKey), {result, case ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(affiliation)s " "from pubsub_state i, pubsub_node n where " "i.nodeid = n.nodeid and jid=%(J)s and host=%(H)s")) of {selected, RItems} -> [{nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), decode_affiliation(A)} || {N, T, I, A} <- RItems]; _ -> [] end}. get_node_affiliations(Nidx) -> {result, case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(affiliation)s from pubsub_state " "where nodeid=%(Nidx)d")) of {selected, RItems} -> [{decode_jid(J), decode_affiliation(A)} || {J, A} <- RItems]; _ -> [] end}. get_affiliation(Nidx, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), J = encode_jid(GenKey), {result, case ejabberd_sql:sql_query_t( ?SQL("select @(affiliation)s from pubsub_state " "where nodeid=%(Nidx)d and jid=%(J)s")) of {selected, [{A}]} -> decode_affiliation(A); _ -> none end}. set_affiliation(Nidx, Owner, Affiliation) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), {_, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey), case {Affiliation, Subscriptions} of {none, []} -> {result, del_state(Nidx, GenKey)}; _ -> {result, update_affiliation(Nidx, GenKey, Affiliation)} end. get_entity_subscriptions(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), H = encode_host(Host), GJ = encode_jid(GenKey), Query = case SubKey of GenKey -> GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " "where i.nodeid = n.nodeid and " "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); _ -> SJ = encode_jid(SubKey), ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n " "where i.nodeid = n.nodeid and " "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") end, {result, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> lists:foldl( fun({N, T, I, J, S}, Acc) -> Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), Jid = decode_jid(J), lists:foldl( fun({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid} | Acc2] end, Acc, decode_subscriptions(S)) end, [], RItems); _ -> [] end}. -spec get_entity_subscriptions_for_send_last(Host :: mod_pubsub:hostPubsub(), Owner :: jid()) -> {result, [{mod_pubsub:pubsubNode(), mod_pubsub:subscription(), mod_pubsub:subId(), ljid()}]}. get_entity_subscriptions_for_send_last(Host, Owner) -> SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), H = encode_host(Host), GJ = encode_jid(GenKey), Query = case SubKey of GenKey -> GJLike = <<(encode_jid_like(GenKey))/binary, "/%">>, ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n, pubsub_node_option o " "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " "name='send_last_published_item' and val='on_sub_and_presence' and " "(jid=%(GJ)s or jid like %(GJLike)s %ESCAPE) and host=%(H)s"); _ -> SJ = encode_jid(SubKey), ?SQL("select @(node)s, @(plugin)s, @(i.nodeid)d, @(jid)s, @(subscriptions)s " "from pubsub_state i, pubsub_node n, pubsub_node_option o " "where i.nodeid = n.nodeid and n.nodeid = o.nodeid and " "name='send_last_published_item' and val='on_sub_and_presence' and " "jid in (%(SJ)s, %(GJ)s) and host=%(H)s") end, {result, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> lists:foldl( fun ({N, T, I, J, S}, Acc) -> Node = nodetree_tree_sql:raw_to_node(Host, {N, <<"">>, T, I}), Jid = decode_jid(J), lists:foldl( fun ({Sub, SubId}, Acc2) -> [{Node, Sub, SubId, Jid}| Acc2] end, Acc, decode_subscriptions(S)) end, [], RItems); _ -> [] end}. get_node_subscriptions(Nidx) -> {result, case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(subscriptions)s from pubsub_state " "where nodeid=%(Nidx)d")) of {selected, RItems} -> lists:foldl( fun ({J, S}, Acc) -> Jid = decode_jid(J), lists:foldl( fun ({Sub, SubId}, Acc2) -> [{Jid, Sub, SubId} | Acc2] end, Acc, decode_subscriptions(S)) end, [], RItems); _ -> [] end}. get_subscriptions(Nidx, Owner) -> SubKey = jid:tolower(Owner), J = encode_jid(SubKey), {result, case ejabberd_sql:sql_query_t( ?SQL("select @(subscriptions)s from pubsub_state" " where nodeid=%(Nidx)d and jid=%(J)s")) of {selected, [{S}]} -> decode_subscriptions(S); _ -> [] end}. set_subscriptions(Nidx, Owner, Subscription, SubId) -> SubKey = jid:tolower(Owner), SubState = get_state_without_itemids(Nidx, SubKey), case {SubId, SubState#pubsub_state.subscriptions} of {_, []} -> case Subscription of none -> {error, mod_pubsub:extended_error( xmpp:err_bad_request(), mod_pubsub:err_not_subscribed())}; _ -> new_subscription(Nidx, Owner, Subscription, SubState) end; {<<>>, [{_, SID}]} -> case Subscription of none -> unsub_with_subid(Nidx, SID, SubState); _ -> replace_subscription({Subscription, SID}, SubState) end; {<<>>, [_ | _]} -> {error, mod_pubsub:extended_error( xmpp:err_bad_request(), mod_pubsub:err_subid_required())}; _ -> case Subscription of none -> unsub_with_subid(Nidx, SubId, SubState); _ -> replace_subscription({Subscription, SubId}, SubState) end end. replace_subscription(NewSub, SubState) -> NewSubs = replace_subscription(NewSub, SubState#pubsub_state.subscriptions, []), {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})}. replace_subscription(_, [], Acc) -> Acc; replace_subscription({Sub, SubId}, [{_, SubId} | T], Acc) -> replace_subscription({Sub, SubId}, T, [{Sub, SubId} | Acc]). new_subscription(_Nidx, _Owner, Subscription, SubState) -> %%{result, SubId} = pubsub_subscription_sql:subscribe_node(Owner, Nidx, []), SubId = pubsub_subscription_sql:make_subid(), Subscriptions = [{Subscription, SubId} | SubState#pubsub_state.subscriptions], set_state(SubState#pubsub_state{subscriptions = Subscriptions}), {result, {Subscription, SubId}}. unsub_with_subid(Nidx, SubId, SubState) -> %%pubsub_subscription_sql:unsubscribe_node(SubState#pubsub_state.stateid, Nidx, SubId), NewSubs = [{S, Sid} || {S, Sid} <- SubState#pubsub_state.subscriptions, SubId =/= Sid], case {NewSubs, SubState#pubsub_state.affiliation} of {[], none} -> {result, del_state(Nidx, element(1, SubState#pubsub_state.stateid))}; _ -> {result, set_state(SubState#pubsub_state{subscriptions = NewSubs})} end. get_pending_nodes(Host, Owner) -> GenKey = encode_jid(jid:remove_resource(jid:tolower(Owner))), PendingIdxs = case ejabberd_sql:sql_query_t( ?SQL("select @(nodeid)d from pubsub_state " "where subscriptions like '%p%' and affiliation='o'" "and jid=%(GenKey)s")) of {selected, RItems} -> [Nidx || {Nidx} <- RItems]; _ -> [] end, NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun(Nidx, Acc) -> case NodeTree:get_node(Nidx) of #pubsub_node{nodeid = {_, Node}} -> [Node | Acc]; _ -> Acc end end, [], PendingIdxs), {result, Reply}. get_states(Nidx) -> case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " "from pubsub_state where nodeid=%(Nidx)d")) of {selected, RItems} -> {result, lists:map( fun({SJID, Aff, Subs}) -> JID = decode_jid(SJID), #pubsub_state{stateid = {JID, Nidx}, nodeidx = Nidx, items = itemids(Nidx, JID), affiliation = decode_affiliation(Aff), subscriptions = decode_subscriptions(Subs)} end, RItems)}; _ -> {result, []} end. get_state(Nidx, JID) -> State = get_state_without_itemids(Nidx, JID), {SJID, _} = State#pubsub_state.stateid, State#pubsub_state{items = itemids(Nidx, SJID)}. -spec get_state_without_itemids(Nidx :: mod_pubsub:nodeIdx(), Key :: ljid()) -> mod_pubsub:pubsubState(). get_state_without_itemids(Nidx, JID) -> J = encode_jid(JID), case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s " "from pubsub_state " "where nodeid=%(Nidx)d and jid=%(J)s")) of {selected, [{SJID, Aff, Subs}]} -> #pubsub_state{stateid = {decode_jid(SJID), Nidx}, nodeidx = Nidx, affiliation = decode_affiliation(Aff), subscriptions = decode_subscriptions(Subs)}; _ -> #pubsub_state{stateid = {JID, Nidx}, nodeidx = Nidx} end. set_state(State) -> {_, Nidx} = State#pubsub_state.stateid, set_state(Nidx, State). set_state(Nidx, State) -> {JID, _} = State#pubsub_state.stateid, J = encode_jid(JID), S = encode_subscriptions(State#pubsub_state.subscriptions), A = encode_affiliation(State#pubsub_state.affiliation), ?SQL_UPSERT_T( "pubsub_state", ["!nodeid=%(Nidx)d", "!jid=%(J)s", "affiliation=%(A)s", "subscriptions=%(S)s" ]), ok. del_state(Nidx, JID) -> J = encode_jid(JID), catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_state" " where jid=%(J)s and nodeid=%(Nidx)d")), ok. get_items(Nidx, _From, undefined) -> SNidx = misc:i2l(Nidx), case ejabberd_sql:sql_query_t( [<<"select itemid, publisher, creation, modification, payload", " from pubsub_item where nodeid='", SNidx/binary, "'", " order by creation asc">>]) of {selected, _, AllItems} -> {result, {[raw_to_item(Nidx, RItem) || RItem <- AllItems], undefined}}; _ -> {result, {[], undefined}} end; get_items(Nidx, _From, #rsm_set{max = Max, index = IncIndex, 'after' = After, before = Before}) -> Count = case catch ejabberd_sql:sql_query_t( ?SQL("select @(count(itemid))d from pubsub_item" " where nodeid=%(Nidx)d")) of {selected, [{C}]} -> C; _ -> 0 end, Offset = case {IncIndex, Before, After} of {I, undefined, undefined} when is_integer(I) -> I; _ -> 0 end, Limit = case Max of undefined -> ?MAXITEMS; _ -> Max end, Filters = rsm_filters(misc:i2l(Nidx), Before, After), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( [<<"select top ", (integer_to_binary(Limit))/binary, " itemid, publisher, creation, modification, payload", " from pubsub_item", Filters/binary>>]); %OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY; (_, _) -> ejabberd_sql:sql_query_t( [<<"select itemid, publisher, creation, modification, payload", " from pubsub_item", Filters/binary, " limit ", (integer_to_binary(Limit))/binary, " offset ", (integer_to_binary(Offset))/binary>>]) end, case ejabberd_sql:sql_query_t(Query) of {selected, _, []} -> {result, {[], #rsm_set{count = Count}}}; {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> Rsm = rsm_page(Count, IncIndex, Offset, RItems), {result, {[raw_to_item(Nidx, RItem) || RItem <- RItems], Rsm}}; _ -> {result, {[], undefined}} end. get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId, RSM) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), if %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; (AccessModel == presence) and not PresenceSubscription -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and not RosterGroup -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and not Whitelisted -> {error, mod_pubsub:extended_error( xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; (AccessModel == authorize) and not Whitelisted -> {error, xmpp:err_forbidden()}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_items(Nidx, JID, RSM) end. get_last_items(Nidx, _From, Limit) -> SNidx = misc:i2l(Nidx), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( [<<"select top ", (integer_to_binary(Limit))/binary, " itemid, publisher, creation, modification, payload", " from pubsub_item where nodeid='", SNidx/binary, "' order by modification desc">>]); (_, _) -> ejabberd_sql:sql_query_t( [<<"select itemid, publisher, creation, modification, payload", " from pubsub_item where nodeid='", SNidx/binary, "' order by modification desc ", " limit ", (integer_to_binary(Limit))/binary>>]) end, case catch ejabberd_sql:sql_query_t(Query) of {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; _ -> {result, []} end. get_only_item(Nidx, _From) -> SNidx = misc:i2l(Nidx), Query = fun(mssql, _) -> ejabberd_sql:sql_query_t( [<<"select itemid, publisher, creation, modification, payload", " from pubsub_item where nodeid='", SNidx/binary, "'">>]); (_, _) -> ejabberd_sql:sql_query_t( [<<"select itemid, publisher, creation, modification, payload", " from pubsub_item where nodeid='", SNidx/binary, "'">>]) end, case catch ejabberd_sql:sql_query_t(Query) of {selected, [<<"itemid">>, <<"publisher">>, <<"creation">>, <<"modification">>, <<"payload">>], RItems} -> {result, [raw_to_item(Nidx, RItem) || RItem <- RItems]}; _ -> {result, []} end. get_item(Nidx, ItemId) -> case catch ejabberd_sql:sql_query_t( ?SQL("select @(itemid)s, @(publisher)s, @(creation)s," " @(modification)s, @(payload)s from pubsub_item" " where nodeid=%(Nidx)d and itemid=%(ItemId)s")) of {selected, [RItem]} -> {result, raw_to_item(Nidx, RItem)}; {selected, []} -> {error, xmpp:err_item_not_found()}; {'EXIT', _} -> {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} end. get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), {Affiliation, Subscriptions} = select_affiliation_subscriptions(Nidx, GenKey, SubKey), Whitelisted = node_flat:can_fetch_item(Affiliation, Subscriptions), if %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED(?ERR_BAD_REQUEST, "subid-required")}; %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED(?ERR_NOT_ACCEPTABLE, "invalid-subid")}; (Affiliation == outcast) or (Affiliation == publish_only) -> {error, xmpp:err_forbidden()}; (AccessModel == presence) and not PresenceSubscription -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_presence_subscription_required())}; (AccessModel == roster) and not RosterGroup -> {error, mod_pubsub:extended_error( xmpp:err_not_authorized(), mod_pubsub:err_not_in_roster_group())}; (AccessModel == whitelist) and not Whitelisted -> {error, mod_pubsub:extended_error( xmpp:err_not_allowed(), mod_pubsub:err_closed_node())}; (AccessModel == authorize) and not Whitelisted -> {error, xmpp:err_forbidden()}; %%MustPay -> %% % Payment is required for a subscription %% {error, ?ERR_PAYMENT_REQUIRED}; true -> get_item(Nidx, ItemId) end. set_item(Item) -> {ItemId, Nidx} = Item#pubsub_item.itemid, {C, _} = Item#pubsub_item.creation, {M, JID} = Item#pubsub_item.modification, P = encode_jid(JID), Payload = Item#pubsub_item.payload, XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), SM = encode_now(M), SC = encode_now(C), ?SQL_UPSERT_T( "pubsub_item", ["!nodeid=%(Nidx)d", "!itemid=%(ItemId)s", "publisher=%(P)s", "modification=%(SM)s", "payload=%(XML)s", "-creation=%(SC)s" ]), ok. del_item(Nidx, ItemId) -> catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_item where itemid=%(ItemId)s" " and nodeid=%(Nidx)d")). del_items(_, []) -> ok; del_items(Nidx, [ItemId]) -> del_item(Nidx, ItemId); del_items(Nidx, ItemIds) -> I = str:join([ejabberd_sql:to_string_literal_t(X) || X <- ItemIds], <<",">>), SNidx = misc:i2l(Nidx), catch ejabberd_sql:sql_query_t([<<"delete from pubsub_item where itemid in (">>, I, <<") and nodeid='">>, SNidx, <<"';">>]). get_item_name(_Host, _Node, Id) -> {result, Id}. node_to_path(Node) -> node_flat:node_to_path(Node). path_to_node(Path) -> node_flat:path_to_node(Path). first_in_list(_Pred, []) -> false; first_in_list(Pred, [H | T]) -> case Pred(H) of true -> {value, H}; _ -> first_in_list(Pred, T) end. itemids(Nidx) -> case catch ejabberd_sql:sql_query_t( ?SQL("select @(itemid)s from pubsub_item where " "nodeid=%(Nidx)d order by modification desc")) of {selected, RItems} -> [ItemId || {ItemId} <- RItems]; _ -> [] end. itemids(Nidx, {_U, _S, _R} = JID) -> SJID = encode_jid(JID), SJIDLike = <<(encode_jid_like(JID))/binary, "/%">>, case catch ejabberd_sql:sql_query_t( ?SQL("select @(itemid)s from pubsub_item where " "nodeid=%(Nidx)d and (publisher=%(SJID)s" " or publisher like %(SJIDLike)s %ESCAPE) " "order by modification desc")) of {selected, RItems} -> [ItemId || {ItemId} <- RItems]; _ -> [] end. select_affiliation_subscriptions(Nidx, JID) -> J = encode_jid(JID), case catch ejabberd_sql:sql_query_t( ?SQL("select @(affiliation)s, @(subscriptions)s from " " pubsub_state where nodeid=%(Nidx)d and jid=%(J)s")) of {selected, [{A, S}]} -> {decode_affiliation(A), decode_subscriptions(S)}; _ -> {none, []} end. select_affiliation_subscriptions(Nidx, JID, JID) -> select_affiliation_subscriptions(Nidx, JID); select_affiliation_subscriptions(Nidx, GenKey, SubKey) -> GJ = encode_jid(GenKey), SJ = encode_jid(SubKey), case catch ejabberd_sql:sql_query_t( ?SQL("select @(jid)s, @(affiliation)s, @(subscriptions)s from " " pubsub_state where nodeid=%(Nidx)d and jid in (%(GJ)s, %(SJ)s)")) of {selected, Res} -> lists:foldr( fun({Jid, A, S}, {_, Subs}) when Jid == GJ -> {decode_affiliation(A), Subs ++ decode_subscriptions(S)}; ({_, _, S}, {Aff, Subs}) -> {Aff, Subs ++ decode_subscriptions(S)} end, {none, []}, Res); _ -> {none, []} end. update_affiliation(Nidx, JID, Affiliation) -> J = encode_jid(JID), A = encode_affiliation(Affiliation), ?SQL_UPSERT_T( "pubsub_state", ["!nodeid=%(Nidx)d", "!jid=%(J)s", "affiliation=%(A)s", "-subscriptions=''" ]). update_subscription(Nidx, JID, Subscription) -> J = encode_jid(JID), S = encode_subscriptions(Subscription), ?SQL_UPSERT_T( "pubsub_state", ["!nodeid=%(Nidx)d", "!jid=%(J)s", "subscriptions=%(S)s", "-affiliation='n'" ]). -spec maybe_remove_extra_items(mod_pubsub:nodeIdx(), non_neg_integer() | unlimited, ljid(), mod_pubsub:itemId()) -> [mod_pubsub:itemId()]. maybe_remove_extra_items(_Nidx, unlimited, _GenKey, _ItemId) -> []; maybe_remove_extra_items(Nidx, MaxItems, GenKey, ItemId) -> ItemIds = [ItemId | itemids(Nidx, GenKey)], {result, {_NewIds, OldIds}} = remove_extra_items(Nidx, MaxItems, ItemIds), OldIds. -spec decode_jid(SJID :: binary()) -> ljid(). decode_jid(SJID) -> jid:tolower(jid:decode(SJID)). -spec decode_affiliation(Arg :: binary()) -> atom(). decode_affiliation(<<"o">>) -> owner; decode_affiliation(<<"p">>) -> publisher; decode_affiliation(<<"u">>) -> publish_only; decode_affiliation(<<"m">>) -> member; decode_affiliation(<<"c">>) -> outcast; decode_affiliation(_) -> none. -spec decode_subscription(Arg :: binary()) -> atom(). decode_subscription(<<"s">>) -> subscribed; decode_subscription(<<"p">>) -> pending; decode_subscription(<<"u">>) -> unconfigured; decode_subscription(_) -> none. -spec decode_subscriptions(Subscriptions :: binary()) -> [] | [{atom(), binary()},...]. decode_subscriptions(Subscriptions) -> lists:foldl(fun (Subscription, Acc) -> case str:tokens(Subscription, <<":">>) of [S, SubId] -> [{decode_subscription(S), SubId} | Acc]; _ -> Acc end end, [], str:tokens(Subscriptions, <<",">>)). -spec encode_jid(JID :: ljid()) -> binary(). encode_jid(JID) -> jid:encode(JID). -spec encode_jid_like(JID :: ljid()) -> binary(). encode_jid_like(JID) -> ejabberd_sql:escape_like_arg(jid:encode(JID)). -spec encode_host(Host :: host()) -> binary(). encode_host({_U, _S, _R} = LJID) -> encode_jid(LJID); encode_host(Host) -> Host. -spec encode_host_like(Host :: host()) -> binary(). encode_host_like({_U, _S, _R} = LJID) -> encode_jid_like(LJID); encode_host_like(Host) -> ejabberd_sql:escape_like_arg(Host). -spec encode_affiliation(Arg :: atom()) -> binary(). encode_affiliation(owner) -> <<"o">>; encode_affiliation(publisher) -> <<"p">>; encode_affiliation(publish_only) -> <<"u">>; encode_affiliation(member) -> <<"m">>; encode_affiliation(outcast) -> <<"c">>; encode_affiliation(_) -> <<"n">>. -spec encode_subscription(Arg :: atom()) -> binary(). encode_subscription(subscribed) -> <<"s">>; encode_subscription(pending) -> <<"p">>; encode_subscription(unconfigured) -> <<"u">>; encode_subscription(_) -> <<"n">>. -spec encode_subscriptions(Subscriptions :: [] | [{atom(), binary()},...]) -> binary(). encode_subscriptions(Subscriptions) -> str:join([<<(encode_subscription(S))/binary, ":", SubId/binary>> || {S, SubId} <- Subscriptions], <<",">>). %%% record getter/setter raw_to_item(Nidx, [ItemId, SJID, Creation, Modification, XML]) -> raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}); raw_to_item(Nidx, {ItemId, SJID, Creation, Modification, XML}) -> JID = decode_jid(SJID), Payload = case fxml_stream:parse_element(XML) of {error, _Reason} -> []; El -> [El] end, #pubsub_item{itemid = {ItemId, Nidx}, nodeidx = Nidx, creation = {decode_now(Creation), jid:remove_resource(JID)}, modification = {decode_now(Modification), JID}, payload = Payload}. rsm_filters(SNidx, undefined, undefined) -> <<" where nodeid='", SNidx/binary, "'", " order by creation asc">>; rsm_filters(SNidx, undefined, After) -> <<" where nodeid='", SNidx/binary, "'", " and creation>'", (encode_stamp(After))/binary, "'", " order by creation asc">>; rsm_filters(SNidx, <<>>, undefined) -> %% 2.5 Requesting the Last Page in a Result Set <<" where nodeid='", SNidx/binary, "'", " order by creation desc">>; rsm_filters(SNidx, Before, undefined) -> <<" where nodeid='", SNidx/binary, "'", " and creation<'", (encode_stamp(Before))/binary, "'", " order by creation desc">>. rsm_page(Count, Index, Offset, Items) -> First = decode_stamp(lists:nth(3, hd(Items))), Last = decode_stamp(lists:nth(3, lists:last(Items))), #rsm_set{count = Count, index = Index, first = #rsm_first{index = Offset, data = First}, last = Last}. encode_stamp(Stamp) -> try xmpp_util:decode_timestamp(Stamp) of Now -> encode_now(Now) catch _:{bad_timestamp, _} -> Stamp % We should return a proper error to the client instead. end. decode_stamp(Stamp) -> xmpp_util:encode_timestamp(decode_now(Stamp)). encode_now({T1, T2, T3}) -> <<(misc:i2l(T1, 6))/binary, ":", (misc:i2l(T2, 6))/binary, ":", (misc:i2l(T3, 6))/binary>>. decode_now(NowStr) -> [MS, S, US] = binary:split(NowStr, <<":">>, [global]), {binary_to_integer(MS), binary_to_integer(S), binary_to_integer(US)}. ejabberd-21.12/src/ejabberd_access_permissions.erl0000644000232200023220000003005514154362354022642 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_access_permissions.erl %%% Author : Paweł Chmielowski %%% Purpose : Administrative functions and commands %%% Created : 7 Sep 2016 by Paweł Chmielowski %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_access_permissions). -author("pawel@process-one.net"). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -behaviour(gen_server). %% API -export([start_link/0, can_access/2, invalidate/0, validator/0, show_current_definitions/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -define(CACHE_TAB, access_permissions_cache). -record(state, {definitions = none :: none | [definition()]}). -type state() :: #state{}. -type rule() :: {access, acl:access()} | {acl, all | none | acl:acl_rule()}. -type what() :: all | none | [atom() | {tag, atom()}]. -type who() :: rule() | {oauth, {[binary()], [rule()]}}. -type from() :: atom(). -type permission() :: {binary(), {[from()], [who()], {what(), what()}}}. -type definition() :: {binary(), {[from()], [who()], [atom()] | all}}. -type caller_info() :: #{caller_module => module(), caller_host => global | binary(), tag => binary() | none, extra_permissions => [definition()], atom() => term()}. -export_type([permission/0]). %%%=================================================================== %%% API %%%=================================================================== -spec can_access(atom(), caller_info()) -> allow | deny. can_access(Cmd, CallerInfo) -> Defs0 = show_current_definitions(), CallerModule = maps:get(caller_module, CallerInfo, none), Host = maps:get(caller_host, CallerInfo, global), Tag = maps:get(tag, CallerInfo, none), Defs = maps:get(extra_permissions, CallerInfo, []) ++ Defs0, Res = lists:foldl( fun({Name, _} = Def, none) -> case matches_definition(Def, Cmd, CallerModule, Tag, Host, CallerInfo) of true -> ?DEBUG("Command '~p' execution allowed by rule " "'~ts' (CallerInfo=~p)", [Cmd, Name, CallerInfo]), allow; _ -> none end; (_, Val) -> Val end, none, Defs), case Res of allow -> allow; _ -> ?DEBUG("Command '~p' execution denied " "(CallerInfo=~p)", [Cmd, CallerInfo]), deny end. -spec invalidate() -> ok. invalidate() -> gen_server:cast(?MODULE, invalidate), ets_cache:delete(?CACHE_TAB, definitions). -spec show_current_definitions() -> [definition()]. show_current_definitions() -> ets_cache:lookup(?CACHE_TAB, definitions, fun() -> {cache, gen_server:call(?MODULE, show_current_definitions)} end). start_link() -> ets_cache:new(?CACHE_TAB, [{max_size, 2}]), gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -spec init([]) -> {ok, state()}. init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, invalidate, 90), ets_cache:new(access_permissions), {ok, #state{}}. -spec handle_call(show_current_definitions | term(), term(), state()) -> {reply, term(), state()}. handle_call(show_current_definitions, _From, State) -> {State2, Defs} = get_definitions(State), {reply, Defs, State2}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(invalidate | term(), state()) -> {noreply, state()}. handle_cast(invalidate, State) -> {noreply, State#state{definitions = none}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, invalidate, 90). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_definitions(state()) -> {state(), [definition()]}. get_definitions(#state{definitions = Defs} = State) when Defs /= none -> {State, Defs}; get_definitions(#state{definitions = none} = State) -> ApiPerms = ejabberd_option:api_permissions(), AllCommands = ejabberd_commands:get_commands_definition(), NDefs0 = lists:map( fun({Name, {From, Who, {Add, Del}}}) -> Cmds = filter_commands_with_permissions(AllCommands, Add, Del), {Name, {From, Who, Cmds}} end, ApiPerms), NDefs = case lists:keyfind(<<"console commands">>, 1, NDefs0) of false -> [{<<"console commands">>, {[ejabberd_ctl], [{acl, all}], filter_commands_with_permissions(AllCommands, all, none)}} | NDefs0]; _ -> NDefs0 end, {State#state{definitions = NDefs}, NDefs}. -spec matches_definition(definition(), atom(), module(), atom(), global | binary(), caller_info()) -> boolean(). matches_definition({_Name, {From, Who, What}}, Cmd, Module, Tag, Host, CallerInfo) -> case What == all orelse lists:member(Cmd, What) of true -> {Tags, Modules} = lists:partition(fun({tag, _}) -> true; (_) -> false end, From), case (Modules == [] orelse lists:member(Module, Modules)) andalso (Tags == [] orelse lists:member({tag, Tag}, Tags)) of true -> Scope = maps:get(oauth_scope, CallerInfo, none), lists:any( fun({access, Access}) when Scope == none -> acl:match_rule(Host, Access, CallerInfo) == allow; ({acl, Name} = Acl) when Scope == none, is_atom(Name) -> acl:match_acl(Host, Acl, CallerInfo); ({acl, Acl}) when Scope == none -> acl:match_acl(Host, Acl, CallerInfo); ({oauth, {Scopes, List}}) when Scope /= none -> case ejabberd_oauth:scope_in_scope_list(Scope, Scopes) of true -> lists:any( fun({access, Access}) -> acl:match_rule(Host, Access, CallerInfo) == allow; ({acl, Name} = Acl) when is_atom(Name) -> acl:match_acl(Host, Acl, CallerInfo); ({acl, Acl}) -> acl:match_acl(Host, Acl, CallerInfo) end, List); _ -> false end; (_) -> false end, Who); _ -> false end; _ -> false end. -spec filter_commands_with_permissions([#ejabberd_commands{}], what(), what()) -> [atom()]. filter_commands_with_permissions(AllCommands, Add, Del) -> CommandsAdd = filter_commands_with_patterns(AllCommands, Add, []), CommandsDel = filter_commands_with_patterns(CommandsAdd, Del, []), lists:map(fun(#ejabberd_commands{name = N}) -> N end, CommandsAdd -- CommandsDel). -spec filter_commands_with_patterns([#ejabberd_commands{}], what(), [#ejabberd_commands{}]) -> [#ejabberd_commands{}]. filter_commands_with_patterns([], _Patterns, Acc) -> Acc; filter_commands_with_patterns([C | CRest], Patterns, Acc) -> case command_matches_patterns(C, Patterns) of true -> filter_commands_with_patterns(CRest, Patterns, [C | Acc]); _ -> filter_commands_with_patterns(CRest, Patterns, Acc) end. -spec command_matches_patterns(#ejabberd_commands{}, what()) -> boolean(). command_matches_patterns(_, all) -> true; command_matches_patterns(_, none) -> false; command_matches_patterns(_, []) -> false; command_matches_patterns(#ejabberd_commands{tags = Tags} = C, [{tag, Tag} | Tail]) -> case lists:member(Tag, Tags) of true -> true; _ -> command_matches_patterns(C, Tail) end; command_matches_patterns(#ejabberd_commands{name = Name}, [Name | _Tail]) -> true; command_matches_patterns(C, [_ | Tail]) -> command_matches_patterns(C, Tail). %%%=================================================================== %%% Validators %%%=================================================================== -spec parse_what([binary()]) -> {what(), what()}. parse_what(Defs) -> {A, D} = lists:foldl( fun(Def, {Add, Del}) -> case parse_single_what(Def) of {error, Err} -> econf:fail({invalid_syntax, [Err, ": ", Def]}); all -> {case Add of none -> none; _ -> all end, Del}; {neg, all} -> {none, all}; {neg, Value} -> {Add, case Del of L when is_list(L) -> [Value | L]; L2 -> L2 end}; Value -> {case Add of L when is_list(L) -> [Value | L]; L2 -> L2 end, Del} end end, {[], []}, Defs), case {A, D} of {[], _} -> {none, all}; {A2, []} -> {A2, none}; V -> V end. -spec parse_single_what(binary()) -> atom() | {neg, atom()} | {tag, atom()} | {error, string()}. parse_single_what(<<"*">>) -> all; parse_single_what(<<"!*">>) -> {neg, all}; parse_single_what(<<"!", Rest/binary>>) -> case parse_single_what(Rest) of {neg, _} -> {error, "double negation"}; {error, _} = Err -> Err; V -> {neg, V} end; parse_single_what(<<"[tag:", Rest/binary>>) -> case binary:split(Rest, <<"]">>) of [TagName, <<"">>] -> case parse_single_what(TagName) of {error, _} = Err -> Err; V when is_atom(V) -> {tag, V}; _ -> {error, "invalid tag"} end; _ -> {error, "invalid tag"} end; parse_single_what(B) -> case re:run(B, "^[a-z0-9_\\-]*$") of nomatch -> {error, "invalid command"}; _ -> binary_to_atom(B, latin1) end. validator(Map, Opts) -> econf:and_then( fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:atom())(K), V}; (A) -> {acl, (econf:atom())(A)} end, lists:flatten(L)); (A) -> [{acl, (econf:atom())(A)}] end, econf:and_then( econf:options(maps:merge(acl:validators(), Map), Opts), fun(Rules) -> lists:flatmap( fun({Type, Rs}) when is_list(Rs) -> case maps:is_key(Type, acl:validators()) of true -> [{acl, {Type, R}} || R <- Rs]; false -> [{Type, Rs}] end; (Other) -> [Other] end, Rules) end)). validator(from) -> fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:enum([tag]))(K), (econf:binary())(V)}; (A) -> (econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A) end, lists:flatten(L)); (A) -> [(econf:enum([ejabberd_xmlrpc, mod_http_api, ejabberd_ctl]))(A)] end; validator(what) -> econf:and_then( econf:list_or_single(econf:non_empty(econf:binary())), fun parse_what/1); validator(who) -> validator(#{access => econf:acl(), oauth => validator(oauth)}, []); validator(oauth) -> econf:and_then( validator(#{access => econf:acl(), scope => econf:non_empty( econf:list_or_single(econf:binary()))}, [{required, [scope]}]), fun(Os) -> {[Scopes], Rest} = proplists:split(Os, [scope]), {lists:flatten([S || {_, S} <- Scopes]), Rest} end). validator() -> econf:map( econf:binary(), econf:and_then( econf:options( #{from => validator(from), what => validator(what), who => validator(who)}), fun(Os) -> {proplists:get_value(from, Os, []), proplists:get_value(who, Os, none), proplists:get_value(what, Os, [])} end), [unique]). ejabberd-21.12/src/ejabberd_websocket.erl0000644000232200023220000004575714154362354020753 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_websocket.erl %%% Author : Eric Cestari %%% Purpose : XMPP Websocket support %%% Created : 09-10-2010 by Eric Cestari %%% %%% Some code lifted from MISULTIN - WebSocket misultin_websocket.erl - >-|-|-(°> %%% (http://github.com/ostinelli/misultin/blob/master/src/misultin_websocket.erl) %%% Copyright (C) 2010, Roberto Ostinelli , Joe Armstrong. %%% All rights reserved. %%% %%% Code portions from Joe Armstrong have been originally taken under MIT license at the address: %%% %%% %%% BSD License %%% %%% Redistribution and use in source and binary forms, with or without modification, are permitted provided %%% that the following conditions are met: %%% %%% * Redistributions of source code must retain the above copyright notice, this list of conditions and the %%% following disclaimer. %%% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and %%% the following disclaimer in the documentation and/or other materials provided with the distribution. %%% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote %%% products derived from this software without specific prior written permission. %%% %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED %%% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A %%% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR %%% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED %%% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) %%% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %%% ========================================================================================================== %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%%---------------------------------------------------------------------- -module(ejabberd_websocket). -protocol({rfc, 6455}). -author('ecestari@process-one.net'). -export([socket_handoff/5]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). -define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, OPTIONS">>}). -define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type">>}). -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). is_valid_websocket_upgrade(_Path, Headers) -> HeadersToValidate = [{'Upgrade', <<"websocket">>}, {'Connection', ignore}, {'Host', ignore}, {<<"Sec-Websocket-Key">>, ignore}, {<<"Sec-Websocket-Version">>, <<"13">>}], Res = lists:all( fun({Tag, Val}) -> case lists:keyfind(Tag, 1, Headers) of false -> false; {_, _} when Val == ignore -> true; {_, HVal} -> str:to_lower(HVal) == Val end end, HeadersToValidate), case {Res, lists:keyfind(<<"Origin">>, 1, Headers), get_origin()} of {false, _, _} -> false; {true, _, []} -> true; {true, {_, HVal}, Origins} -> HValLow = str:to_lower(HVal), case lists:any(fun(V) -> V == HValLow end, Origins) of true -> true; _ -> invalid_origin end; {true, false, _} -> true end. socket_handoff(LocalPath, #request{method = 'GET', ip = IP, q = Q, path = Path, headers = Headers, host = Host, port = Port, socket = Socket, sockmod = SockMod, data = Buf, opts = HOpts}, _Opts, HandlerModule, InfoMsgFun) -> case is_valid_websocket_upgrade(LocalPath, Headers) of true -> WS = #ws{socket = Socket, sockmod = SockMod, ip = IP, q = Q, host = Host, port = Port, path = Path, headers = Headers, local_path = LocalPath, buf = Buf, http_opts = HOpts}, connect(WS, HandlerModule); false -> {200, ?HEADER, InfoMsgFun()}; invalid_origin -> {403, ?HEADER, #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"403 Bad Request - Invalid origin">>}]}} end; socket_handoff(_, #request{method = 'OPTIONS'}, _, _, _) -> {200, ?OPTIONS_HEADER, []}; socket_handoff(_, #request{method = 'HEAD'}, _, _, _) -> {200, ?HEADER, []}; socket_handoff(_, _, _, _, _) -> {400, ?HEADER, #xmlel{name = <<"h1">>, children = [{xmlcdata, <<"400 Bad Request">>}]}}. connect(#ws{socket = Socket, sockmod = SockMod} = Ws, WsLoop) -> {NewWs, HandshakeResponse} = handshake(Ws), SockMod:send(Socket, HandshakeResponse), ?DEBUG("Sent handshake response : ~p", [HandshakeResponse]), Ws0 = {Ws, self()}, {ok, WsHandleLoopPid} = WsLoop:start_link(Ws0), erlang:monitor(process, WsHandleLoopPid), case NewWs#ws.buf of <<>> -> ok; Data -> self() ! {raw, Socket, Data} end, % set opts case SockMod of gen_tcp -> inet:setopts(Socket, [{packet, 0}, {active, true}]); _ -> SockMod:setopts(Socket, [{packet, 0}, {active, true}]) end, ws_loop(none, Socket, WsHandleLoopPid, SockMod, none). handshake(#ws{headers = Headers} = State) -> {_, Key} = lists:keyfind(<<"Sec-Websocket-Key">>, 1, Headers), SubProtocolHeader = case find_subprotocol(Headers) of false -> []; V -> [<<"Sec-Websocket-Protocol:">>, V, <<"\r\n">>] end, Hash = base64:encode( crypto:hash(sha, <>)), {State, [<<"HTTP/1.1 101 Switching Protocols\r\n">>, <<"Upgrade: websocket\r\n">>, <<"Connection: Upgrade\r\n">>, SubProtocolHeader, <<"Sec-WebSocket-Accept: ">>, Hash, <<"\r\n\r\n">>]}. find_subprotocol(Headers) -> case lists:keysearch(<<"Sec-Websocket-Protocol">>, 1, Headers) of false -> case lists:keysearch(<<"Websocket-Protocol">>, 1, Headers) of false -> false; {value, {_, Protocol2}} -> Protocol2 end; {value, {_, Protocol}} -> Protocol end. ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper) -> receive {DataType, _Socket, Data} when DataType =:= tcp orelse DataType =:= raw -> case handle_data(DataType, FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) of {error, Error} -> ?DEBUG("TLS decode error ~p", [Error]), websocket_close(Socket, WsHandleLoopPid, SockMod, 1002); % protocol error {NewFrameInfo, ToSend, NewShaper} -> lists:foreach(fun(Pkt) -> SockMod:send(Socket, Pkt) end, ToSend), ws_loop(NewFrameInfo, Socket, WsHandleLoopPid, SockMod, NewShaper) end; {new_shaper, NewShaper} -> NewShaper = case NewShaper of none when Shaper /= none -> activate(Socket, SockMod, true), none; _ -> NewShaper end, ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, NewShaper); {tcp_closed, _Socket} -> ?DEBUG("TCP connection was closed, exit", []), websocket_close(Socket, WsHandleLoopPid, SockMod, 0); {tcp_error, Socket, Reason} -> ?DEBUG("TCP connection error: ~ts", [inet:format_error(Reason)]), websocket_close(Socket, WsHandleLoopPid, SockMod, 0); {'DOWN', Ref, process, WsHandleLoopPid, Reason} -> Code = case Reason of normal -> 1000; % normal close _ -> ?ERROR_MSG("Linked websocket controlling loop crashed " "with reason: ~p", [Reason]), 1011 % internal error end, erlang:demonitor(Ref), websocket_close(Socket, WsHandleLoopPid, SockMod, Code); {text_with_reply, Data, Sender} -> SockMod:send(Socket, encode_frame(Data, 1)), Sender ! {text_reply, self()}, ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper); {data_with_reply, Data, Sender} -> SockMod:send(Socket, encode_frame(Data, 2)), Sender ! {data_reply, self()}, ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper); {text, Data} -> SockMod:send(Socket, encode_frame(Data, 1)), ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper); {data, Data} -> SockMod:send(Socket, encode_frame(Data, 2)), ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper); {ping, Data} -> SockMod:send(Socket, encode_frame(Data, 9)), ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper); shutdown -> ?DEBUG("Shutdown request received, closing websocket " "with pid ~p", [self()]), websocket_close(Socket, WsHandleLoopPid, SockMod, 1001); % going away _Ignored -> ?WARNING_MSG("Received unexpected message, ignoring: ~p", [_Ignored]), ws_loop(FrameInfo, Socket, WsHandleLoopPid, SockMod, Shaper) end. encode_frame(Data, Opcode) -> case byte_size(Data) of S1 when S1 < 126 -> <<1:1, 0:3, Opcode:4, 0:1, S1:7, Data/binary>>; S2 when S2 < 65536 -> <<1:1, 0:3, Opcode:4, 0:1, 126:7, S2:16, Data/binary>>; S3 -> <<1:1, 0:3, Opcode:4, 0:1, 127:7, S3:64, Data/binary>> end. -record(frame_info, {mask = none, offset = 0, left, final_frame = true, opcode, unprocessed = <<>>, unmasked = <<>>, unmasked_msg = <<>>}). decode_header(<>) when Len < 126 -> {Len, Final, Opcode, none, Data}; decode_header(<>) -> {Len, Final, Opcode, none, Data}; decode_header(<>) -> {Len, Final, Opcode, none, Data}; decode_header(<>) when Len < 126 -> {Len, Final, Opcode, Mask, Data}; decode_header(<>) -> {Len, Final, Opcode, Mask, Data}; decode_header(<>) -> {Len, Final, Opcode, Mask, Data}; decode_header(_) -> none. unmask_int(Offset, _, <<>>, Acc) -> {Acc, Offset}; unmask_int(0, <> = Mask, <>, Acc) -> unmask_int(0, Mask, Rest, <>); unmask_int(0, <> = Mask, <>, Acc) -> unmask_int(1, Mask, Rest, <>); unmask_int(1, <<_:8, M:8, _/binary>> = Mask, <>, Acc) -> unmask_int(2, Mask, Rest, <>); unmask_int(2, <<_:16, M:8, _/binary>> = Mask, <>, Acc) -> unmask_int(3, Mask, Rest, <>); unmask_int(3, <<_:24, M:8>> = Mask, <>, Acc) -> unmask_int(0, Mask, Rest, <>). unmask(#frame_info{mask = none} = State, Data) -> {State, Data}; unmask(#frame_info{mask = Mask, offset = Offset} = State, Data) -> {Unmasked, NewOffset} = unmask_int(Offset, Mask, Data, <<>>), {State#frame_info{offset = NewOffset}, Unmasked}. process_frame(none, Data) -> process_frame(#frame_info{}, Data); process_frame(#frame_info{left = Left} = FrameInfo, <<>>) when Left > 0 -> {FrameInfo, [], []}; process_frame(#frame_info{unprocessed = none, unmasked = UnmaskedPre, left = Left} = State, Data) when byte_size(Data) < Left -> {State2, Unmasked} = unmask(State, Data), {State2#frame_info{left = Left - byte_size(Data), unmasked = [UnmaskedPre, Unmasked]}, [], []}; process_frame(#frame_info{unprocessed = none, unmasked = UnmaskedPre, opcode = Opcode, final_frame = Final, left = Left, unmasked_msg = UnmaskedMsg} = FrameInfo, Data) -> <> = Data, {_, Unmasked} = unmask(FrameInfo, ToProcess), case Final of true -> {FrameInfo3, Recv, Send} = process_frame(#frame_info{}, Unprocessed), case Opcode of X when X < 3 -> {FrameInfo3, [iolist_to_binary([UnmaskedMsg, UnmaskedPre, Unmasked]) | Recv], Send}; 9 -> % Ping Frame = encode_frame(Unmasked, 10), {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, [ping | Recv], [Frame | Send]}; 10 -> % Pong {FrameInfo3, [pong | Recv], Send}; 8 -> % Close CloseCode = case Unmasked of <> -> ?DEBUG("WebSocket close op: ~p ~ts", [Code, Message]), Code; <> -> ?DEBUG("WebSocket close op: ~p", [Code]), Code; _ -> ?DEBUG("WebSocket close op unknown: ~p", [Unmasked]), 1000 end, Frame = encode_frame(<>, 8), {FrameInfo3#frame_info{unmasked_msg=UnmaskedMsg}, Recv, [Frame | Send]}; _ -> {FrameInfo3#frame_info{unmasked_msg = UnmaskedMsg}, Recv, Send} end; _ -> process_frame(#frame_info{unmasked_msg = [UnmaskedMsg, UnmaskedPre, Unmasked]}, Unprocessed) end; process_frame(#frame_info{unprocessed = <<>>} = FrameInfo, Data) -> case decode_header(Data) of none -> {FrameInfo#frame_info{unprocessed = Data}, [], []}; {Len, Final, Opcode, Mask, Rest} -> process_frame(FrameInfo#frame_info{mask = Mask, final_frame = Final == 1, left = Len, opcode = Opcode, unprocessed = none}, Rest) end; process_frame(#frame_info{unprocessed = UnprocessedPre} = FrameInfo, Data) -> process_frame(FrameInfo#frame_info{unprocessed = <<>>}, <>). handle_data(tcp, FrameInfo, Data, Socket, WsHandleLoopPid, fast_tls, Shaper) -> case fast_tls:recv_data(Socket, Data) of {ok, NewData} -> handle_data_int(FrameInfo, NewData, Socket, WsHandleLoopPid, fast_tls, Shaper); {error, Error} -> {error, Error} end; handle_data(_, FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> handle_data_int(FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper). handle_data_int(FrameInfo, Data, Socket, WsHandleLoopPid, SockMod, Shaper) -> {NewFrameInfo, Recv, Send} = process_frame(FrameInfo, Data), lists:foreach(fun (El) -> case El of pong -> WsHandleLoopPid ! pong; ping -> WsHandleLoopPid ! ping; _ -> WsHandleLoopPid ! {received, El} end end, Recv), {NewFrameInfo, Send, handle_shaping(Data, Socket, SockMod, Shaper)}. websocket_close(Socket, WsHandleLoopPid, SockMod, CloseCode) when CloseCode > 0 -> Frame = encode_frame(<>, 8), SockMod:send(Socket, Frame), websocket_close(Socket, WsHandleLoopPid, SockMod, 0); websocket_close(Socket, WsHandleLoopPid, SockMod, _CloseCode) -> WsHandleLoopPid ! closed, SockMod:close(Socket). get_origin() -> ejabberd_option:websocket_origin(). handle_shaping(_Data, _Socket, _SockMod, none) -> none; handle_shaping(Data, Socket, SockMod, Shaper) -> {NewShaper, Pause} = ejabberd_shaper:update(Shaper, byte_size(Data)), if Pause > 0 -> activate_after(Socket, self(), Pause); true -> activate(Socket, SockMod, once) end, NewShaper. activate(Socket, SockMod, ActiveState) -> case SockMod of gen_tcp -> inet:setopts(Socket, [{active, ActiveState}]); _ -> SockMod:setopts(Socket, [{active, ActiveState}]) end. activate_after(Socket, Pid, Pause) -> if Pause > 0 -> erlang:send_after(Pause, Pid, {tcp, Socket, <<>>}); true -> Pid ! {tcp, Socket, <<>>} end, ok. ejabberd-21.12/src/mod_muc_admin.erl0000644000232200023220000014142414154362354017726 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_muc_admin.erl %%% Author : Badlop %%% Purpose : Tools for additional MUC administration %%% Created : 8 Sep 2007 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_admin). -author('badlop@ono.com'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, depends/2, mod_doc/0, muc_online_rooms/1, muc_online_rooms_by_regex/2, muc_register_nick/3, muc_unregister_nick/2, create_room_with_opts/4, create_room/3, destroy_room/2, create_rooms_file/1, destroy_rooms_file/1, rooms_unused_list/2, rooms_unused_destroy/2, rooms_empty_list/1, rooms_empty_destroy/1, get_user_rooms/2, get_user_subscriptions/2, get_room_occupants/2, get_room_occupants_number/2, send_direct_invitation/5, change_room_option/4, get_room_options/2, set_room_affiliation/4, get_room_affiliations/2, get_room_affiliation/3, web_menu_main/2, web_page_main/2, web_menu_host/3, subscribe_room/4, unsubscribe_room/2, get_subscribers/2, web_page_host/3, mod_options/1, get_commands_spec/0, find_hosts/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_commands.hrl"). -include("translate.hrl"). %%---------------------------- %% gen_mod %%---------------------------- start(Host, _Opts) -> ejabberd_commands:register_commands(?MODULE, get_commands_spec()), ejabberd_hooks:add(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:add(webadmin_page_main, ?MODULE, web_page_main, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, web_page_host, 50). stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end, ejabberd_hooks:delete(webadmin_menu_main, ?MODULE, web_menu_main, 50), ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, web_menu_host, 50), ejabberd_hooks:delete(webadmin_page_main, ?MODULE, web_page_main, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, web_page_host, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_muc, hard}]. %%% %%% Register commands %%% get_commands_spec() -> [ #ejabberd_commands{name = muc_online_rooms, tags = [muc], desc = "List existing rooms ('global' to get all vhosts)", policy = admin, module = ?MODULE, function = muc_online_rooms, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = muc_online_rooms_by_regex, tags = [muc], desc = "List existing rooms ('global' to get all vhosts) by regex", policy = admin, module = ?MODULE, function = muc_online_rooms_by_regex, args_desc = ["MUC service, or 'global' for all", "Regex pattern for room name"], args_example = ["muc.example.com", "^prefix"], result_desc = "List of rooms with summary", result_example = [{"room1@muc.example.com", "true", 10}, {"room2@muc.example.com", "false", 10}], args = [{service, binary}, {regex, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, {tuple, [{jid, string}, {public, string}, {participants, integer} ]}}}}}, #ejabberd_commands{name = muc_register_nick, tags = [muc], desc = "Register a nick to a User JID in a MUC service", module = ?MODULE, function = muc_register_nick, args_desc = ["Nick", "User JID", "Service"], args_example = [<<"Tim">>, <<"tim@example.org">>, <<"muc.example.org">>], args = [{nick, binary}, {jid, binary}, {service, binary}], args_rename = [{host, service}], result = {res, rescode}}, #ejabberd_commands{name = muc_unregister_nick, tags = [muc], desc = "Unregister the nick registered by that account in the MUC service", module = ?MODULE, function = muc_unregister_nick, args_desc = ["User JID", "MUC service"], args_example = [<<"tim@example.org">>, <<"muc.example.org">>], args = [{jid, binary}, {service, binary}], args_rename = [{host, service}], result = {res, rescode}}, #ejabberd_commands{name = create_room, tags = [muc_room], desc = "Create a MUC room name@service in host", module = ?MODULE, function = create_room, args_desc = ["Room name", "MUC service", "Server host"], args_example = ["room1", "muc.example.com", "example.com"], args = [{name, binary}, {service, binary}, {host, binary}], result = {res, rescode}}, #ejabberd_commands{name = destroy_room, tags = [muc_room], desc = "Destroy a MUC room", module = ?MODULE, function = destroy_room, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], args = [{name, binary}, {service, binary}], result = {res, rescode}}, #ejabberd_commands{name = create_rooms_file, tags = [muc], desc = "Create the rooms indicated in file", longdesc = "Provide one room JID per line. Rooms will be created after restart.", module = ?MODULE, function = create_rooms_file, args_desc = ["Path to the text file with one room JID per line"], args_example = ["/home/ejabberd/rooms.txt"], 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_desc = ["Room name", "MUC service", "Server host", "List of options"], args_example = ["room1", "muc.example.com", "localhost", [{"members_only","true"}]], 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.", module = ?MODULE, function = destroy_rooms_file, args_desc = ["Path to the text file with one room JID per line"], args_example = ["/home/ejabberd/rooms.txt"], args = [{file, string}], result = {res, rescode}}, #ejabberd_commands{name = rooms_unused_list, tags = [muc], desc = "List the rooms that are unused for many days in the service", longdesc = "The room recent history is used, so it's recommended " " to wait a few days after service start before running this." " The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_unused_list, args_desc = ["MUC service, or 'global' for all", "Number of days"], args_example = ["muc.example.com", 31], result_desc = "List of unused rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}, {days, integer}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_unused_destroy, tags = [muc], desc = "Destroy the rooms that are unused for many days in the service", longdesc = "The room recent history is used, so it's recommended " " to wait a few days after service start before running this." " The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_unused_destroy, args_desc = ["MUC service, or 'global' for all", "Number of days"], args_example = ["muc.example.com", 31], result_desc = "List of unused rooms that has been destroyed", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}, {days, integer}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_empty_list, tags = [muc], desc = "List the rooms that have no messages in archive", longdesc = "The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_empty_list, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of empty rooms", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = rooms_empty_destroy, tags = [muc], desc = "Destroy the rooms that have no messages in archive", longdesc = "The MUC service argument can be 'global' to get all hosts.", module = ?MODULE, function = rooms_empty_destroy, args_desc = ["MUC service, or 'global' for all"], args_example = ["muc.example.com"], result_desc = "List of empty rooms that have been destroyed", result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{service, binary}], args_rename = [{host, service}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = get_user_rooms, tags = [muc], desc = "Get the list of rooms where this user is occupant", module = ?MODULE, function = get_user_rooms, args_desc = ["Username", "Server host"], args_example = ["tom", "example.com"], result_example = ["room1@muc.example.com", "room2@muc.example.com"], args = [{user, binary}, {host, binary}], result = {rooms, {list, {room, string}}}}, #ejabberd_commands{name = get_user_subscriptions, tags = [muc], desc = "Get the list of rooms where this user is subscribed", note = "added in 21.04", module = ?MODULE, function = get_user_subscriptions, args_desc = ["Username", "Server host"], args_example = ["tom", "example.com"], result_example = [{"room1@muc.example.com", "Tommy", ["mucsub:config"]}], args = [{user, binary}, {host, binary}], result = {rooms, {list, {room, {tuple, [{roomjid, string}, {usernick, string}, {nodes, {list, {node, string}}} ]}} }}}, #ejabberd_commands{name = get_room_occupants, tags = [muc_room], desc = "Get the list of occupants of a MUC room", module = ?MODULE, function = get_room_occupants, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of occupants with JID, nick and affiliation", result_example = [{"user1@example.com/psi", "User 1", "owner"}], args = [{name, binary}, {service, binary}], result = {occupants, {list, {occupant, {tuple, [{jid, string}, {nick, string}, {role, string} ]}} }}}, #ejabberd_commands{name = get_room_occupants_number, tags = [muc_room], desc = "Get the number of occupants of a MUC room", module = ?MODULE, function = get_room_occupants_number, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "Number of room occupants", result_example = 7, args = [{name, binary}, {service, binary}], result = {occupants, integer}}, #ejabberd_commands{name = send_direct_invitation, tags = [muc_room], desc = "Send a direct invitation to several destinations", longdesc = "Since ejabberd 20.10, this command is " "asynchronous: the API call may return before the " "server has send all the invitations.\n\n" "Password and Message can also be: none. " "Users JIDs are separated with : ", module = ?MODULE, function = send_direct_invitation, args_desc = ["Room name", "MUC service", "Password, or none", "Reason text, or none", "Users JIDs separated with : characters"], args_example = [<<"room1">>, <<"muc.example.com">>, <<>>, <<"Check this out!">>, "user2@localhost:user3@example.com"], args = [{name, binary}, {service, binary}, {password, binary}, {reason, binary}, {users, binary}], result = {res, rescode}}, #ejabberd_commands{name = change_room_option, tags = [muc_room], desc = "Change an option in a MUC room", module = ?MODULE, function = change_room_option, args_desc = ["Room name", "MUC service", "Option name", "Value to assign"], args_example = ["room1", "muc.example.com", "members_only", "true"], args = [{name, binary}, {service, binary}, {option, binary}, {value, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_room_options, tags = [muc_room], desc = "Get options from a MUC room", module = ?MODULE, function = get_room_options, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "List of room options tuples with name and value", result_example = [{"members_only", "true"}], args = [{name, binary}, {service, binary}], result = {options, {list, {option, {tuple, [{name, string}, {value, string} ]}} }}}, #ejabberd_commands{name = subscribe_room, tags = [muc_room], desc = "Subscribe to a MUC conference", module = ?MODULE, function = subscribe_room, args_desc = ["User JID", "a user's nick", "the room to subscribe", "nodes separated by commas: ,"], args_example = ["tom@localhost", "Tom", "room1@conference.localhost", "urn:xmpp:mucsub:nodes:messages,urn:xmpp:mucsub:nodes:affiliations"], result_desc = "The list of nodes that has subscribed", result_example = ["urn:xmpp:mucsub:nodes:messages", "urn:xmpp:mucsub:nodes:affiliations"], args = [{user, binary}, {nick, binary}, {room, binary}, {nodes, binary}], result = {nodes, {list, {node, string}}}}, #ejabberd_commands{name = unsubscribe_room, tags = [muc_room], desc = "Unsubscribe from a MUC conference", module = ?MODULE, function = unsubscribe_room, args_desc = ["User JID", "the room to subscribe"], args_example = ["tom@localhost", "room1@conference.localhost"], 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_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of users that are subscribed to that room", result_example = ["user2@example.com", "user3@example.com"], 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, args_desc = ["Room name", "MUC service", "User JID", "Affiliation to set"], args_example = ["room1", "muc.example.com", "user2@example.com", "member"], args = [{name, binary}, {service, binary}, {jid, binary}, {affiliation, binary}], result = {res, rescode}}, #ejabberd_commands{name = get_room_affiliations, tags = [muc_room], desc = "Get the list of affiliations of a MUC room", module = ?MODULE, function = get_room_affiliations, args_desc = ["Room name", "MUC service"], args_example = ["room1", "muc.example.com"], result_desc = "The list of affiliations with username, domain, affiliation and reason", result_example = [{"user1", "example.com", member, "member"}], args = [{name, binary}, {service, binary}], result = {affiliations, {list, {affiliation, {tuple, [{username, string}, {domain, string}, {affiliation, atom}, {reason, string} ]}} }}}, #ejabberd_commands{name = get_room_affiliation, tags = [muc_room], desc = "Get affiliation of a user in MUC room", module = ?MODULE, function = get_room_affiliation, args_desc = ["Room name", "MUC service", "User JID"], args_example = ["room1", "muc.example.com", "user1@example.com"], result_desc = "Affiliation of the user", result_example = member, args = [{name, binary}, {service, binary}, {jid, binary}], result = {affiliation, atom}} ]. %%% %%% ejabberd commands %%% muc_online_rooms(ServiceArg) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> [<> || {Name, _, _} <- mod_muc:get_online_rooms(Host)] end, Hosts). muc_online_rooms_by_regex(ServiceArg, Regex) -> {_, P} = re:compile(Regex), Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> [build_summary_room(Name, RoomHost, Pid) || {Name, RoomHost, Pid} <- mod_muc:get_online_rooms(Host), is_name_match(Name, P)] end, Hosts). is_name_match(Name, P) -> case re:run(Name, P) of {match, _} -> true; nomatch -> false end. build_summary_room(Name, Host, Pid) -> C = get_room_config(Pid), Public = C#config.public, S = get_room_state(Pid), Participants = maps:size(S#state.users), {<>, misc:atom_to_binary(Public), Participants }. muc_register_nick(Nick, FromBinary, Service) -> try {get_room_serverhost(Service), jid:decode(FromBinary)} of {ServerHost, From} -> Lang = <<"en">>, case mod_muc:iq_set_register_info(ServerHost, Service, From, Nick, Lang) of {result, undefined} -> ok; {error, #stanza_error{reason = 'conflict'}} -> throw({error, "Nick already registered"}); {error, _} -> throw({error, "Database error"}) end catch error:{invalid_domain, _} -> throw({error, "Invalid 'service'"}); error:{unregistered_route, _} -> throw({error, "Invalid 'service'"}); error:{bad_jid, _} -> throw({error, "Invalid 'jid'"}); _ -> throw({error, "Internal error"}) end. muc_unregister_nick(FromBinary, Service) -> muc_register_nick(<<"">>, FromBinary, Service). get_user_rooms(User, Server) -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> Rooms = mod_muc:get_online_rooms_by_user( ServerHost, jid:nodeprep(User), jid:nodeprep(Server)), [<> || {Name, Host} <- Rooms]; false -> [] end end, ejabberd_option:hosts()). get_user_subscriptions(User, Server) -> Services = find_services(global), UserJid = jid:make(jid:nodeprep(User), jid:nodeprep(Server)), lists:flatmap( fun(ServerHost) -> {ok, Rooms} = mod_muc:get_subscribed_rooms(ServerHost, UserJid), [{jid:encode(RoomJid), UserNick, Nodes} || {RoomJid, UserNick, Nodes} <- Rooms] end, Services). %%---------------------------- %% Ad-hoc commands %%---------------------------- %%---------------------------- %% Web Admin %%---------------------------- %%--------------- %% Web Admin Menu web_menu_main(Acc, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. web_menu_host(Acc, _Host, Lang) -> Acc ++ [{<<"muc">>, translate:translate(Lang, ?T("Multi-User Chat"))}]. %%--------------- %% Web Admin Page -define(TDTD(L, N), ?XE(<<"tr">>, [?XCT(<<"td">>, L), ?XC(<<"td">>, integer_to_binary(N)) ])). web_page_main(_, #request{path=[<<"muc">>], lang = Lang} = _Request) -> OnlineRoomsNumber = lists:foldl( fun(Host, Acc) -> Acc + mod_muc:count_online_rooms(Host) end, 0, find_hosts(global)), PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), Res = ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ [?XCT(<<"h3">>, ?T("Statistics")), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?TDTD(?T("Total rooms"), OnlineRoomsNumber) ]) ]), ?XE(<<"ul">>, [?LI([?ACT(<<"rooms/">>, ?T("List of rooms"))])]) ], {stop, Res}; web_page_main(_, #request{path=[<<"muc">>, <<"rooms">>], q = Q, lang = Lang} = _Request) -> Sort_query = get_sort_query(Q), Res = make_rooms_page(global, Lang, Sort_query), {stop, Res}; web_page_main(Acc, _) -> Acc. web_page_host(_, Host, #request{path = [<<"muc">>], q = Q, lang = Lang} = _Request) -> Sort_query = get_sort_query(Q), Res = make_rooms_page(Host, Lang, Sort_query), {stop, Res}; web_page_host(Acc, _, _) -> Acc. %% Returns: {normal | reverse, Integer} get_sort_query(Q) -> case catch get_sort_query2(Q) of {ok, Res} -> Res; _ -> {normal, 1} end. get_sort_query2(Q) -> {value, {_, Binary}} = lists:keysearch(<<"sort">>, 1, Q), Integer = list_to_integer(string:strip(binary_to_list(Binary), right, $/)), case Integer >= 0 of true -> {ok, {normal, Integer}}; false -> {ok, {reverse, abs(Integer)}} end. make_rooms_page(Host, Lang, {Sort_direction, Sort_column}) -> Service = find_service(Host), Rooms_names = get_online_rooms(Service), Rooms_infos = build_info_rooms(Rooms_names), Rooms_sorted = sort_rooms(Sort_direction, Sort_column, Rooms_infos), Rooms_prepared = prepare_rooms_infos(Rooms_sorted), TList = lists:map( fun(Room) -> ?XE(<<"tr">>, [?XC(<<"td">>, E) || E <- Room]) end, Rooms_prepared), Titles = [?T("Jabber ID"), ?T("# participants"), ?T("Last message"), ?T("Public"), ?T("Persistent"), ?T("Logging"), ?T("Just created"), ?T("Room title"), ?T("Node")], {Titles_TR, _} = lists:mapfoldl( fun(Title, Num_column) -> NCS = integer_to_binary(Num_column), TD = ?XE(<<"td">>, [?CT(Title), ?C(<<" ">>), ?AC(<<"?sort=", NCS/binary>>, <<"<">>), ?C(<<" ">>), ?AC(<<"?sort=-", NCS/binary>>, <<">">>)]), {TD, Num_column+1} end, 1, Titles), PageTitle = translate:translate(Lang, ?T("Multi-User Chat")), ?H1GL(PageTitle, <<"modules/#mod-muc">>, <<"mod_muc">>) ++ [?XCT(<<"h2">>, ?T("Chatrooms")), ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, Titles_TR)] ), ?XE(<<"tbody">>, TList) ] ) ]. sort_rooms(Direction, Column, Rooms) -> Rooms2 = lists:keysort(Column, Rooms), case Direction of normal -> Rooms2; reverse -> lists:reverse(Rooms2) end. build_info_rooms(Rooms) -> [build_info_room(Room) || Room <- Rooms]. build_info_room({Name, Host, _ServerHost, Pid}) -> C = get_room_config(Pid), Title = C#config.title, Public = C#config.public, Persistent = C#config.persistent, Logging = C#config.logging, S = get_room_state(Pid), Just_created = S#state.just_created, Num_participants = maps:size(S#state.users), Node = node(Pid), History = (S#state.history)#lqueue.queue, Ts_last_message = case p1_queue:is_empty(History) of true -> <<"A long time ago">>; false -> Last_message1 = get_queue_last(History), {_, _, _, Ts_last, _} = Last_message1, xmpp_util:encode_timestamp(Ts_last) end, {<>, Num_participants, Ts_last_message, Public, Persistent, Logging, Just_created, Title, Node}. get_queue_last(Queue) -> List = p1_queue:to_list(Queue), lists:last(List). prepare_rooms_infos(Rooms) -> [prepare_room_info(Room) || Room <- Rooms]. prepare_room_info(Room_info) -> {NameHost, Num_participants, Ts_last_message, Public, Persistent, Logging, Just_created, Title, Node} = Room_info, [NameHost, integer_to_binary(Num_participants), Ts_last_message, misc:atom_to_binary(Public), misc:atom_to_binary(Persistent), misc:atom_to_binary(Logging), justcreated_to_binary(Just_created), Title, misc:atom_to_binary(Node)]. justcreated_to_binary(J) when is_integer(J) -> JNow = misc:usec_to_now(J), {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(JNow), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]); justcreated_to_binary(J) when is_atom(J) -> misc:atom_to_binary(J). %%---------------------------- %% Create/Delete Room %%---------------------------- %% @spec (Name::binary(), Host::binary(), ServerHost::binary()) -> %% 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, ServerHost1, CustomRoomOpts) -> case {jid:nodeprep(Name1), jid:nodeprep(Host1), jid:nodeprep(ServerHost1)} of {error, _, _} -> throw({error, "Invalid 'name'"}); {_, error, _} -> throw({error, "Invalid 'host'"}); {_, _, error} -> throw({error, "Invalid 'serverhost'"}); {Name, Host, ServerHost} -> case get_room_pid(Name, Host) of room_not_found -> %% Get the default room options from the muc configuration DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), %% 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)), case mod_muc:create_room(Host, Name, RoomOpts) of ok -> maybe_store_room(ServerHost, Host, Name, RoomOpts); {error, _} -> throw({error, "Unable to start room"}) end; invalid_service -> throw({error, "Invalid 'service'"}); _ -> throw({error, "Room already exists"}) end end. maybe_store_room(ServerHost, Host, Name, RoomOpts) -> case proplists:get_bool(persistent, RoomOpts) of true -> {atomic, _} = mod_muc:store_room(ServerHost, Host, Name, RoomOpts), ok; false -> ok end. %% Create the room only in the database. %% It is required to restart the MUC service for the room to appear. muc_create_room(ServerHost, {Name, Host, _}, DefRoomOpts) -> io:format("Creating room ~ts@~ts~n", [Name, Host]), mod_muc:store_room(ServerHost, Host, Name, DefRoomOpts). %% @spec (Name::binary(), Host::binary()) -> %% ok | {error, room_not_exists} %% @doc Destroy the room immediately. %% If the room has participants, they are not notified that the room was destroyed; %% they will notice when they try to chat and receive an error that the room doesn't exist. destroy_room(Name1, Service1) -> case {jid:nodeprep(Name1), jid:nodeprep(Service1)} of {error, _} -> throw({error, "Invalid 'name'"}); {_, error} -> throw({error, "Invalid 'service'"}); {Name, Service} -> case get_room_pid(Name, Service) of room_not_found -> throw({error, "Room doesn't exists"}); invalid_service -> throw({error, "Invalid 'service'"}); Pid -> mod_muc_room:destroy(Pid), ok end end. destroy_room({N, H, SH}) -> io:format("Destroying room: ~ts@~ts - vhost: ~ts~n", [N, H, SH]), destroy_room(N, H). %%---------------------------- %% Destroy Rooms in File %%---------------------------- %% The format of the file is: one chatroom JID per line %% The file encoding must be UTF-8 destroy_rooms_file(Filename) -> {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), [destroy_room(A) || A <- Rooms], ok. read_rooms(_F, eof, L) -> L; read_rooms(F, no_room, L) -> RJID2 = read_room(F), read_rooms(F, RJID2, L); read_rooms(F, RJID, L) -> RJID2 = read_room(F), read_rooms(F, RJID2, [RJID | L]). read_room(F) -> case io:get_line(F, "") of eof -> eof; String -> case io_lib:fread("~ts", String) of {ok, [RoomJID], _} -> split_roomjid(list_to_binary(RoomJID)); {error, What} -> io:format("Parse error: what: ~p~non the line: ~p~n~n", [What, String]) end end. %% This function is quite rudimentary %% and may not be accurate split_roomjid(RoomJID) -> split_roomjid2(binary:split(RoomJID, <<"@">>)). split_roomjid2([Name, Host]) -> [_MUC_service_name, ServerHost] = binary:split(Host, <<".">>), {Name, Host, ServerHost}; split_roomjid2(_) -> no_room. %%---------------------------- %% Create Rooms in File %%---------------------------- create_rooms_file(Filename) -> {ok, F} = file:open(Filename, [read]), RJID = read_room(F), Rooms = read_rooms(F, RJID, []), file:close(F), %% Read the default room options defined for the first virtual host DefRoomOpts = mod_muc_opt:default_room_options(ejabberd_config:get_myname()), [muc_create_room(ejabberd_config:get_myname(), A, DefRoomOpts) || A <- Rooms], ok. %%--------------------------------- %% List/Delete Unused/Empty Rooms %%--------------------------------- %%--------------- %% Control rooms_unused_list(Service, Days) -> rooms_report(unused, list, Service, Days). rooms_unused_destroy(Service, Days) -> rooms_report(unused, destroy, Service, Days). rooms_empty_list(Service) -> rooms_report(empty, list, Service, 0). rooms_empty_destroy(Service) -> rooms_report(empty, destroy, Service, 0). rooms_report(Method, Action, Service, Days) -> {NA, NP, RP} = muc_unused(Method, Action, Service, Days), io:format("rooms ~ts: ~p out of ~p~n", [Method, NP, NA]), [<> || {R, H, _SH, _P} <- RP]. muc_unused(Method, Action, Service, Last_allowed) -> %% Get all required info about all existing rooms Rooms_all = get_all_rooms(Service), %% Decide which ones pass the requirements Rooms_pass = decide_rooms(Method, Rooms_all, Last_allowed), Num_rooms_all = length(Rooms_all), Num_rooms_pass = length(Rooms_pass), %% Perform the desired action for matching rooms act_on_rooms(Method, Action, Rooms_pass), {Num_rooms_all, Num_rooms_pass, Rooms_pass}. %%--------------- %% Get info get_online_rooms(ServiceArg) -> Hosts = find_services(ServiceArg), lists:flatmap( fun(Host) -> ServerHost = get_room_serverhost(Host), [{RoomName, RoomHost, ServerHost, Pid} || {RoomName, RoomHost, Pid} <- mod_muc:get_online_rooms(Host)] end, Hosts). get_all_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), OnlineRooms = get_online_rooms(Host), OnlineMap = lists:foldl( fun({Room, _, _, _}, Map) -> Map#{Room => 1} end, #{}, OnlineRooms), Mod = gen_mod:db_mod(ServerHost, mod_muc), DbRooms = case erlang:function_exported(Mod, get_rooms_without_subscribers, 2) of true -> Mod:get_rooms_without_subscribers(ServerHost, Host); _ -> Mod:get_rooms(ServerHost, Host) end, StoredRooms = lists:filtermap( fun(#muc_room{name_host = {Room, _}, opts = Opts}) -> case maps:is_key(Room, OnlineMap) of true -> false; _ -> {true, {Room, Host, ServerHost, Opts}} end end, DbRooms), OnlineRooms ++ StoredRooms. get_room_config(Room_pid) -> {ok, R} = mod_muc_room:get_config(Room_pid), R. get_room_state(Room_pid) -> {ok, R} = mod_muc_room:get_state(Room_pid), R. %%--------------- %% Decide decide_rooms(Method, Rooms, Last_allowed) -> Decide = fun(R) -> decide_room(Method, R, Last_allowed) end, lists:filter(Decide, Rooms). decide_room(unused, {_Room_name, _Host, ServerHost, Room_pid}, Last_allowed) -> NodeStartTime = erlang:system_time(microsecond) - 1000000*(erlang:monotonic_time(second)-ejabberd_config:get_node_start()), OnlyHibernated = case mod_muc_opt:hibernation_timeout(ServerHost) of Value when Value < Last_allowed*24*60*60*1000 -> true; _ -> false end, {Just_created, Num_users} = case Room_pid of Pid when is_pid(Pid) andalso OnlyHibernated -> {erlang:system_time(microsecond), 0}; Pid when is_pid(Pid) -> case mod_muc_room:get_state(Room_pid) of {ok, #state{just_created = JC, users = U}} -> {JC, maps:size(U)}; _ -> {erlang:system_time(microsecond), 0} end; Opts -> case lists:keyfind(hibernation_time, 1, Opts) of false -> {NodeStartTime, 0}; {_, T} -> {T, 0} end end, Last = case Just_created of true -> 0; _ -> (erlang:system_time(microsecond) - Just_created) div 1000000 end, case {Num_users, seconds_to_days(Last)} of {0, Last_days} when (Last_days >= Last_allowed) -> true; _ -> false end; decide_room(empty, {Room_name, Host, ServerHost, Room_pid}, _Last_allowed) -> case gen_mod:is_loaded(ServerHost, mod_mam) of true -> Room_options = case Room_pid of _ when is_pid(Room_pid) -> get_room_options(Room_pid); Opts -> Opts end, case lists:keyfind(<<"mam">>, 1, Room_options) of {<<"mam">>, <<"true">>} -> mod_mam:is_empty_for_room(ServerHost, Room_name, Host); _ -> false end; _ -> false end. seconds_to_days(S) -> S div (60*60*24). %%--------------- %% Act act_on_rooms(Method, Action, Rooms) -> Delete = fun(Room) -> act_on_room(Method, Action, Room) end, lists:foreach(Delete, Rooms). act_on_room(Method, destroy, {N, H, _SH, Pid}) -> Message = iolist_to_binary(io_lib:format( <<"Room destroyed by rooms_~s_destroy.">>, [Method])), case Pid of V when is_pid(V) -> mod_muc_room:destroy(Pid, Message); _ -> case get_room_pid(N, H) of Pid2 when is_pid(Pid2) -> mod_muc_room:destroy(Pid2, Message); _ -> ok end end; act_on_room(_Method, list, _) -> ok. %%---------------------------- %% Change Room Option %%---------------------------- get_room_occupants(Room, Host) -> case get_room_pid(Room, Host) of Pid when is_pid(Pid) -> get_room_occupants(Pid); _ -> throw({error, room_not_found}) end. get_room_occupants(Pid) -> S = get_room_state(Pid), lists:map( fun({_LJID, Info}) -> {jid:encode(Info#user.jid), Info#user.nick, atom_to_list(Info#user.role)} end, maps:to_list(S#state.users)). get_room_occupants_number(Room, Host) -> case get_room_pid(Room, Host) of Pid when is_pid(Pid )-> S = get_room_state(Pid), maps:size(S#state.users); _ -> throw({error, room_not_found}) end. %%---------------------------- %% Send Direct Invitation %%---------------------------- %% http://xmpp.org/extensions/xep-0249.html send_direct_invitation(RoomName, RoomService, Password, Reason, UsersString) -> case jid:make(RoomName, RoomService) of error -> throw({error, "Invalid 'roomname' or 'service'"}); RoomJid -> XmlEl = build_invitation(Password, Reason, RoomJid), Users = get_users_to_invite(RoomJid, UsersString), [send_direct_invitation(RoomJid, UserJid, XmlEl) || UserJid <- Users], ok end. get_users_to_invite(RoomJid, UsersString) -> UsersStrings = binary:split(UsersString, <<":">>, [global]), OccupantsTuples = get_room_occupants(RoomJid#jid.luser, RoomJid#jid.lserver), OccupantsJids = [jid:decode(JidString) || {JidString, _Nick, _} <- OccupantsTuples], lists:filtermap( fun(UserString) -> UserJid = jid:decode(UserString), Val = lists:all(fun(OccupantJid) -> UserJid#jid.luser /= OccupantJid#jid.luser orelse UserJid#jid.lserver /= OccupantJid#jid.lserver end, OccupantsJids), case {UserJid#jid.luser, Val} of {<<>>, _} -> false; {_, true} -> {true, UserJid}; _ -> false end end, UsersStrings). build_invitation(Password, Reason, RoomJid) -> Invite = #x_conference{jid = RoomJid, password = case Password of <<"none">> -> <<>>; _ -> Password end, reason = case Reason of <<"none">> -> <<>>; _ -> Reason end}, #message{sub_els = [Invite]}. send_direct_invitation(FromJid, UserJid, Msg) -> ejabberd_router:route(xmpp:set_from_to(Msg, FromJid, UserJid)). %%---------------------------- %% Change Room Option %%---------------------------- %% @spec(Name::string(), Service::string(), Option::string(), Value) -> ok %% Value = atom() | integer() | string() %% @doc Change an option in an existing room. %% Requires the name of the room, the MUC service where it exists, %% 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, OptionString, ValueString) -> case get_room_pid(Name, Service) of room_not_found -> throw({error, "Room not found"}); invalid_service -> throw({error, "Invalid 'service'"}); Pid -> {Option, Value} = format_room_option(OptionString, ValueString), change_room_option(Pid, Option, Value) end. change_room_option(Pid, Option, Value) -> case {Option, gen_mod:is_loaded((get_room_state(Pid))#state.server_host, mod_muc_log)} of {logging, false} -> mod_muc_log_not_enabled; _ -> Config = get_room_config(Pid), Config2 = change_option(Option, Value, Config), {ok, _} = mod_muc_room:set_config(Pid, Config2), ok end. format_room_option(OptionString, ValueString) -> Option = misc:binary_to_atom(OptionString), Value = case Option of title -> ValueString; description -> ValueString; password -> ValueString; subject ->ValueString; subject_author ->ValueString; presence_broadcast ->misc:expr_to_term(ValueString); max_users -> binary_to_integer(ValueString); voice_request_min_interval -> binary_to_integer(ValueString); vcard -> ValueString; vcard_xupdate when ValueString /= <<"undefined">>, ValueString /= <<"external">> -> ValueString; lang -> ValueString; pubsub -> ValueString; _ -> misc:binary_to_atom(ValueString) end, {Option, Value}. %% @doc Get the Pid of an existing MUC room, or 'room_not_found'. -spec get_room_pid(binary(), binary()) -> pid() | room_not_found | invalid_service. get_room_pid(Name, Service) -> try get_room_serverhost(Service) of ServerHost -> case mod_muc:unhibernate_room(ServerHost, Service, Name) of error -> room_not_found; {ok, Pid} -> Pid end catch error:{invalid_domain, _} -> invalid_service; error:{unregistered_route, _} -> invalid_service end. %% It is required to put explicitly all the options because %% the record elements are replaced at compile time. %% So, this can't be parametrized. change_option(Option, Value, Config) -> case Option of allow_change_subj -> Config#config{allow_change_subj = Value}; 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}; allow_voice_requests -> Config#config{allow_voice_requests = Value}; anonymous -> Config#config{anonymous = Value}; captcha_protected -> Config#config{captcha_protected = Value}; description -> Config#config{description = Value}; lang -> Config#config{lang = Value}; logging -> Config#config{logging = Value}; mam -> Config#config{mam = Value}; max_users -> Config#config{max_users = Value}; members_by_default -> Config#config{members_by_default = Value}; members_only -> Config#config{members_only = Value}; moderated -> Config#config{moderated = Value}; password -> Config#config{password = Value}; password_protected -> Config#config{password_protected = Value}; persistent -> Config#config{persistent = Value}; presence_broadcast -> Config#config{presence_broadcast = Value}; public -> Config#config{public = Value}; public_list -> Config#config{public_list = Value}; pubsub -> Config#config{pubsub = Value}; title -> Config#config{title = Value}; vcard -> Config#config{vcard = Value}; vcard_xupdate -> Config#config{vcard_xupdate = Value}; voice_request_min_interval -> Config#config{voice_request_min_interval = Value} end. %%---------------------------- %% Get Room Options %%---------------------------- get_room_options(Name, Service) -> case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> get_room_options(Pid); _ -> [] end. get_room_options(Pid) -> Config = get_room_config(Pid), get_options(Config). get_options(Config) -> Fields = [misc:atom_to_binary(Field) || Field <- record_info(fields, config)], [config | ValuesRaw] = tuple_to_list(Config), Values = lists:map(fun(V) when is_atom(V) -> misc:atom_to_binary(V); (V) when is_integer(V) -> integer_to_binary(V); (V) when is_tuple(V); is_list(V) -> list_to_binary(hd(io_lib:format("~w", [V]))); (V) -> V end, ValuesRaw), lists:zip(Fields, Values). %%---------------------------- %% Get Room Affiliations %%---------------------------- %% @spec(Name::binary(), Service::binary()) -> %% [{JID::string(), Domain::string(), Role::string(), Reason::string()}] %% @doc Get the affiliations of the room Name@Service. get_room_affiliations(Name, Service) -> case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID of the online room, then request its state {ok, StateData} = mod_muc_room:get_state(Pid), Affiliations = maps:to_list(StateData#state.affiliations), lists:map( fun({{Uname, Domain, _Res}, {Aff, Reason}}) when is_atom(Aff)-> {Uname, Domain, Aff, Reason}; ({{Uname, Domain, _Res}, Aff}) when is_atom(Aff)-> {Uname, Domain, Aff, <<>>} end, Affiliations); _ -> throw({error, "The room does not exist."}) end. %%---------------------------- %% Get Room Affiliation %%---------------------------- %% @spec(Name::binary(), Service::binary(), JID::binary()) -> %% {Affiliation::string()} %% @doc Get affiliation of a user in the room Name@Service. get_room_affiliation(Name, Service, JID) -> case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID of the online room, then request its state {ok, StateData} = mod_muc_room:get_state(Pid), UserJID = jid:decode(JID), mod_muc_room:get_affiliation(UserJID, StateData); _ -> throw({error, "The room does not exist."}) end. %%---------------------------- %% Change Room Affiliation %%---------------------------- %% @spec(Name, Service, JID, AffiliationString) -> ok | {error, Error} %% Name = binary() %% Service = binary() %% JID = binary() %% AffiliationString = "outcast" | "none" | "member" | "admin" | "owner" %% @doc Set the affiliation of JID in the room Name@Service. %% If the affiliation is 'none', the action is to remove, %% In any other case the action will be to create the affiliation. set_room_affiliation(Name, Service, JID, AffiliationString) -> Affiliation = case AffiliationString of <<"outcast">> -> outcast; <<"none">> -> none; <<"member">> -> member; <<"admin">> -> admin; <<"owner">> -> owner; _ -> throw({error, "Invalid affiliation"}) end, case get_room_pid(Name, Service) of Pid when is_pid(Pid) -> %% Get the PID for the online room so we can get the state of the room case mod_muc_room:change_item(Pid, jid:decode(JID), affiliation, Affiliation, <<"">>) of {ok, _} -> ok; {error, notfound} -> throw({error, "Room doesn't exists"}); {error, _} -> throw({error, "Unable to perform change"}) end; room_not_found -> throw({error, "Room doesn't exists"}); invalid_service -> throw({error, "Invalid 'service'"}) end. %%% %%% MUC Subscription %%% subscribe_room(_User, Nick, _Room, _Nodes) when Nick == <<"">> -> throw({error, "Nickname must be set"}); subscribe_room(User, Nick, Room, Nodes) -> NodeList = re:split(Nodes, "\\h*,\\h*"), try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> try jid:decode(User) of UserJID1 -> UserJID = jid:replace_resource(UserJID1, <<"modmucadmin">>), case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> case mod_muc_room:subscribe( Pid, UserJID, Nick, NodeList) of {ok, SubscribedNodes} -> SubscribedNodes; {error, Reason} -> throw({error, binary_to_list(Reason)}) end; _ -> throw({error, "The room does not exist"}) end catch _:{bad_jid, _} -> throw({error, "Malformed user JID"}) end; _ -> throw({error, "Malformed room JID"}) catch _:{bad_jid, _} -> throw({error, "Malformed room JID"}) end. unsubscribe_room(User, Room) -> try jid:decode(Room) of #jid{luser = Name, lserver = Host} when Name /= <<"">> -> try jid:decode(User) of UserJID -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> case mod_muc_room:unsubscribe(Pid, UserJID) of ok -> ok; {error, Reason} -> throw({error, binary_to_list(Reason)}) end; _ -> throw({error, "The room does not exist"}) end catch _:{bad_jid, _} -> throw({error, "Malformed user JID"}) end; _ -> throw({error, "Malformed room JID"}) catch _:{bad_jid, _} -> throw({error, "Malformed room JID"}) end. get_subscribers(Name, Host) -> case get_room_pid(Name, Host) of Pid when is_pid(Pid) -> {ok, JIDList} = mod_muc_room:get_subscribers(Pid), [jid:encode(jid:remove_resource(J)) || J <- JIDList]; _ -> throw({error, "The room does not exist"}) end. %%---------------------------- %% Utils %%---------------------------- find_service(global) -> global; find_service(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). find_services(Global) when Global == global; Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_service(ServerHost)]; false -> [] end end, ejabberd_option:hosts()); find_services(Service) when is_binary(Service) -> [Service]. get_room_serverhost(Service) when is_binary(Service) -> ejabberd_router:host_of_route(Service). find_host(ServerHost) -> hd(gen_mod:get_module_opt_hosts(ServerHost, mod_muc)). find_hosts(Global) when Global == global; Global == <<"global">> -> lists:flatmap( fun(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_host(ServerHost)]; false -> [] end end, ejabberd_option:hosts()); find_hosts(ServerHost) -> case gen_mod:is_loaded(ServerHost, mod_muc) of true -> [find_host(ServerHost)]; false -> [] end. mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides commands to administer local MUC " "services and their MUC rooms. It also provides simple " "WebAdmin pages to view the existing rooms."), "", ?T("This module depends on _`mod_muc`_.")]}. ejabberd-21.12/src/ejabberd_s2s_out.erl0000644000232200023220000003457114154362354020353 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_s2s_out). -behaviour(xmpp_stream_out). %% xmpp_stream_out callbacks -export([tls_options/1, tls_required/1, tls_verify/1, tls_enabled/1, connect_options/3, connect_timeout/1, address_families/1, default_port/1, dns_retries/1, dns_timeout/1, handle_auth_success/2, handle_auth_failure/3, handle_packet/2, handle_stream_end/2, handle_stream_downgraded/2, handle_recv/3, handle_send/3, handle_cdata/2, handle_stream_established/1, handle_timeout/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% Hooks -export([process_auth_result/2, process_closed/2, handle_unexpected_info/2, handle_unexpected_cast/2, process_downgraded/2]). %% API -export([start/3, start_link/3, connect/1, close/1, close/2, stop_async/1, send/2, route/2, establish/1, update_state/2, host_up/1, host_down/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -type state() :: xmpp_stream_out:state(). -export_type([state/0]). %%%=================================================================== %%% API %%%=================================================================== start(From, To, Opts) -> case proplists:get_value(supervisor, Opts, true) of true -> case supervisor:start_child(ejabberd_s2s_out_sup, [From, To, Opts]) of {ok, undefined} -> ignore; Res -> Res end; _ -> xmpp_stream_out:start(?MODULE, [From, To, Opts], ejabberd_config:fsm_limit_opts([])) end. start_link(From, To, Opts) -> xmpp_stream_out:start_link(?MODULE, [From, To, Opts], ejabberd_config:fsm_limit_opts([])). -spec connect(pid()) -> ok. connect(Ref) -> xmpp_stream_out:connect(Ref). -spec close(pid()) -> ok; (state()) -> state(). close(Ref) -> xmpp_stream_out:close(Ref). -spec close(pid(), atom()) -> ok. close(Ref, Reason) -> xmpp_stream_out:close(Ref, Reason). -spec stop_async(pid()) -> ok. stop_async(Pid) -> xmpp_stream_out:stop_async(Pid). -spec send(pid(), xmpp_element()) -> ok; (state(), xmpp_element()) -> state(). send(Stream, Pkt) -> xmpp_stream_out:send(Stream, Pkt). -spec route(pid(), xmpp_element()) -> ok. route(Ref, Pkt) -> Ref ! {route, Pkt}, ok. -spec establish(state()) -> state(). establish(State) -> xmpp_stream_out:establish(State). -spec update_state(pid(), fun((state()) -> state()) | {module(), atom(), list()}) -> ok. update_state(Ref, Callback) -> xmpp_stream_out:cast(Ref, {update_state, Callback}). -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, process_auth_result, 100), ejabberd_hooks:add(s2s_out_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:add(s2s_out_handle_info, Host, ?MODULE, handle_unexpected_info, 100), ejabberd_hooks:add(s2s_out_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100), ejabberd_hooks:add(s2s_out_downgraded, Host, ?MODULE, process_downgraded, 100). -spec host_down(binary()) -> ok. host_down(Host) -> ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, process_auth_result, 100), ejabberd_hooks:delete(s2s_out_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:delete(s2s_out_handle_info, Host, ?MODULE, handle_unexpected_info, 100), ejabberd_hooks:delete(s2s_out_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100), ejabberd_hooks:delete(s2s_out_downgraded, Host, ?MODULE, process_downgraded, 100). %%%=================================================================== %%% Hooks %%%=================================================================== process_auth_result(#{server := LServer, remote_server := RServer} = State, {false, Reason}) -> Delay = get_delay(), ?WARNING_MSG("Failed to establish outbound s2s connection ~ts -> ~ts: " "authentication failed; bouncing for ~p seconds", [LServer, RServer, Delay div 1000]), State1 = State#{on_route => bounce, stop_reason => Reason}, State2 = close(State1), State3 = bounce_queue(State2), xmpp_stream_out:set_timeout(State3, Delay); process_auth_result(State, true) -> State. process_closed(#{server := LServer, remote_server := RServer, on_route := send} = State, Reason) -> ?INFO_MSG("Closing outbound s2s connection ~ts -> ~ts: ~ts", [LServer, RServer, format_error(Reason)]), stop_async(self()), State; process_closed(#{server := LServer, remote_server := RServer} = State, Reason) -> Delay = get_delay(), ?WARNING_MSG("Failed to establish outbound s2s connection ~ts -> ~ts: ~ts; " "bouncing for ~p seconds", [LServer, RServer, format_error(Reason), Delay div 1000]), State1 = State#{on_route => bounce}, State2 = bounce_queue(State1), xmpp_stream_out:set_timeout(State2, Delay). handle_unexpected_info(State, Info) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), State. handle_unexpected_cast(State, Msg) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), State. process_downgraded(State, _StreamStart) -> send(State, xmpp:serr_unsupported_version()). %%%=================================================================== %%% xmpp_stream_out callbacks %%%=================================================================== tls_options(#{server := LServer, server_host := ServerHost}) -> ejabberd_s2s:tls_options(LServer, ServerHost, []). tls_required(#{server_host := ServerHost}) -> ejabberd_s2s:tls_required(ServerHost). tls_verify(#{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_tls_verify, ServerHost, true, [State]). tls_enabled(#{server_host := ServerHost}) -> ejabberd_s2s:tls_enabled(ServerHost). connect_options(Addr, Opts, #{server_host := ServerHost}) -> BindAddr = case get_addr_type(Addr) of inet -> ejabberd_option:outgoing_s2s_ipv4_address(ServerHost); inet6 -> ejabberd_option:outgoing_s2s_ipv6_address(ServerHost) end, case BindAddr of undefined -> Opts; _ -> [{ip, BindAddr} | Opts] end. connect_timeout(#{server_host := ServerHost}) -> ejabberd_option:outgoing_s2s_timeout(ServerHost). default_port(#{server_host := ServerHost}) -> ejabberd_option:outgoing_s2s_port(ServerHost). address_families(#{server_host := ServerHost}) -> ejabberd_option:outgoing_s2s_families(ServerHost). dns_retries(#{server_host := ServerHost}) -> ejabberd_option:s2s_dns_retries(ServerHost). dns_timeout(#{server_host := ServerHost}) -> ejabberd_option:s2s_dns_timeout(ServerHost). handle_auth_success(Mech, #{socket := Socket, ip := IP, remote_server := RServer, server_host := ServerHost, server := LServer} = State) -> ?INFO_MSG("(~ts) Accepted outbound s2s ~ts authentication ~ts -> ~ts (~ts)", [xmpp_socket:pp(Socket), Mech, LServer, RServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), ejabberd_hooks:run_fold(s2s_out_auth_result, ServerHost, State, [true]). handle_auth_failure(Mech, Reason, #{socket := Socket, ip := IP, remote_server := RServer, server_host := ServerHost, server := LServer} = State) -> ?WARNING_MSG("(~ts) Failed outbound s2s ~ts authentication ~ts -> ~ts (~ts): ~ts", [xmpp_socket:pp(Socket), Mech, LServer, RServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP)), xmpp_stream_out:format_error(Reason)]), ejabberd_hooks:run_fold(s2s_out_auth_result, ServerHost, State, [{false, Reason}]). handle_packet(Pkt, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_packet, ServerHost, State, [Pkt]). handle_stream_end(Reason, #{server_host := ServerHost} = State) -> State1 = State#{stop_reason => Reason}, ejabberd_hooks:run_fold(s2s_out_closed, ServerHost, State1, [Reason]). handle_stream_downgraded(StreamStart, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_downgraded, ServerHost, State, [StreamStart]). handle_stream_established(State) -> State1 = State#{on_route => send}, State2 = resend_queue(State1), set_idle_timeout(State2). handle_cdata(Data, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_cdata, ServerHost, State, [Data]). handle_recv(El, Pkt, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_recv, ServerHost, State, [El, Pkt]). handle_send(El, Pkt, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_send, ServerHost, State, [El, Pkt]). handle_timeout(#{on_route := Action, lang := Lang} = State) -> case Action of bounce -> stop_async(self()), State; _ -> Txt = ?T("Idle connection"), send(State, xmpp:serr_connection_timeout(Txt, Lang)) end. init([#{server := LServer, remote_server := RServer} = State, Opts]) -> ServerHost = ejabberd_router:host_of_route(LServer), QueueType = ejabberd_s2s:queue_type(ServerHost), QueueLimit = case lists:keyfind( max_queue, 1, ejabberd_config:fsm_limit_opts([])) of {_, N} -> N; false -> unlimited end, Timeout = ejabberd_option:negotiation_timeout(), State1 = State#{on_route => queue, queue => p1_queue:new(QueueType, QueueLimit), xmlns => ?NS_SERVER, lang => ejabberd_option:language(), server_host => ServerHost, shaper => none}, State2 = xmpp_stream_out:set_timeout(State1, Timeout), ?INFO_MSG("Outbound s2s connection started: ~ts -> ~ts", [LServer, RServer]), ejabberd_hooks:run_fold(s2s_out_init, ServerHost, {ok, State2}, [Opts]). handle_call(Request, From, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_call, ServerHost, State, [Request, From]). handle_cast({update_state, Fun}, State) -> case Fun of {M, F, A} -> erlang:apply(M, F, [State|A]); _ when is_function(Fun) -> Fun(State) end; handle_cast(Msg, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_cast, ServerHost, State, [Msg]). handle_info({route, Pkt}, #{queue := Q, on_route := Action} = State) -> case Action of queue -> try State#{queue => p1_queue:in(Pkt, Q)} catch error:full -> Q1 = p1_queue:set_limit(Q, unlimited), Q2 = p1_queue:in(Pkt, Q1), handle_stream_end(queue_full, State#{queue => Q2}) end; bounce -> bounce_packet(Pkt, State); send -> set_idle_timeout(send(State, Pkt)) end; handle_info(Info, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_out_handle_info, ServerHost, State, [Info]). terminate(Reason, #{server := LServer, remote_server := RServer} = State) -> State1 = case Reason of normal -> State; _ -> State#{stop_reason => internal_failure} end, State2 = bounce_queue(State1), bounce_message_queue({LServer, RServer}, State2). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_addr_type(inet:ip_address()) -> inet:address_family(). get_addr_type({_, _, _, _}) -> inet; get_addr_type({_, _, _, _, _, _, _, _}) -> inet6. -spec resend_queue(state()) -> state(). resend_queue(State) -> queue_fold( fun(Pkt, AccState) -> send(AccState, Pkt) end, State). -spec bounce_queue(state()) -> state(). bounce_queue(State) -> queue_fold( fun(Pkt, AccState) -> bounce_packet(Pkt, AccState) end, State). -spec bounce_message_queue({binary(), binary()}, state()) -> state(). bounce_message_queue(FromTo, State) -> receive {route, Pkt} -> State1 = bounce_packet(Pkt, State), bounce_message_queue(FromTo, State1) after 0 -> State end. -spec bounce_packet(xmpp_element(), state()) -> state(). bounce_packet(Pkt, State) when ?is_stanza(Pkt) -> Lang = xmpp:get_lang(Pkt), Err = mk_bounce_error(Lang, State), ejabberd_router:route_error(Pkt, Err), State; bounce_packet(_, State) -> State. -spec mk_bounce_error(binary(), state()) -> stanza_error(). mk_bounce_error(Lang, #{stop_reason := Why}) -> Reason = format_error(Why), case Why of internal_failure -> xmpp:err_internal_server_error(Reason, Lang); queue_full -> xmpp:err_resource_constraint(Reason, Lang); {dns, _} -> xmpp:err_remote_server_not_found(Reason, Lang); {idna, _} -> xmpp:err_remote_server_not_found(Reason, Lang); _ -> xmpp:err_remote_server_timeout(Reason, Lang) end; mk_bounce_error(_Lang, _State) -> %% We should not be here. Probably :) xmpp:err_remote_server_not_found(). -spec get_delay() -> non_neg_integer(). get_delay() -> MaxDelay = ejabberd_option:s2s_max_retry_delay(), p1_rand:uniform(MaxDelay). -spec set_idle_timeout(state()) -> state(). set_idle_timeout(#{on_route := send, server_host := ServerHost} = State) -> Timeout = ejabberd_s2s:get_idle_timeout(ServerHost), xmpp_stream_out:set_timeout(State, Timeout); set_idle_timeout(State) -> State. -spec queue_fold(fun((xmpp_element(), state()) -> state()), state()) -> state(). queue_fold(F, #{queue := Q} = State) -> case p1_queue:out(Q) of {{value, Pkt}, Q1} -> State1 = F(Pkt, State#{queue => Q1}), queue_fold(F, State1); {empty, Q1} -> State#{queue => Q1} end. format_error(internal_failure) -> <<"Internal server error">>; format_error(queue_full) -> <<"Stream queue is overloaded">>; format_error(Reason) -> xmpp_stream_out:format_error(Reason). ejabberd-21.12/src/gen_pubsub_node.erl0000644000232200023220000001517714154362354020276 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : gen_pubsub_node.erl %%% Author : Christophe Romain %%% Purpose : Define pubsub plugin behaviour %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(gen_pubsub_node). -include_lib("xmpp/include/xmpp.hrl"). -type(host() :: mod_pubsub:host()). -type(nodeId() :: mod_pubsub:nodeId()). -type(nodeIdx() :: mod_pubsub:nodeIdx()). -type(itemId() :: mod_pubsub:itemId()). -type(pubsubNode() :: mod_pubsub:pubsubNode()). -type(pubsubState() :: mod_pubsub:pubsubState()). -type(pubsubItem() :: mod_pubsub:pubsubItem()). -type(subOptions() :: mod_pubsub:subOptions()). -type(pubOptions() :: mod_pubsub:pubOptions()). -type(affiliation() :: mod_pubsub:affiliation()). -type(subscription() :: mod_pubsub:subscription()). -type(subId() :: mod_pubsub:subId()). -type(accessModel() :: mod_pubsub:accessModel()). -type(publishModel() :: mod_pubsub:publishModel()). -type(payload() :: mod_pubsub:payload()). -callback init(Host :: binary(), ServerHost :: binary(), Opts :: [any()]) -> atom(). -callback terminate(Host :: host(), ServerHost :: binary()) -> atom(). -callback options() -> [{atom(), any()}]. -callback features() -> [binary()]. -callback create_node_permission(Host :: host(), ServerHost :: binary(), Node :: nodeId(), ParentNode :: nodeId(), Owner :: jid(), Access :: atom()) -> {result, boolean()}. -callback create_node(NodeIdx :: nodeIdx(), Owner :: jid()) -> {result, {default, broadcast}}. -callback delete_node(Nodes :: [pubsubNode(),...]) -> {result, {default, broadcast, [{pubsubNode(), [{ljid(), [{subscription(), subId()}]},...]},...] } } | {result, {[], [{pubsubNode(), [{ljid(), [{subscription(), subId()}]},...]},...] } }. -callback purge_node(NodeIdx :: nodeIdx(), Owner :: jid()) -> {result, {default, broadcast}} | {error, stanza_error()}. -callback subscribe_node(NodeIdx :: nodeIdx(), Sender :: jid(), Subscriber :: jid(), AccessModel :: accessModel(), SendLast :: 'never' | 'on_sub' | 'on_sub_and_presence', PresenceSubscription :: boolean(), RosterGroup :: boolean(), Options :: subOptions()) -> {result, {default, subscribed, subId()}} | {result, {default, subscribed, subId(), send_last}} | {result, {default, pending, subId()}} | {error, stanza_error()}. -callback unsubscribe_node(NodeIdx :: nodeIdx(), Sender :: jid(), Subscriber :: jid(), SubId :: subId()) -> {result, []} | {error, stanza_error()}. -callback publish_item(NodeId :: nodeIdx(), Publisher :: jid(), PublishModel :: publishModel(), Max_Items :: non_neg_integer(), ItemId :: <<>> | itemId(), Payload :: payload(), Options :: pubOptions()) -> {result, {default, broadcast, [itemId()]}} | {error, stanza_error()}. -callback delete_item(NodeIdx :: nodeIdx(), Publisher :: jid(), PublishModel :: publishModel(), ItemId :: <<>> | itemId()) -> {result, {default, broadcast}} | {error, stanza_error()}. -callback remove_extra_items(NodeIdx :: nodeIdx(), Max_Items :: unlimited | non_neg_integer()) -> {result, {[itemId()], [itemId()]} }. -callback remove_extra_items(NodeIdx :: nodeIdx(), Max_Items :: unlimited | non_neg_integer(), ItemIds :: [itemId()]) -> {result, {[itemId()], [itemId()]} }. -callback remove_expired_items(NodeIdx :: nodeIdx(), Seconds :: infinity | non_neg_integer()) -> {result, [itemId()]}. -callback get_node_affiliations(NodeIdx :: nodeIdx()) -> {result, [{ljid(), affiliation()}]}. -callback get_entity_affiliations(Host :: host(), Owner :: jid()) -> {result, [{pubsubNode(), affiliation()}]}. -callback get_affiliation(NodeIdx :: nodeIdx(), Owner :: jid()) -> {result, affiliation()}. -callback set_affiliation(NodeIdx :: nodeIdx(), Owner :: jid(), Affiliation :: affiliation()) -> {result, ok} | {error, stanza_error()}. -callback get_node_subscriptions(NodeIdx :: nodeIdx()) -> {result, [{ljid(), subscription(), subId()}] | [{ljid(), none},...] }. -callback get_entity_subscriptions(Host :: host(), Key :: jid()) -> {result, [{pubsubNode(), subscription(), subId(), ljid()}] }. -callback get_subscriptions(NodeIdx :: nodeIdx(), Owner :: jid()) -> {result, [{subscription(), subId()}]}. -callback get_pending_nodes(Host :: host(), Owner :: jid()) -> {result, [nodeId()]}. -callback get_states(NodeIdx::nodeIdx()) -> {result, [pubsubState()]}. -callback get_state(NodeIdx :: nodeIdx(), Key :: ljid()) -> pubsubState(). -callback set_state(State::pubsubState()) -> ok | {error, stanza_error()}. -callback get_items(nodeIdx(), jid(), accessModel(), boolean(), boolean(), binary(), undefined | rsm_set()) -> {result, {[pubsubItem()], undefined | rsm_set()}} | {error, stanza_error()}. -callback get_items(nodeIdx(), jid(), undefined | rsm_set()) -> {result, {[pubsubItem()], undefined | rsm_set()}}. -callback get_last_items(nodeIdx(), jid(), undefined | rsm_set()) -> {result, [pubsubItem()]}. -callback get_only_item(nodeIdx(), jid()) -> {result, [pubsubItem()]}. -callback get_item(NodeIdx :: nodeIdx(), ItemId :: itemId(), JID :: jid(), AccessModel :: accessModel(), PresenceSubscription :: boolean(), RosterGroup :: boolean(), SubId :: subId()) -> {result, pubsubItem()} | {error, stanza_error()}. -callback get_item(NodeIdx :: nodeIdx(), ItemId :: itemId()) -> {result, pubsubItem()} | {error, stanza_error()}. -callback set_item(Item :: pubsubItem()) -> ok. % | {error, _}. -callback get_item_name(Host :: host(), ServerHost :: binary(), Node :: nodeId()) -> {result, itemId()}. -callback node_to_path(Node :: nodeId()) -> {result, [nodeId()]}. -callback path_to_node(Node :: [nodeId()]) -> {result, nodeId()}. ejabberd-21.12/src/mod_carboncopy.erl0000644000232200023220000002747314154362354020140 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_carboncopy.erl %%% Author : Eric Cestari %%% Purpose : Message Carbons XEP-0280 0.8 %%% Created : 5 May 2008 by Mickael Remond %%% Usage : Add the following line in modules section of ejabberd.yml: %%% {mod_carboncopy, []} %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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 (mod_carboncopy). -author ('ecestari@process-one.net'). -protocol({xep, 280, '0.13.2'}). -behaviour(gen_mod). %% API: -export([start/2, stop/1, reload/3]). -export([user_send_packet/1, user_receive_packet/1, iq_handler/1, disco_features/5, depends/2, mod_options/1, mod_doc/0]). -export([c2s_copy_session/2, c2s_session_opened/1, c2s_session_resumed/1]). %% For debugging purposes -export([list/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -type direction() :: sent | received. -type c2s_state() :: ejabberd_c2s:state(). start(Host, _Opts) -> ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) ejabberd_hooks:add(user_send_packet,Host, ?MODULE, user_send_packet, 89), ejabberd_hooks:add(user_receive_packet,Host, ?MODULE, user_receive_packet, 89), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:add(c2s_session_opened, Host, ?MODULE, c2s_session_opened, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2, ?MODULE, iq_handler). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CARBONS_2), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), %% why priority 89: to define clearly that we must run BEFORE mod_logdb hook (90) ejabberd_hooks:delete(user_send_packet,Host, ?MODULE, user_send_packet, 89), ejabberd_hooks:delete(user_receive_packet,Host, ?MODULE, user_receive_packet, 89), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:delete(c2s_session_opened, Host, ?MODULE, c2s_session_opened, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. disco_features(empty, From, To, <<"">>, Lang) -> disco_features({result, []}, From, To, <<"">>, Lang); disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> {result, [?NS_CARBONS_2,?NS_CARBONS_RULES_0|Feats]}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec iq_handler(iq()) -> iq(). iq_handler(#iq{type = set, lang = Lang, from = From, sub_els = [El]} = IQ) when is_record(El, carbons_enable); is_record(El, carbons_disable) -> {U, S, R} = jid:tolower(From), Result = case El of #carbons_enable{} -> enable(S, U, R, ?NS_CARBONS_2); #carbons_disable{} -> disable(S, U, R) end, case Result of ok -> xmpp:make_iq_result(IQ); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; iq_handler(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Only or tags are allowed"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); iq_handler(#iq{type = get, lang = Lang} = IQ)-> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)). -spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}. user_send_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) -> %% Stop the hook chain, we don't want logging modules to duplicate this %% message. {stop, Acc}; user_send_packet({#message{from = From, to = To} = Msg, C2SState}) -> {check_and_forward(From, To, Msg, sent), C2SState}; user_send_packet(Acc) -> Acc. -spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()} | {stop, {stanza(), ejabberd_c2s:state()}}. user_receive_packet({#message{meta = #{carbon_copy := true}}, _C2SState} = Acc) -> %% Stop the hook chain, we don't want logging modules to duplicate this %% message. {stop, Acc}; user_receive_packet({#message{to = To} = Msg, #{jid := JID} = C2SState}) -> {check_and_forward(JID, To, Msg, received), C2SState}; user_receive_packet(Acc) -> Acc. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{user := U, server := S, resource := R}) -> case ejabberd_sm:get_user_info(U, S, R) of offline -> State; Info -> case lists:keyfind(carboncopy, 1, Info) of {_, CC} -> State#{carboncopy => CC}; false -> State end end. -spec c2s_session_resumed(c2s_state()) -> c2s_state(). c2s_session_resumed(#{user := U, server := S, resource := R, carboncopy := CC} = State) -> ejabberd_sm:set_user_info(U, S, R, carboncopy, CC), maps:remove(carboncopy, State); c2s_session_resumed(State) -> State. -spec c2s_session_opened(c2s_state()) -> c2s_state(). c2s_session_opened(State) -> maps:remove(carboncopy, State). % Modified from original version: % - registered to the user_send_packet hook, to be called only once even for multicast % - do not support "private" message mode, and do not modify the original packet in any way % - we also replicate "read" notifications -spec check_and_forward(jid(), jid(), message(), direction()) -> message(). check_and_forward(JID, To, Msg, Direction)-> case (is_chat_message(Msg) orelse is_received_muc_invite(Msg, Direction)) andalso not is_received_muc_pm(To, Msg, Direction) andalso not xmpp:has_subtag(Msg, #carbons_private{}) andalso not xmpp:has_subtag(Msg, #hint{type = 'no-copy'}) of true -> send_copies(JID, To, Msg, Direction); false -> ok end, Msg. %%% Internal %% Direction = received | sent -spec send_copies(jid(), jid(), message(), direction()) -> ok. send_copies(JID, To, Msg, Direction)-> {U, S, R} = jid:tolower(JID), PrioRes = ejabberd_sm:get_user_present_resources(U, S), {_, AvailRs} = lists:unzip(PrioRes), {MaxPrio, _MaxRes} = case catch lists:max(PrioRes) of {Prio, Res} -> {Prio, Res}; _ -> {0, undefined} end, %% unavailable resources are handled like bare JIDs IsBareTo = case {Direction, To} of {received, #jid{lresource = <<>>}} -> true; {received, #jid{lresource = LRes}} -> not lists:member(LRes, AvailRs); _ -> false end, %% list of JIDs that should receive a carbon copy of this message (excluding the %% receiver(s) of the original message TargetJIDs = case {IsBareTo, Msg} of {true, #message{meta = #{sm_copy := true}}} -> %% The message was sent to our bare JID, and we currently have %% multiple resources with the same highest priority, so the session %% manager routes the message to each of them. We create carbon %% copies only from one of those resources in order to avoid %% duplicates. []; {true, _} -> OrigTo = fun(Res) -> lists:member({MaxPrio, Res}, PrioRes) end, [ {jid:make({U, S, CCRes}), CC_Version} || {CCRes, CC_Version} <- list(U, S), lists:member(CCRes, AvailRs), not OrigTo(CCRes) ]; {false, _} -> [ {jid:make({U, S, CCRes}), CC_Version} || {CCRes, CC_Version} <- list(U, S), lists:member(CCRes, AvailRs), CCRes /= R ] %TargetJIDs = lists:delete(JID, [ jid:make({U, S, CCRes}) || CCRes <- list(U, S) ]), end, lists:foreach( fun({Dest, _Version}) -> {_, _, Resource} = jid:tolower(Dest), ?DEBUG("Sending: ~p =/= ~p", [R, Resource]), Sender = jid:make({U, S, <<>>}), New = build_forward_packet(Msg, Sender, Dest, Direction), ejabberd_router:route(xmpp:set_from_to(New, Sender, Dest)) end, TargetJIDs). -spec build_forward_packet(message(), jid(), jid(), direction()) -> message(). build_forward_packet(#message{type = T} = Msg, Sender, Dest, Direction) -> Forwarded = #forwarded{sub_els = [Msg]}, Carbon = case Direction of sent -> #carbons_sent{forwarded = Forwarded}; received -> #carbons_received{forwarded = Forwarded} end, #message{from = Sender, to = Dest, type = T, sub_els = [Carbon], meta = #{carbon_copy => true}}. -spec enable(binary(), binary(), binary(), binary()) -> ok | {error, any()}. enable(Host, U, R, CC)-> ?DEBUG("Enabling carbons for ~ts@~ts/~ts", [U, Host, R]), case ejabberd_sm:set_user_info(U, Host, R, carboncopy, CC) of ok -> ok; {error, Reason} = Err -> ?ERROR_MSG("Failed to enable carbons for ~ts@~ts/~ts: ~p", [U, Host, R, Reason]), Err end. -spec disable(binary(), binary(), binary()) -> ok | {error, any()}. disable(Host, U, R)-> ?DEBUG("Disabling carbons for ~ts@~ts/~ts", [U, Host, R]), case ejabberd_sm:del_user_info(U, Host, R, carboncopy) of ok -> ok; {error, notfound} -> ok; {error, Reason} = Err -> ?ERROR_MSG("Failed to disable carbons for ~ts@~ts/~ts: ~p", [U, Host, R, Reason]), Err end. -spec is_chat_message(message()) -> boolean(). is_chat_message(#message{type = chat}) -> true; is_chat_message(#message{type = normal, body = [_|_]}) -> true; is_chat_message(#message{type = Type} = Msg) when Type == chat; Type == normal -> has_chatstate(Msg) orelse xmpp:has_subtag(Msg, #receipt_response{}); is_chat_message(_) -> false. -spec is_received_muc_invite(message(), direction()) -> boolean(). is_received_muc_invite(_Msg, sent) -> false; is_received_muc_invite(Msg, received) -> case xmpp:get_subtag(Msg, #muc_user{}) of #muc_user{invites = [_|_]} -> true; _ -> xmpp:has_subtag(Msg, #x_conference{jid = jid:make(<<"">>)}) end. -spec is_received_muc_pm(jid(), message(), direction()) -> boolean(). is_received_muc_pm(#jid{lresource = <<>>}, _Msg, _Direction) -> false; is_received_muc_pm(_To, _Msg, sent) -> false; is_received_muc_pm(_To, Msg, received) -> xmpp:has_subtag(Msg, #muc_user{}). -spec has_chatstate(message()) -> boolean(). has_chatstate(#message{sub_els = Els}) -> lists:any(fun(El) -> xmpp:get_ns(El) == ?NS_CHATSTATES end, Els). -spec list(binary(), binary()) -> [{Resource :: binary(), Namespace :: binary()}]. list(User, Server) -> lists:filtermap( fun({Resource, Info}) -> case lists:keyfind(carboncopy, 1, Info) of {_, NS} -> {true, {Resource, NS}}; false -> false end end, ejabberd_sm:get_user_info(User, Server)). depends(_Host, _Opts) -> []. mod_options(_) -> []. mod_doc() -> #{desc => ?T("The module implements https://xmpp.org/extensions/xep-0280.html" "[XEP-0280: Message Carbons]. " "The module broadcasts messages on all connected " "user resources (devices).")}. ejabberd-21.12/src/mod_jidprep_opt.erl0000644000232200023220000000051614154362354020305 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_jidprep_opt). -export([access/1]). -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_jidprep, access). ejabberd-21.12/src/mod_mqtt_mnesia.erl0000644000232200023220000002331014154362354020304 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mod_mqtt_mnesia). -behaviour(mod_mqtt). %% API -export([init/2, publish/6, delete_published/2, lookup_published/2]). -export([list_topics/1, use_cache/1]). -export([init/0]). -export([subscribe/4, unsubscribe/2, find_subscriber/2]). -export([open_session/1, close_session/1, lookup_session/1, get_sessions/2]). -include("logger.hrl"). -include("mqtt.hrl"). -record(mqtt_pub, {topic_server :: {binary(), binary()}, user :: binary(), resource :: binary(), qos :: 0..2, payload :: binary(), expiry :: non_neg_integer(), payload_format = binary :: binary | utf8, response_topic = <<>> :: binary(), correlation_data = <<>> :: binary(), content_type = <<>> :: binary(), user_properties = [] :: [{binary(), binary()}]}). -record(mqtt_sub, {topic :: {binary(), binary(), binary(), binary()}, options :: sub_opts(), id :: non_neg_integer(), pid :: pid(), timestamp :: erlang:timestamp()}). -record(mqtt_session, {usr :: jid:ljid() | {'_', '_', '$1'}, pid :: pid() | '_', timestamp :: erlang:timestamp() | '_'}). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> case ejabberd_mnesia:create( ?MODULE, mqtt_pub, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mqtt_pub)}]) of {atomic, _} -> ok; Err -> {error, Err} end. use_cache(Host) -> case mnesia:table_info(mqtt_pub, storage_type) of disc_only_copies -> mod_mqtt_opt:use_cache(Host); _ -> false end. publish({U, LServer, R}, Topic, Payload, QoS, Props, ExpiryTime) -> PayloadFormat = maps:get(payload_format_indicator, Props, binary), ResponseTopic = maps:get(response_topic, Props, <<"">>), CorrelationData = maps:get(correlation_data, Props, <<"">>), ContentType = maps:get(content_type, Props, <<"">>), UserProps = maps:get(user_property, Props, []), mnesia:dirty_write(#mqtt_pub{topic_server = {Topic, LServer}, user = U, resource = R, qos = QoS, payload = Payload, expiry = ExpiryTime, payload_format = PayloadFormat, response_topic = ResponseTopic, correlation_data = CorrelationData, content_type = ContentType, user_properties = UserProps}). delete_published({_, S, _}, Topic) -> mnesia:dirty_delete(mqtt_pub, {Topic, S}). lookup_published({_, S, _}, Topic) -> case mnesia:dirty_read(mqtt_pub, {Topic, S}) of [#mqtt_pub{qos = QoS, payload = Payload, expiry = ExpiryTime, payload_format = PayloadFormat, response_topic = ResponseTopic, correlation_data = CorrelationData, content_type = ContentType, user_properties = UserProps}] -> Props = #{payload_format => PayloadFormat, response_topic => ResponseTopic, correlation_data => CorrelationData, content_type => ContentType, user_property => UserProps}, {ok, {Payload, QoS, Props, ExpiryTime}}; [] -> {error, notfound} end. list_topics(S) -> {ok, [Topic || {Topic, S1} <- mnesia:dirty_all_keys(mqtt_pub), S1 == S]}. init() -> case mqtree:whereis(mqtt_sub_index) of undefined -> T = mqtree:new(), mqtree:register(mqtt_sub_index, T); _ -> ok end, try {atomic, ok} = ejabberd_mnesia:create( ?MODULE, mqtt_session, [{ram_copies, [node()]}, {attributes, record_info(fields, mqtt_session)}]), {atomic, ok} = ejabberd_mnesia:create( ?MODULE, mqtt_sub, [{ram_copies, [node()]}, {type, ordered_set}, {attributes, record_info(fields, mqtt_sub)}]), ok catch _:{badmatch, Err} -> {error, Err} end. open_session(USR) -> TS1 = misc:unique_timestamp(), P1 = self(), F = fun() -> case mnesia:read(mqtt_session, USR) of [#mqtt_session{pid = P2, timestamp = TS2}] -> if TS1 >= TS2 -> mod_mqtt_session:route(P2, {replaced, P1}), mnesia:write( #mqtt_session{usr = USR, pid = P1, timestamp = TS1}); true -> case is_process_dead(P2) of true -> mnesia:write( #mqtt_session{usr = USR, pid = P1, timestamp = TS1}); false -> mod_mqtt_session:route(P1, {replaced, P2}) end end; [] -> mnesia:write( #mqtt_session{usr = USR, pid = P1, timestamp = TS1}) end end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> db_fail("Failed to register MQTT session for ~ts", Reason, [jid:encode(USR)]) end. close_session(USR) -> close_session(USR, self()). lookup_session(USR) -> case mnesia:dirty_read(mqtt_session, USR) of [#mqtt_session{pid = Pid}] -> case is_process_dead(Pid) of true -> %% Read-Repair close_session(USR, Pid), {error, notfound}; false -> {ok, Pid} end; [] -> {error, notfound} end. get_sessions(U, S) -> Resources = mnesia:dirty_select(mqtt_session, [{#mqtt_session{usr = {U, S, '$1'}, _ = '_'}, [], ['$1']}]), [{U, S, Resource} || Resource <- Resources]. subscribe({U, S, R} = USR, TopicFilter, SubOpts, ID) -> T1 = misc:unique_timestamp(), P1 = self(), Key = {TopicFilter, S, U, R}, F = fun() -> case mnesia:read(mqtt_sub, Key) of [#mqtt_sub{timestamp = T2}] when T1 < T2 -> ok; _ -> Tree = mqtree:whereis(mqtt_sub_index), mqtree:insert(Tree, TopicFilter), mnesia:write( #mqtt_sub{topic = {TopicFilter, S, U, R}, options = SubOpts, id = ID, pid = P1, timestamp = T1}) end end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> db_fail("Failed to subscribe ~ts to ~ts", Reason, [jid:encode(USR), TopicFilter]) end. unsubscribe({U, S, R} = USR, Topic) -> Pid = self(), F = fun() -> Tree = mqtree:whereis(mqtt_sub_index), mqtree:delete(Tree, Topic), case mnesia:read(mqtt_sub, {Topic, S, U, R}) of [#mqtt_sub{pid = Pid} = Obj] -> mnesia:delete_object(Obj); _ -> ok end end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> db_fail("Failed to unsubscribe ~ts from ~ts", Reason, [jid:encode(USR), Topic]) end. find_subscriber(S, Topic) when is_binary(Topic) -> Tree = mqtree:whereis(mqtt_sub_index), case mqtree:match(Tree, Topic) of [Filter|Filters] -> find_subscriber(S, {Filters, {Filter, S, '_', '_'}}); [] -> {error, notfound} end; find_subscriber(S, {Filters, {Filter, S, _, _} = Prev}) -> case mnesia:dirty_next(mqtt_sub, Prev) of {Filter, S, _, _} = Next -> case mnesia:dirty_read(mqtt_sub, Next) of [#mqtt_sub{options = SubOpts, id = ID, pid = Pid}] -> case is_process_dead(Pid) of true -> find_subscriber(S, {Filters, Next}); false -> {ok, {Pid, SubOpts, ID}, {Filters, Next}} end; [] -> find_subscriber(S, {Filters, Next}) end; _ -> case Filters of [] -> {error, notfound}; [Filter1|Filters1] -> find_subscriber(S, {Filters1, {Filter1, S, '_', '_'}}) end end. %%%=================================================================== %%% Internal functions %%%=================================================================== close_session(USR, Pid) -> F = fun() -> case mnesia:read(mqtt_session, USR) of [#mqtt_session{pid = Pid} = Obj] -> mnesia:delete_object(Obj); _ -> ok end end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> db_fail("Failed to unregister MQTT session for ~ts", Reason, [jid:encode(USR)]) end. is_process_dead(Pid) -> node(Pid) == node() andalso not is_process_alive(Pid). db_fail(Format, Reason, Args) -> ?ERROR_MSG(Format ++ ": ~p", Args ++ [Reason]), {error, db_failure}. ejabberd-21.12/src/mod_configure.erl0000644000232200023220000015466514154362354017766 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_configure.erl %%% Author : Alexey Shchepin %%% Purpose : Support for online configuration of ejabberd %%% Created : 19 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_configure). -author('alexey@process-one.net'). -protocol({xep, 133, '1.1'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, get_local_identity/5, get_local_features/5, get_local_items/5, adhoc_local_items/4, adhoc_local_commands/4, get_sm_identity/5, get_sm_features/5, get_sm_items/5, adhoc_sm_items/4, adhoc_sm_commands/4, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_sm.hrl"). -include("translate.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). start(Host, _Opts) -> ejabberd_hooks:add(disco_local_items, Host, ?MODULE, get_local_items, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, get_local_features, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, get_local_identity, 50), ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:add(adhoc_local_items, Host, ?MODULE, adhoc_local_items, 50), ejabberd_hooks:add(adhoc_local_commands, Host, ?MODULE, adhoc_local_commands, 50), ejabberd_hooks:add(adhoc_sm_items, Host, ?MODULE, adhoc_sm_items, 50), ejabberd_hooks:add(adhoc_sm_commands, Host, ?MODULE, adhoc_sm_commands, 50), ok. stop(Host) -> ejabberd_hooks:delete(adhoc_sm_commands, Host, ?MODULE, adhoc_sm_commands, 50), ejabberd_hooks:delete(adhoc_sm_items, Host, ?MODULE, adhoc_sm_items, 50), ejabberd_hooks:delete(adhoc_local_commands, Host, ?MODULE, adhoc_local_commands, 50), ejabberd_hooks:delete(adhoc_local_items, Host, ?MODULE, adhoc_local_items, 50), ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, get_sm_identity, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, get_sm_items, 50), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, get_local_identity, 50), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, get_local_features, 50), ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, get_local_items, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_adhoc, hard}, {mod_last, soft}]. %%%----------------------------------------------------------------------- -define(INFO_IDENTITY(Category, Type, Name, Lang), [#identity{category = Category, type = Type, name = tr(Lang, Name)}]). -define(INFO_COMMAND(Name, Lang), ?INFO_IDENTITY(<<"automation">>, <<"command-node">>, Name, Lang)). -define(NODEJID(To, Name, Node), #disco_item{jid = To, name = tr(Lang, Name), node = Node}). -define(NODE(Name, Node), #disco_item{jid = jid:make(Server), node = Node, name = tr(Lang, Name)}). -define(NS_ADMINX(Sub), <<(?NS_ADMIN)/binary, "#", Sub/binary>>). -define(NS_ADMINL(Sub), [<<"http:">>, <<"jabber.org">>, <<"protocol">>, <<"admin">>, Sub]). -spec tokenize(binary()) -> [binary()]. tokenize(Node) -> str:tokens(Node, <<"/#">>). -spec get_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_sm_identity(Acc, _From, _To, Node, Lang) -> case Node of <<"config">> -> ?INFO_COMMAND(?T("Configuration"), Lang); _ -> Acc end. -spec get_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. get_local_identity(Acc, _From, _To, Node, Lang) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, ENode] -> ?INFO_IDENTITY(<<"ejabberd">>, <<"node">>, ENode, Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> ?INFO_COMMAND(?T("Database"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"backup">>] -> ?INFO_COMMAND(?T("Backup"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"restore">>] -> ?INFO_COMMAND(?T("Restore"), Lang); [<<"running nodes">>, _ENode, <<"backup">>, <<"textfile">>] -> ?INFO_COMMAND(?T("Dump to Text File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"file">>] -> ?INFO_COMMAND(?T("Import File"), Lang); [<<"running nodes">>, _ENode, <<"import">>, <<"dir">>] -> ?INFO_COMMAND(?T("Import Directory"), Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?INFO_COMMAND(?T("Restart Service"), Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?INFO_COMMAND(?T("Shut Down Service"), Lang); ?NS_ADMINL(<<"add-user">>) -> ?INFO_COMMAND(?T("Add User"), Lang); ?NS_ADMINL(<<"delete-user">>) -> ?INFO_COMMAND(?T("Delete User"), Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_COMMAND(?T("End User Session"), Lang); ?NS_ADMINL(<<"get-user-password">>) -> ?INFO_COMMAND(?T("Get User Password"), Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_COMMAND(?T("Change User Password"), Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?INFO_COMMAND(?T("Get User Last Login Time"), Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_COMMAND(?T("Get User Statistics"), Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Registered Users"), Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_COMMAND(?T("Get Number of Online Users"), Lang); _ -> Acc end. %%%----------------------------------------------------------------------- -define(INFO_RESULT(Allow, Feats, Lang), case Allow of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> {result, Feats} end). -spec get_sm_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_sm_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Allow = acl:match_rule(LServer, configure, From), case Node of <<"config">> -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. -spec get_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). get_local_features(Acc, From, #jid{lserver = LServer} = _To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), case LNode of [<<"config">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"user">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"online users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"all users">>, <<$@, _/binary>>] -> ?INFO_RESULT(Allow, [], Lang); [<<"outgoing s2s">> | _] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"stopped nodes">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode] -> ?INFO_RESULT(Allow, [?NS_STATS], Lang); [<<"running nodes">>, _ENode, <<"DB">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"backup">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"import">>] -> ?INFO_RESULT(Allow, [], Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); [<<"config">>, _] -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"add-user">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"delete-user">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"end-user-session">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-password">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"change-user-password">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"user-stats">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); ?NS_ADMINL(<<"get-online-users-num">>) -> ?INFO_RESULT(Allow, [?NS_COMMANDS], Lang); _ -> Acc end end. %%%----------------------------------------------------------------------- -spec adhoc_sm_items(mod_disco:items_acc(), jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_sm_items(Acc, From, #jid{lserver = LServer} = To, Lang) -> case acl:match_rule(LServer, configure, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Nodes = [#disco_item{jid = To, node = <<"config">>, name = tr(Lang, ?T("Configuration"))}], {result, Items ++ Nodes}; _ -> Acc end. %%%----------------------------------------------------------------------- -spec get_sm_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_sm_items(Acc, From, #jid{user = User, server = Server, lserver = LServer} = To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Items = case Acc of {result, Its} -> Its; empty -> [] end, case {acl:match_rule(LServer, configure, From), Node} of {allow, <<"">>} -> Nodes = [?NODEJID(To, ?T("Configuration"), <<"config">>), ?NODEJID(To, ?T("User Management"), <<"user">>)], {result, Items ++ Nodes ++ get_user_resources(User, Server)}; {allow, <<"config">>} -> {result, []}; {_, <<"config">>} -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; _ -> Acc end end. -spec get_user_resources(binary(), binary()) -> [disco_item()]. get_user_resources(User, Server) -> Rs = ejabberd_sm:get_user_resources(User, Server), lists:map(fun (R) -> #disco_item{jid = jid:make(User, Server, R), name = User} end, lists:sort(Rs)). %%%----------------------------------------------------------------------- -spec adhoc_local_items(mod_disco:items_acc(), jid(), jid(), binary()) -> mod_disco:items_acc(). adhoc_local_items(Acc, From, #jid{lserver = LServer, server = Server} = To, Lang) -> case acl:match_rule(LServer, configure, From) of allow -> Items = case Acc of {result, Its} -> Its; empty -> [] end, PermLev = get_permission_level(From), Nodes = recursively_get_local_items(PermLev, LServer, <<"">>, Server, Lang), Nodes1 = lists:filter( fun (#disco_item{node = Nd}) -> F = get_local_features(empty, From, To, Nd, Lang), case F of {result, [?NS_COMMANDS]} -> true; _ -> false end end, Nodes), {result, Items ++ Nodes1}; _ -> Acc end. -spec recursively_get_local_items(global | vhost, binary(), binary(), binary(), binary()) -> [disco_item()]. recursively_get_local_items(_PermLev, _LServer, <<"online users">>, _Server, _Lang) -> []; recursively_get_local_items(_PermLev, _LServer, <<"all users">>, _Server, _Lang) -> []; recursively_get_local_items(PermLev, LServer, Node, Server, Lang) -> LNode = tokenize(Node), Items = case get_local_items({PermLev, LServer}, LNode, Server, Lang) of {result, Res} -> Res; {error, _Error} -> [] end, lists:flatten( lists:map( fun(#disco_item{jid = #jid{server = S}, node = Nd} = Item) -> if (S /= Server) or (Nd == <<"">>) -> []; true -> [Item, recursively_get_local_items( PermLev, LServer, Nd, Server, Lang)] end end, Items)). -spec get_permission_level(jid()) -> global | vhost. get_permission_level(JID) -> case acl:match_rule(global, configure, JID) of allow -> global; deny -> vhost end. %%%----------------------------------------------------------------------- -define(ITEMS_RESULT(Allow, LNode, Fallback), case Allow of deny -> Fallback; allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, LNode, jid:encode(To), Lang) of {result, Res} -> {result, Res}; {error, Error} -> {error, Error} end end). -spec get_local_items(mod_disco:items_acc(), jid(), jid(), binary(), binary()) -> mod_disco:items_acc(). get_local_items(Acc, From, #jid{lserver = LServer} = To, <<"">>, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> Items = case Acc of {result, Its} -> Its; empty -> [] end, Allow = acl:match_rule(LServer, configure, From), case Allow of deny -> {result, Items}; allow -> PermLev = get_permission_level(From), case get_local_items({PermLev, LServer}, [], jid:encode(To), Lang) of {result, Res} -> {result, Items ++ Res}; {error, _Error} -> {result, Items} end end end; get_local_items(Acc, From, #jid{lserver = LServer} = To, Node, Lang) -> case gen_mod:is_loaded(LServer, mod_adhoc) of false -> Acc; _ -> LNode = tokenize(Node), Allow = acl:match_rule(LServer, configure, From), Err = xmpp:err_forbidden(?T("Access denied by service policy"), Lang), case LNode of [<<"config">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"user">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"online users">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"all users">>, <<$@, _/binary>>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"outgoing s2s">> | _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"stopped nodes">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"DB">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"restart">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); [<<"config">>, _] -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"add-user">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"delete-user">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"end-user-session">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-password">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"change-user-password">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-user-lastlogin">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"user-stats">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-registered-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); ?NS_ADMINL(<<"get-online-users-num">>) -> ?ITEMS_RESULT(Allow, LNode, {error, Err}); _ -> Acc end end. %%%----------------------------------------------------------------------- -spec get_local_items({global | vhost, binary()}, [binary()], binary(), binary()) -> {result, [disco_item()]} | {error, stanza_error()}. get_local_items(_Host, [], Server, Lang) -> {result, [?NODE(?T("Configuration"), <<"config">>), ?NODE(?T("User Management"), <<"user">>), ?NODE(?T("Online Users"), <<"online users">>), ?NODE(?T("All Users"), <<"all users">>), ?NODE(?T("Outgoing s2s Connections"), <<"outgoing s2s">>), ?NODE(?T("Running Nodes"), <<"running nodes">>), ?NODE(?T("Stopped Nodes"), <<"stopped nodes">>)]}; get_local_items(_Host, [<<"config">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"user">>], Server, Lang) -> {result, [?NODE(?T("Add User"), (?NS_ADMINX(<<"add-user">>))), ?NODE(?T("Delete User"), (?NS_ADMINX(<<"delete-user">>))), ?NODE(?T("End User Session"), (?NS_ADMINX(<<"end-user-session">>))), ?NODE(?T("Get User Password"), (?NS_ADMINX(<<"get-user-password">>))), ?NODE(?T("Change User Password"), (?NS_ADMINX(<<"change-user-password">>))), ?NODE(?T("Get User Last Login Time"), (?NS_ADMINX(<<"get-user-lastlogin">>))), ?NODE(?T("Get User Statistics"), (?NS_ADMINX(<<"user-stats">>))), ?NODE(?T("Get Number of Registered Users"), (?NS_ADMINX(<<"get-registered-users-num">>))), ?NODE(?T("Get Number of Online Users"), (?NS_ADMINX(<<"get-online-users-num">>)))]}; get_local_items(_Host, [<<"http:">> | _], _Server, _Lang) -> {result, []}; get_local_items({_, Host}, [<<"online users">>], _Server, _Lang) -> {result, get_online_vh_users(Host)}; get_local_items({_, Host}, [<<"all users">>], _Server, _Lang) -> {result, get_all_vh_users(Host)}; get_local_items({_, Host}, [<<"all users">>, <<$@, Diap/binary>>], _Server, _Lang) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), try [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), {result, lists:map( fun({S, U}) -> #disco_item{jid = jid:make(U, S), name = <>} end, Sub)} catch _:_ -> {error, xmpp:err_not_acceptable()} end; get_local_items({_, Host}, [<<"outgoing s2s">>], _Server, Lang) -> {result, get_outgoing_s2s(Host, Lang)}; get_local_items({_, Host}, [<<"outgoing s2s">>, To], _Server, Lang) -> {result, get_outgoing_s2s(Host, Lang, To)}; get_local_items(_Host, [<<"running nodes">>], Server, Lang) -> {result, get_running_nodes(Server, Lang)}; get_local_items(_Host, [<<"stopped nodes">>], _Server, Lang) -> {result, get_stopped_nodes(Lang)}; get_local_items({global, _Host}, [<<"running nodes">>, ENode], Server, Lang) -> {result, [?NODE(?T("Database"), <<"running nodes/", ENode/binary, "/DB">>), ?NODE(?T("Backup Management"), <<"running nodes/", ENode/binary, "/backup">>), ?NODE(?T("Import Users From jabberd14 Spool Files"), <<"running nodes/", ENode/binary, "/import">>), ?NODE(?T("Restart Service"), <<"running nodes/", ENode/binary, "/restart">>), ?NODE(?T("Shut Down Service"), <<"running nodes/", ENode/binary, "/shutdown">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"DB">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, ENode, <<"backup">>], Server, Lang) -> {result, [?NODE(?T("Backup"), <<"running nodes/", ENode/binary, "/backup/backup">>), ?NODE(?T("Restore"), <<"running nodes/", ENode/binary, "/backup/restore">>), ?NODE(?T("Dump to Text File"), <<"running nodes/", ENode/binary, "/backup/textfile">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"backup">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, ENode, <<"import">>], Server, Lang) -> {result, [?NODE(?T("Import File"), <<"running nodes/", ENode/binary, "/import/file">>), ?NODE(?T("Import Directory"), <<"running nodes/", ENode/binary, "/import/dir">>)]}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"import">>, _], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"restart">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, [<<"running nodes">>, _ENode, <<"shutdown">>], _Server, _Lang) -> {result, []}; get_local_items(_Host, _, _Server, _Lang) -> {error, xmpp:err_item_not_found()}. -spec get_online_vh_users(binary()) -> [disco_item()]. get_online_vh_users(Host) -> USRs = ejabberd_sm:get_vh_session_list(Host), SURs = lists:sort([{S, U, R} || {U, S, R} <- USRs]), lists:map( fun({S, U, R}) -> #disco_item{jid = jid:make(U, S, R), name = <>} end, SURs). -spec get_all_vh_users(binary()) -> [disco_item()]. get_all_vh_users(Host) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), case length(SUsers) of N when N =< 100 -> lists:map(fun({S, U}) -> #disco_item{jid = jid:make(U, S), name = <>} end, SUsers); N -> NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, M = trunc(N / NParts) + 1, lists:map( fun (K) -> L = K + M - 1, Node = <<"@", (integer_to_binary(K))/binary, "-", (integer_to_binary(L))/binary>>, {FS, FU} = lists:nth(K, SUsers), {LS, LU} = if L < N -> lists:nth(L, SUsers); true -> lists:last(SUsers) end, Name = <>, #disco_item{jid = jid:make(Host), node = <<"all users/", Node/binary>>, name = Name} end, lists:seq(1, N, M)) end. -spec get_outgoing_s2s(binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang) -> Connections = ejabberd_s2s:dirty_get_connections(), DotHost = <<".", Host/binary>>, TConns = [TH || {FH, TH} <- Connections, Host == FH orelse str:suffix(DotHost, FH)], lists:map( fun (T) -> Name = str:translate_and_format(Lang, ?T("To ~ts"),[T]), #disco_item{jid = jid:make(Host), node = <<"outgoing s2s/", T/binary>>, name = Name} end, lists:usort(TConns)). -spec get_outgoing_s2s(binary(), binary(), binary()) -> [disco_item()]. get_outgoing_s2s(Host, Lang, To) -> Connections = ejabberd_s2s:dirty_get_connections(), lists:map( fun ({F, _T}) -> Node = <<"outgoing s2s/", To/binary, "/", F/binary>>, Name = str:translate_and_format(Lang, ?T("From ~ts"), [F]), #disco_item{jid = jid:make(Host), node = Node, name = Name} end, lists:keysort( 1, lists:filter(fun (E) -> element(2, E) == To end, Connections))). -spec get_running_nodes(binary(), binary()) -> [disco_item()]. get_running_nodes(Server, _Lang) -> DBNodes = mnesia:system_info(running_db_nodes), lists:map( fun (N) -> S = iolist_to_binary(atom_to_list(N)), #disco_item{jid = jid:make(Server), node = <<"running nodes/", S/binary>>, name = S} end, lists:sort(DBNodes)). -spec get_stopped_nodes(binary()) -> [disco_item()]. get_stopped_nodes(_Lang) -> DBNodes = lists:usort(mnesia:system_info(db_nodes) ++ mnesia:system_info(extra_db_nodes)) -- mnesia:system_info(running_db_nodes), lists:map( fun (N) -> S = iolist_to_binary(atom_to_list(N)), #disco_item{jid = jid:make(ejabberd_config:get_myname()), node = <<"stopped nodes/", S/binary>>, name = S} end, lists:sort(DBNodes)). %%------------------------------------------------------------------------- -define(COMMANDS_RESULT(LServerOrGlobal, From, To, Request, Lang), case acl:match_rule(LServerOrGlobal, configure, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> adhoc_local_commands(From, To, Request) end). -spec adhoc_local_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. adhoc_local_commands(Acc, From, #jid{lserver = LServer} = To, #adhoc_command{node = Node, lang = Lang} = Request) -> LNode = tokenize(Node), case LNode of [<<"running nodes">>, _ENode, <<"DB">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"backup">>, _] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"import">>, _] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"restart">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"running nodes">>, _ENode, <<"shutdown">>] -> ?COMMANDS_RESULT(global, From, To, Request, Lang); [<<"config">>, _] -> ?COMMANDS_RESULT(LServer, From, To, Request, Lang); ?NS_ADMINL(_) -> ?COMMANDS_RESULT(LServer, From, To, Request, Lang); _ -> Acc end. -spec adhoc_local_commands(jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. adhoc_local_commands(From, #jid{lserver = LServer} = _To, #adhoc_command{lang = Lang, node = Node, sid = SessionID, action = Action, xdata = XData} = Request) -> LNode = tokenize(Node), ActionIsExecute = Action == execute orelse Action == complete, if Action == cancel -> #adhoc_command{status = canceled, lang = Lang, node = Node, sid = SessionID}; XData == undefined, ActionIsExecute -> case get_form(LServer, LNode, Lang) of {result, Form} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = executing, xdata = Form}); {result, Status, Form} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = Status, xdata = Form}); {error, Error} -> {error, Error} end; XData /= undefined, ActionIsExecute -> case set_form(From, LServer, LNode, Lang, XData) of {result, Res} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{xdata = Res, status = completed}); %%{'EXIT', _} -> {error, xmpp:err_bad_request()}; {error, Error} -> {error, Error} end; true -> {error, xmpp:err_bad_request(?T("Unexpected action"), Lang)} end. -define(TVFIELD(Type, Var, Val), #xdata_field{type = Type, var = Var, values = [Val]}). -define(HFIELD(), ?TVFIELD(hidden, <<"FORM_TYPE">>, (?NS_ADMIN))). -define(TLFIELD(Type, Label, Var), #xdata_field{type = Type, label = tr(Lang, Label), var = Var}). -define(XFIELD(Type, Label, Var, Val), #xdata_field{type = Type, label = tr(Lang, Label), var = Var, values = [Val]}). -define(XMFIELD(Type, Label, Var, Vals), #xdata_field{type = Type, label = tr(Lang, Label), var = Var, values = Vals}). -define(TABLEFIELD(Table, Val), #xdata_field{ type = 'list-single', label = iolist_to_binary(atom_to_list(Table)), var = iolist_to_binary(atom_to_list(Table)), values = [iolist_to_binary(atom_to_list(Val))], options = [#xdata_option{label = tr(Lang, ?T("RAM copy")), value = <<"ram_copies">>}, #xdata_option{label = tr(Lang, ?T("RAM and disc copy")), value = <<"disc_copies">>}, #xdata_option{label = tr(Lang, ?T("Disc only copy")), value = <<"disc_only_copies">>}, #xdata_option{label = tr(Lang, ?T("Remote copy")), value = <<"unknown">>}]}). -spec get_form(binary(), [binary()], binary()) -> {result, xdata()} | {result, completed, xdata()} | {error, stanza_error()}. get_form(_Host, [<<"running nodes">>, ENode, <<"DB">>], Lang) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, Reason} -> ?ERROR_MSG("RPC call mnesia:system_info(tables) on node " "~ts failed: ~p", [Node, Reason]), {error, xmpp:err_internal_server_error()}; Tables -> STables = lists:sort(Tables), Title = <<(tr(Lang, ?T("Database Tables Configuration at ")))/binary, ENode/binary>>, Instr = tr(Lang, ?T("Choose storage type of tables")), try Fs = lists:map( fun(Table) -> case ejabberd_cluster:call( Node, mnesia, table_info, [Table, storage_type]) of Type when is_atom(Type) -> ?TABLEFIELD(Table, Type) end end, STables), {result, #xdata{title = Title, type = form, instructions = [Instr], fields = [?HFIELD()|Fs]}} catch _:{case_clause, {badrpc, Reason}} -> ?ERROR_MSG("RPC call mnesia:table_info/2 " "on node ~ts failed: ~p", [Node, Reason]), {error, xmpp:err_internal_server_error()} end end end; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Backup to File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to backup file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Restore Backup from File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to backup file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Dump Backup to Text File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to text file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Import User from File at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to jabberd14 spool file"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to File"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], Lang) -> {result, #xdata{title = <<(tr(Lang, ?T("Import Users from Dir at ")))/binary, ENode/binary>>, type = form, instructions = [tr(Lang, ?T("Enter path to jabberd14 spool dir"))], fields = [?HFIELD(), ?XFIELD('text-single', ?T("Path to Dir"), <<"path">>, <<"">>)]}}; get_form(_Host, [<<"running nodes">>, _ENode, <<"restart">>], Lang) -> Make_option = fun (LabelNum, LabelUnit, Value) -> #xdata_option{ label = <>, value = Value} end, {result, #xdata{title = tr(Lang, ?T("Restart Service")), type = form, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Time delay")), var = <<"delay">>, required = true, options = [Make_option(<<"">>, <<"immediately">>, <<"1">>), Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, #xdata_field{type = fixed, label = tr(Lang, ?T("Send announcement to all online users " "on all hosts"))}, #xdata_field{var = <<"subject">>, type = 'text-single', label = tr(Lang, ?T("Subject"))}, #xdata_field{var = <<"announcement">>, type = 'text-multi', label = tr(Lang, ?T("Message body"))}]}}; get_form(_Host, [<<"running nodes">>, _ENode, <<"shutdown">>], Lang) -> Make_option = fun (LabelNum, LabelUnit, Value) -> #xdata_option{ label = <>, value = Value} end, {result, #xdata{title = tr(Lang, ?T("Shut Down Service")), type = form, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Time delay")), var = <<"delay">>, required = true, options = [Make_option(<<"">>, <<"immediately">>, <<"1">>), Make_option(<<"15 ">>, <<"seconds">>, <<"15">>), Make_option(<<"30 ">>, <<"seconds">>, <<"30">>), Make_option(<<"60 ">>, <<"seconds">>, <<"60">>), Make_option(<<"90 ">>, <<"seconds">>, <<"90">>), Make_option(<<"2 ">>, <<"minutes">>, <<"120">>), Make_option(<<"3 ">>, <<"minutes">>, <<"180">>), Make_option(<<"4 ">>, <<"minutes">>, <<"240">>), Make_option(<<"5 ">>, <<"minutes">>, <<"300">>), Make_option(<<"10 ">>, <<"minutes">>, <<"600">>), Make_option(<<"15 ">>, <<"minutes">>, <<"900">>), Make_option(<<"30 ">>, <<"minutes">>, <<"1800">>)]}, #xdata_field{type = fixed, label = tr(Lang, ?T("Send announcement to all online users " "on all hosts"))}, #xdata_field{var = <<"subject">>, type = 'text-single', label = tr(Lang, ?T("Subject"))}, #xdata_field{var = <<"announcement">>, type = 'text-multi', label = tr(Lang, ?T("Message body"))}]}}; get_form(_Host, ?NS_ADMINL(<<"add-user">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Add User")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password")), required = true, var = <<"password">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password Verification")), required = true, var = <<"password-verify">>}]}}; get_form(_Host, ?NS_ADMINL(<<"delete-user">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Delete User")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-multi', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjids">>}]}}; get_form(_Host, ?NS_ADMINL(<<"end-user-session">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("End User Session")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}]}}; get_form(_Host, ?NS_ADMINL(<<"get-user-password">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Password")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(_Host, ?NS_ADMINL(<<"change-user-password">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Change User Password")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), required = true, var = <<"accountjid">>}, #xdata_field{type = 'text-private', label = tr(Lang, ?T("Password")), required = true, var = <<"password">>}]}}; get_form(_Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Last Login Time")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(_Host, ?NS_ADMINL(<<"user-stats">>), Lang) -> {result, #xdata{title = tr(Lang, ?T("Get User Statistics")), type = form, fields = [?HFIELD(), #xdata_field{type = 'jid-single', label = tr(Lang, ?T("Jabber ID")), var = <<"accountjid">>, required = true}]}}; get_form(Host, ?NS_ADMINL(<<"get-registered-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_auth:count_users(Host)), {result, completed, #xdata{type = form, fields = [?HFIELD(), #xdata_field{type = 'text-single', label = tr(Lang, ?T("Number of registered users")), var = <<"registeredusersnum">>, values = [Num]}]}}; get_form(Host, ?NS_ADMINL(<<"get-online-users-num">>), Lang) -> Num = integer_to_binary(ejabberd_sm:get_vh_session_number(Host)), {result, completed, #xdata{type = form, fields = [?HFIELD(), #xdata_field{type = 'text-single', label = tr(Lang, ?T("Number of online users")), var = <<"onlineusersnum">>, values = [Num]}]}}; get_form(_Host, _, _Lang) -> {error, xmpp:err_service_unavailable()}. -spec set_form(jid(), binary(), [binary()], binary(), xdata()) -> {result, xdata() | undefined} | {error, stanza_error()}. set_form(_From, _Host, [<<"running nodes">>, ENode, <<"DB">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> lists:foreach( fun(#xdata_field{var = SVar, values = SVals}) -> Table = misc:binary_to_atom(SVar), Type = case SVals of [<<"unknown">>] -> unknown; [<<"ram_copies">>] -> ram_copies; [<<"disc_copies">>] -> disc_copies; [<<"disc_only_copies">>] -> disc_only_copies; _ -> false end, if Type == false -> ok; Type == unknown -> mnesia:del_table_copy(Table, Node); true -> case mnesia:add_table_copy(Table, Node, Type) of {aborted, _} -> mnesia:change_table_copy_type( Table, Node, Type); _ -> ok end end end, XData#xdata.fields), {result, undefined} end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"backup">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, mnesia, backup, [binary_to_list(String)], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " "failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call mnesia:backup(~ts) to node ~ts " "failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"restore">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, ejabberd_admin, restore, [String], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " "~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:restore(~ts) to node " "~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"backup">>, <<"textfile">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> case ejabberd_cluster:call( Node, ejabberd_admin, dump_to_textfile, [String], timer:minutes(10)) of {badrpc, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " "to node ~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; {error, Reason} -> ?ERROR_MSG("RPC call ejabberd_admin:dump_to_textfile(~ts) " "to node ~ts failed: ~p", [String, Node, Reason]), {error, xmpp:err_internal_server_error()}; _ -> {result, undefined} end; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"file">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> ejabberd_cluster:call(Node, jd2ejd, import_file, [String]), {result, undefined}; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(_From, _Host, [<<"running nodes">>, ENode, <<"import">>, <<"dir">>], Lang, XData) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> case xmpp_util:get_xdata_values(<<"path">>, XData) of [] -> Txt = ?T("No 'path' found in data form"), {error, xmpp:err_bad_request(Txt, Lang)}; [String] -> ejabberd_cluster:call(Node, jd2ejd, import_dir, [String]), {result, undefined}; _ -> Txt = ?T("Incorrect value of 'path' in data form"), {error, xmpp:err_bad_request(Txt, Lang)} end end; set_form(From, Host, [<<"running nodes">>, ENode, <<"restart">>], _Lang, XData) -> stop_node(From, Host, ENode, restart, XData); set_form(From, Host, [<<"running nodes">>, ENode, <<"shutdown">>], _Lang, XData) -> stop_node(From, Host, ENode, stop, XData); set_form(From, Host, ?NS_ADMINL(<<"add-user">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), Password = get_value(<<"password-verify">>, XData), AccountJID = jid:decode(AccountString), User = AccountJID#jid.luser, Server = AccountJID#jid.lserver, true = lists:member(Server, ejabberd_option:hosts()), true = Server == Host orelse get_permission_level(From) == global, case ejabberd_auth:try_register(User, Server, Password) of ok -> {result, undefined}; {error, exists} -> {error, xmpp:err_conflict()}; {error, not_allowed} -> {error, xmpp:err_not_allowed()} end; set_form(From, Host, ?NS_ADMINL(<<"delete-user">>), _Lang, XData) -> AccountStringList = get_values(<<"accountjids">>, XData), [_ | _] = AccountStringList, ASL2 = lists:map(fun (AccountString) -> JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, true = ejabberd_auth:user_exists(User, Server), {User, Server} end, AccountStringList), [ejabberd_auth:remove_user(User, Server) || {User, Server} <- ASL2], {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"end-user-session">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), LServer = JID#jid.lserver, true = LServer == Host orelse get_permission_level(From) == global, case JID#jid.lresource of <<>> -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver); R -> ejabberd_sm:kick_user(JID#jid.luser, JID#jid.lserver, R) end, {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"get-user-password">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, Password = ejabberd_auth:get_password(User, Server), true = is_binary(Password), {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Password"), <<"password">>, Password)]}}; set_form(From, Host, ?NS_ADMINL(<<"change-user-password">>), _Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), Password = get_value(<<"password">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, true = ejabberd_auth:user_exists(User, Server), ejabberd_auth:set_password(User, Server, Password), {result, undefined}; set_form(From, Host, ?NS_ADMINL(<<"get-user-lastlogin">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, FLast = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> tr(Lang, ?T("Never")); {ok, Timestamp, _Status} -> Shift = Timestamp, TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> tr(Lang, ?T("Online")) end, {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Last login"), <<"lastlogin">>, FLast)]}}; set_form(From, Host, ?NS_ADMINL(<<"user-stats">>), Lang, XData) -> AccountString = get_value(<<"accountjid">>, XData), JID = jid:decode(AccountString), User = JID#jid.luser, Server = JID#jid.lserver, true = Server == Host orelse get_permission_level(From) == global, Resources = ejabberd_sm:get_user_resources(User, Server), IPs1 = [ejabberd_sm:get_user_ip(User, Server, Resource) || Resource <- Resources], IPs = [<<(misc:ip_to_list(IP))/binary, ":", (integer_to_binary(Port))/binary>> || {IP, Port} <- IPs1], Items = ejabberd_hooks:run_fold(roster_get, Server, [], [{User, Server}]), Rostersize = integer_to_binary(erlang:length(Items)), {result, #xdata{type = form, fields = [?HFIELD(), ?XFIELD('jid-single', ?T("Jabber ID"), <<"accountjid">>, AccountString), ?XFIELD('text-single', ?T("Roster size"), <<"rostersize">>, Rostersize), ?XMFIELD('text-multi', ?T("IP addresses"), <<"ipaddresses">>, IPs), ?XMFIELD('text-multi', ?T("Resources"), <<"onlineresources">>, Resources)]}}; set_form(_From, _Host, _, _Lang, _XData) -> {error, xmpp:err_service_unavailable()}. -spec get_value(binary(), xdata()) -> binary(). get_value(Field, XData) -> hd(get_values(Field, XData)). -spec get_values(binary(), xdata()) -> [binary()]. get_values(Field, XData) -> xmpp_util:get_xdata_values(Field, XData). -spec search_running_node(binary()) -> false | node(). search_running_node(SNode) -> search_running_node(SNode, mnesia:system_info(running_db_nodes)). -spec search_running_node(binary(), [node()]) -> false | node(). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case atom_to_binary(Node, utf8) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. -spec stop_node(jid(), binary(), binary(), restart | stop, xdata()) -> {result, undefined}. stop_node(From, Host, ENode, Action, XData) -> Delay = binary_to_integer(get_value(<<"delay">>, XData)), Subject = case get_values(<<"subject">>, XData) of [] -> []; [S|_] -> [#xdata_field{var = <<"subject">>, values = [S]}] end, Announcement = case get_values(<<"announcement">>, XData) of [] -> []; As -> [#xdata_field{var = <<"body">>, values = As}] end, case Subject ++ Announcement of [] -> ok; Fields -> Request = #adhoc_command{node = ?NS_ADMINX(<<"announce-allhosts">>), action = complete, xdata = #xdata{type = submit, fields = Fields}}, To = jid:make(Host), mod_announce:announce_commands(empty, From, To, Request) end, Time = timer:seconds(Delay), Node = misc:binary_to_atom(ENode), {ok, _} = timer:apply_after(Time, ejabberd_cluster, call, [Node, init, Action, []]), {result, undefined}. -spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | not_found. get_last_info(User, Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> mod_last:get_last_info(User, Server); false -> not_found end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec adhoc_sm_commands(adhoc_command(), jid(), jid(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. adhoc_sm_commands(_Acc, From, #jid{user = User, server = Server, lserver = LServer}, #adhoc_command{lang = Lang, node = <<"config">>, action = Action, xdata = XData} = Request) -> case acl:match_rule(LServer, configure, From) of deny -> {error, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)}; allow -> ActionIsExecute = Action == execute orelse Action == complete, if Action == cancel -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = canceled}); XData == undefined, ActionIsExecute -> case get_sm_form(User, Server, <<"config">>, Lang) of {result, Form} -> xmpp_util:make_adhoc_response( Request, #adhoc_command{status = executing, xdata = Form}); {error, Error} -> {error, Error} end; XData /= undefined, ActionIsExecute -> set_sm_form(User, Server, <<"config">>, Request); true -> Txt = ?T("Unexpected action"), {error, xmpp:err_bad_request(Txt, Lang)} end end; adhoc_sm_commands(Acc, _From, _To, _Request) -> Acc. -spec get_sm_form(binary(), binary(), binary(), binary()) -> {result, xdata()} | {error, stanza_error()}. get_sm_form(User, Server, <<"config">>, Lang) -> {result, #xdata{type = form, title = <<(tr(Lang, ?T("Administration of ")))/binary, User/binary>>, fields = [?HFIELD(), #xdata_field{ type = 'list-single', label = tr(Lang, ?T("Action on user")), var = <<"action">>, values = [<<"edit">>], options = [#xdata_option{ label = tr(Lang, ?T("Edit Properties")), value = <<"edit">>}, #xdata_option{ label = tr(Lang, ?T("Remove User")), value = <<"remove">>}]}, ?XFIELD('text-private', ?T("Password"), <<"password">>, ejabberd_auth:get_password_s(User, Server))]}}; get_sm_form(_User, _Server, _Node, _Lang) -> {error, xmpp:err_service_unavailable()}. -spec set_sm_form(binary(), binary(), binary(), adhoc_command()) -> adhoc_command() | {error, stanza_error()}. set_sm_form(User, Server, <<"config">>, #adhoc_command{lang = Lang, node = Node, sid = SessionID, xdata = XData}) -> Response = #adhoc_command{lang = Lang, node = Node, sid = SessionID, status = completed}, case xmpp_util:get_xdata_values(<<"action">>, XData) of [<<"edit">>] -> case xmpp_util:get_xdata_values(<<"password">>, XData) of [Password] -> ejabberd_auth:set_password(User, Server, Password), xmpp_util:make_adhoc_response(Response); _ -> Txt = ?T("No 'password' found in data form"), {error, xmpp:err_not_acceptable(Txt, Lang)} end; [<<"remove">>] -> ejabberd_auth:remove_user(User, Server), xmpp_util:make_adhoc_response(Response); _ -> Txt = ?T("Incorrect value of 'action' in data form"), {error, xmpp:err_not_acceptable(Txt, Lang)} end; set_sm_form(_User, _Server, _Node, _Request) -> {error, xmpp:err_service_unavailable()}. -spec tr(binary(), binary()) -> binary(). tr(Lang, Text) -> translate:translate(Lang, Text). mod_options(_) -> []. mod_doc() -> #{desc => ?T("The module provides server configuration functionality via " "https://xmpp.org/extensions/xep-0050.html" "[XEP-0050: Ad-Hoc Commands]. This module requires " "_`mod_adhoc`_ to be loaded.")}. ejabberd-21.12/src/mod_stun_disco.erl0000644000232200023220000006147114154362354020147 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_stun_disco.erl %%% Author : Holger Weiss %%% Purpose : External Service Discovery (XEP-0215) %%% Created : 18 Apr 2020 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2020-2021 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(mod_stun_disco). -author('holger@zedat.fu-berlin.de'). -protocol({xep, 215, '0.7'}). -behaviour(gen_server). -behaviour(gen_mod). %% gen_mod callbacks. -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% gen_server callbacks. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% ejabberd_hooks callbacks. -export([disco_local_features/5, stun_get_password/3]). %% gen_iq_handler callback. -export([process_iq/1]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -define(STUN_MODULE, ejabberd_stun). -type host_or_hash() :: binary() | {hash, binary()}. -type service_type() :: stun | stuns | turn | turns | undefined. -record(request, {host :: binary() | inet:ip_address() | undefined, port :: 0..65535 | undefined, transport :: udp | tcp | undefined, type :: service_type(), restricted :: true | undefined}). -record(state, {host :: binary(), services :: [service()], secret :: binary(), ttl :: non_neg_integer()}). -type request() :: #request{}. -type state() :: #state{}. %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> {ok, pid()} | {error, any()}. start(Host, Opts) -> Proc = get_proc_name(Host), gen_mod:start_child(?MODULE, Host, Opts, Proc). -spec stop(binary()) -> ok | {error, any()}. stop(Host) -> Proc = get_proc_name(Host), gen_mod:stop_child(Proc). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> cast(Host, {reload, NewOpts, OldOpts}). -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access) -> econf:acl(); mod_opt_type(credentials_lifetime) -> econf:timeout(second); mod_opt_type(offer_local_services) -> econf:bool(); mod_opt_type(secret) -> econf:binary(); mod_opt_type(services) -> econf:list( econf:and_then( econf:options( #{host => econf:either(econf:ip(), econf:binary()), port => econf:port(), type => econf:enum([stun, turn, stuns, turns]), transport => econf:enum([tcp, udp]), restricted => econf:bool()}, [{required, [host]}]), fun(Opts) -> DefPort = fun(stun) -> 3478; (turn) -> 3478; (stuns) -> 5349; (turns) -> 5349 end, DefTrns = fun(stun) -> udp; (turn) -> udp; (stuns) -> tcp; (turns) -> tcp end, DefRstr = fun(stun) -> false; (turn) -> true; (stuns) -> false; (turns) -> true end, Host = proplists:get_value(host, Opts), Type = proplists:get_value(type, Opts, stun), Port = proplists:get_value(port, Opts, DefPort(Type)), Trns = proplists:get_value(transport, Opts, DefTrns(Type)), Rstr = proplists:get_value(restricted, Opts, DefRstr(Type)), #service{host = Host, port = Port, type = Type, transport = Trns, restricted = Rstr} end)). -spec mod_options(binary()) -> [{services, [tuple()]} | {atom(), any()}]. mod_options(_Host) -> [{access, local}, {credentials_lifetime, timer:hours(12)}, {offer_local_services, true}, {secret, undefined}, {services, []}]. mod_doc() -> #{desc => ?T("This module allows XMPP clients to discover STUN/TURN services " "and to obtain temporary credentials for using them as per " "https://xmpp.org/extensions/xep-0215.html" "[XEP-0215: External Service Discovery]. " "This module is included in ejabberd since version 20.04."), opts => [{access, #{value => ?T("AccessName"), desc => ?T("This option defines which access rule will be used to " "control who is allowed to discover STUN/TURN services " "and to request temporary credentials. The default value " "is 'local'.")}}, {credentials_lifetime, #{value => "timeout()", desc => ?T("The lifetime of temporary credentials offered to " "clients. If ejabberd's built-in TURN service is used, " "TURN relays allocated using temporary credentials will " "be terminated shortly after the credentials expired. The " "default value is '12 hours'. Note that restarting the " "ejabberd node invalidates any temporary credentials " "offered before the restart unless a 'secret' is " "specified (see below).")}}, {offer_local_services, #{value => "true | false", desc => ?T("This option specifies whether local STUN/TURN services " "configured as ejabberd listeners should be announced " "automatically. Note that this will not include " "TLS-enabled services, which must be configured manually " "using the 'services' option (see below). For " "non-anonymous TURN services, temporary credentials will " "be offered to the client. The default value is " "'true'.")}}, {secret, #{value => ?T("Text"), desc => ?T("The secret used for generating temporary credentials. If " "this option isn't specified, a secret will be " "auto-generated. However, a secret must be specified " "explicitly if non-anonymous TURN services running on " "other ejabberd nodes and/or external TURN 'services' are " "configured. Also note that auto-generated secrets are " "lost when the node is restarted, which invalidates any " "credentials offered before the restart. Therefore, it's " "recommended to explicitly specify a secret if clients " "cache retrieved credentials (for later use) across " "service restarts.")}}, {services, #{value => "[Service, ...]", example => ["services:", " -", " host: 203.0.113.3", " port: 3478", " type: stun", " transport: udp", " restricted: false", " -", " host: 203.0.113.3", " port: 3478", " type: turn", " transport: udp", " restricted: true", " -", " host: 2001:db8::3", " port: 3478", " type: stun", " transport: udp", " restricted: false", " -", " host: 2001:db8::3", " port: 3478", " type: turn", " transport: udp", " restricted: true", " -", " host: server.example.com", " port: 5349", " type: turns", " transport: tcp", " restricted: true"], desc => ?T("The list of services offered to clients. This list can " "include STUN/TURN services running on any ejabberd node " "and/or external services. However, if any listed TURN " "service not running on the local ejabberd node requires " "authentication, a 'secret' must be specified explicitly, " "and must be shared with that service. This will only " "work with ejabberd's built-in STUN/TURN server and with " "external servers that support the same " "https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00" "[REST API For Access To TURN Services]. Unless the " "'offer_local_services' is set to 'false', the explicitly " "listed services will be offered in addition to those " "announced automatically.")}, [{host, #{value => ?T("Host"), desc => ?T("The hostname or IP address the STUN/TURN service is " "listening on. For non-TLS services, it's recommended " "to specify an IP address (to avoid additional DNS " "lookup latency on the client side). For TLS services, " "the hostname (or IP address) should match the " "certificate. Specifying the 'host' option is " "mandatory.")}}, {port, #{value => "1..65535", desc => ?T("The port number the STUN/TURN service is listening " "on. The default port number is 3478 for non-TLS " "services and 5349 for TLS services.")}}, {type, #{value => "stun | turn | stuns | turns", desc => ?T("The type of service. Must be 'stun' or 'turn' for " "non-TLS services, 'stuns' or 'turns' for TLS services. " "The default type is 'stun'.")}}, {transport, #{value => "tcp | udp", desc => ?T("The transport protocol supported by the service. The " "default is 'udp' for non-TLS services and 'tcp' for " "TLS services.")}}, {restricted, #{value => "true | false", desc => ?T("This option determines whether temporary credentials " "for accessing the service are offered. The default is " "'false' for STUN/STUNS services and 'true' for " "TURN/TURNS services.")}}]}]}. %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(list()) -> {ok, state()}. init([Host, Opts]) -> process_flag(trap_exit, true), Services = get_configured_services(Opts), Secret = get_configured_secret(Opts), TTL = get_configured_ttl(Opts), register_iq_handlers(Host), register_hooks(Host), {ok, #state{host = Host, services = Services, secret = Secret, ttl = TTL}}. -spec handle_call(term(), {pid(), term()}, state()) -> {reply, {turn_disco, [service()] | binary()}, state()} | {noreply, state()}. handle_call({get_services, JID, #request{host = ReqHost, port = ReqPort, type = ReqType, transport = ReqTrns, restricted = ReqRstr}}, _From, #state{host = Host, services = List0, secret = Secret, ttl = TTL} = State) -> ?DEBUG("Getting STUN/TURN service list for ~ts", [jid:encode(JID)]), Hash = <<(hash(jid:encode(JID)))/binary, (hash(Host))/binary>>, List = lists:filtermap( fun(#service{host = H, port = P, type = T, restricted = R}) when (ReqHost /= undefined) and (H /= ReqHost); (ReqPort /= undefined) and (P /= ReqPort); (ReqType /= undefined) and (T /= ReqType); (ReqTrns /= undefined) and (T /= ReqTrns); (ReqRstr /= undefined) and (R /= ReqRstr) -> false; (#service{restricted = false}) -> true; (#service{restricted = true} = Service) -> {true, add_credentials(Service, Hash, Secret, TTL)} end, List0), ?INFO_MSG("Offering STUN/TURN services to ~ts (~s)", [jid:encode(JID), Hash]), {reply, {turn_disco, List}, State}; handle_call({get_password, Username}, _From, #state{secret = Secret} = State) -> ?DEBUG("Getting STUN/TURN password for ~ts", [Username]), Password = make_password(Username, Secret), {reply, {turn_disco, Password}, State}; handle_call(Request, From, State) -> ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({reload, NewOpts, _OldOpts}, #state{host = Host} = State) -> ?DEBUG("Reloading STUN/TURN discovery configuration for ~ts", [Host]), Services = get_configured_services(NewOpts), Secret = get_configured_secret(NewOpts), TTL = get_configured_ttl(NewOpts), {noreply, State#state{services = Services, secret = Secret, ttl = TTL}}; handle_cast(Request, State) -> ?ERROR_MSG("Got unexpected request: ~p", [Request]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. terminate(Reason, #state{host = Host}) -> ?DEBUG("Stopping STUN/TURN discovery process for ~ts: ~p", [Host, Reason]), unregister_hooks(Host), unregister_iq_handlers(Host). -spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, #state{host = Host} = State, _Extra) -> ?DEBUG("Updating STUN/TURN discovery process for ~ts", [Host]), {ok, State}. %%-------------------------------------------------------------------- %% Register/unregister hooks. %%-------------------------------------------------------------------- -spec register_hooks(binary()) -> ok. register_hooks(Host) -> ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_local_features, 50), ejabberd_hooks:add(stun_get_password, ?MODULE, stun_get_password, 50). -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_local_features, 50), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_hooks:delete(stun_get_password, ?MODULE, stun_get_password, 50); true -> ok end. %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- -spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). disco_local_features(empty, From, To, Node, Lang) -> disco_local_features({result, []}, From, To, Node, Lang); disco_local_features({result, OtherFeatures} = Acc, From, #jid{lserver = LServer}, <<"">>, _Lang) -> Access = mod_stun_disco_opt:access(LServer), case acl:match_rule(LServer, Access, From) of allow -> ?DEBUG("Announcing feature to ~ts", [jid:encode(From)]), {result, [?NS_EXTDISCO_2 | OtherFeatures]}; deny -> ?DEBUG("Not announcing feature to ~ts", [jid:encode(From)]), Acc end; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec stun_get_password(any(), binary(), binary()) -> binary() | {stop, binary()}. stun_get_password(<<>>, Username, _Realm) -> case binary:split(Username, <<$:>>) of [Expiration, <<_UserHash:8/binary, HostHash:8/binary>>] -> try binary_to_integer(Expiration) of ExpireTime -> case erlang:system_time(second) of Now when Now < ExpireTime -> ?DEBUG("Looking up password for: ~ts", [Username]), {stop, get_password(Username, HostHash)}; Now when Now >= ExpireTime -> ?INFO_MSG("Credentials expired: ~ts", [Username]), {stop, <<>>} end catch _:badarg -> ?DEBUG("Non-numeric expiration field: ~ts", [Username]), <<>> end; _ -> ?DEBUG("Not an ephemeral username: ~ts", [Username]), <<>> end; stun_get_password(Acc, _Username, _Realm) -> Acc. %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_EXTDISCO_2, ?MODULE, process_iq). -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_EXTDISCO_2). -spec process_iq(iq()) -> iq(). process_iq(#iq{type = get, sub_els = [#services{type = ReqType}]} = IQ) -> Request = #request{type = ReqType}, process_iq_get(IQ, Request); process_iq(#iq{type = get, sub_els = [#credentials{ services = [#service{ host = ReqHost, port = ReqPort, type = ReqType, transport = ReqTrns, name = <<>>, username = <<>>, password = <<>>, expires = undefined, restricted = undefined, action = undefined, xdata = undefined}]}]} = IQ) -> % Accepting the 'transport' request attribute is an ejabberd extension. Request = #request{host = ReqHost, port = ReqPort, type = ReqType, transport = ReqTrns, restricted = true}, process_iq_get(IQ, Request); process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_iq_get(iq(), request()) -> iq(). process_iq_get(#iq{from = From, to = #jid{lserver = Host}, lang = Lang} = IQ, Request) -> Access = mod_stun_disco_opt:access(Host), case acl:match_rule(Host, Access, From) of allow -> ?DEBUG("Performing external service discovery for ~ts", [jid:encode(From)]), case get_services(Host, From, Request) of {ok, Services} -> xmpp:make_iq_result(IQ, #services{list = Services}); {error, timeout} -> % Has been logged already. Txt = ?T("Service list retrieval timed out"), Err = xmpp:err_internal_server_error(Txt, Lang), xmpp:make_error(IQ, Err) end; deny -> ?DEBUG("Won't perform external service discovery for ~ts", [jid:encode(From)]), Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec get_configured_services(gen_mod:opts()) -> [service()]. get_configured_services(Opts) -> LocalServices = case mod_stun_disco_opt:offer_local_services(Opts) of true -> ?DEBUG("Discovering local services", []), find_local_services(); false -> ?DEBUG("Won't discover local services", []), [] end, dedup(LocalServices ++ mod_stun_disco_opt:services(Opts)). -spec get_configured_secret(gen_mod:opts()) -> binary(). get_configured_secret(Opts) -> case mod_stun_disco_opt:secret(Opts) of undefined -> ?DEBUG("Auto-generating secret", []), new_secret(); Secret -> ?DEBUG("Using configured secret", []), Secret end. -spec get_configured_ttl(gen_mod:opts()) -> non_neg_integer(). get_configured_ttl(Opts) -> mod_stun_disco_opt:credentials_lifetime(Opts) div 1000. -spec new_secret() -> binary(). new_secret() -> p1_rand:bytes(20). -spec add_credentials(service(), binary(), binary(), non_neg_integer()) -> service(). add_credentials(Service, Hash, Secret, TTL) -> ExpireAt = erlang:system_time(second) + TTL, Username = make_username(ExpireAt, Hash), Password = make_password(Username, Secret), ?DEBUG("Created ephemeral credentials: ~s | ~s", [Username, Password]), Service#service{username = Username, password = Password, expires = seconds_to_timestamp(ExpireAt)}. -spec make_username(non_neg_integer(), binary()) -> binary(). make_username(ExpireAt, Hash) -> <<(integer_to_binary(ExpireAt))/binary, $:, Hash/binary>>. -spec make_password(binary(), binary()) -> binary(). make_password(Username, Secret) -> base64:encode(misc:crypto_hmac(sha, Secret, Username)). -spec get_password(binary(), binary()) -> binary(). get_password(Username, HostHash) -> try call({hash, HostHash}, {get_password, Username}) of {turn_disco, Password} -> Password catch exit:{timeout, _} -> ?ERROR_MSG("Asking ~ts for password timed out", [HostHash]), <<>>; exit:{noproc, _} -> % Can be triggered by bogus Username. ?DEBUG("Cannot retrieve password for ~ts", [Username]), <<>> end. -spec get_services(binary(), jid(), request()) -> {ok, [service()]} | {error, timeout}. get_services(Host, JID, Request) -> try call(Host, {get_services, JID, Request}) of {turn_disco, Services} -> {ok, Services} catch exit:{timeout, _} -> ?ERROR_MSG("Asking ~ts for services timed out", [Host]), {error, timeout} end. -spec find_local_services() -> [service()]. find_local_services() -> ParseListener = fun(Listener) -> parse_listener(Listener) end, lists:flatmap(ParseListener, ejabberd_option:listen()). -spec parse_listener(ejabberd_listener:listener()) -> [service()]. parse_listener({_EndPoint, ?STUN_MODULE, #{tls := true}}) -> ?DEBUG("Ignoring TLS-enabled STUN/TURN listener", []), []; % Avoid certificate hostname issues. parse_listener({{Port, _Addr, Transport}, ?STUN_MODULE, Opts}) -> case get_listener_ips(Opts) of {undefined, undefined} -> ?INFO_MSG("Won't auto-announce STUN/TURN service on port ~B (~s) " "without public IP address, please specify " "'turn_ipv4_address' and optionally 'turn_ipv6_address'", [Port, Transport]), []; {IPv4Addr, IPv6Addr} -> lists:flatmap( fun(undefined) -> []; (Addr) -> StunService = #service{host = Addr, port = Port, transport = Transport, restricted = false, type = stun}, case Opts of #{use_turn := true} -> ?INFO_MSG("Going to offer STUN/TURN service: " "~s (~s)", [addr_to_str(Addr, Port), Transport]), [StunService, #service{host = Addr, port = Port, transport = Transport, restricted = is_restricted(Opts), type = turn}]; #{use_turn := false} -> ?INFO_MSG("Going to offer STUN service: " "~s (~s)", [addr_to_str(Addr, Port), Transport]), [StunService] end end, [IPv4Addr, IPv6Addr]) end; parse_listener({_EndPoint, Module, _Opts}) -> ?DEBUG("Ignoring ~s listener", [Module]), []. -spec get_listener_ips(map()) -> {inet:ip4_address() | undefined, inet:ip6_address() | undefined}. get_listener_ips(#{ip := {0, 0, 0, 0}} = Opts) -> {get_turn_ipv4_addr(Opts), undefined}; get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 0}} = Opts) -> {get_turn_ipv4_addr(Opts), get_turn_ipv6_addr(Opts)}; % Assume dual-stack. get_listener_ips(#{ip := {127, _, _, _}} = Opts) -> {get_turn_ipv4_addr(Opts), undefined}; get_listener_ips(#{ip := {0, 0, 0, 0, 0, 0, 0, 1}} = Opts) -> {undefined, get_turn_ipv6_addr(Opts)}; get_listener_ips(#{ip := {_, _, _, _} = IP}) -> {IP, undefined}; get_listener_ips(#{ip := {_, _, _, _, _,_, _, _, _} = IP}) -> {undefined, IP}. -spec get_turn_ipv4_addr(map()) -> inet:ip4_address() | undefined. get_turn_ipv4_addr(#{turn_ipv4_address := {_, _, _, _} = TurnIP}) -> TurnIP; get_turn_ipv4_addr(#{turn_ipv4_address := undefined}) -> case misc:get_my_ipv4_address() of {127, _, _, _} -> undefined; IP -> IP end. -spec get_turn_ipv6_addr(map()) -> inet:ip6_address() | undefined. get_turn_ipv6_addr(#{turn_ipv6_address := {_, _, _, _, _, _, _, _} = TurnIP}) -> TurnIP; get_turn_ipv6_addr(#{turn_ipv6_address := undefined}) -> case misc:get_my_ipv6_address() of {0, 0, 0, 0, 0, 0, 0, 1} -> undefined; IP -> IP end. -spec is_restricted(map()) -> boolean(). is_restricted(#{auth_type := user}) -> true; is_restricted(#{auth_type := anonymous}) -> false. -spec call(host_or_hash(), term()) -> term(). call(Host, Request) -> Proc = get_proc_name(Host), gen_server:call(Proc, Request, timer:seconds(15)). -spec cast(host_or_hash(), term()) -> ok. cast(Host, Request) -> Proc = get_proc_name(Host), gen_server:cast(Proc, Request). -spec get_proc_name(host_or_hash()) -> atom(). get_proc_name(Host) when is_binary(Host) -> get_proc_name({hash, hash(Host)}); get_proc_name({hash, HostHash}) -> gen_mod:get_module_proc(HostHash, ?MODULE). -spec hash(binary()) -> binary(). hash(Host) -> str:to_hexlist(binary_part(crypto:hash(sha, Host), 0, 4)). -spec dedup(list()) -> list(). dedup([]) -> []; dedup([H | T]) -> [H | [E || E <- dedup(T), E /= H]]. -spec seconds_to_timestamp(non_neg_integer()) -> erlang:timestamp(). seconds_to_timestamp(Seconds) -> {Seconds div 1000000, Seconds rem 1000000, 0}. -spec addr_to_str(inet:ip_address(), 0..65535) -> iolist(). addr_to_str({_, _, _, _, _, _, _, _} = Addr, Port) -> [$[, inet_parse:ntoa(Addr), $], $:, integer_to_list(Port)]; addr_to_str({_, _, _, _} = Addr, Port) -> [inet_parse:ntoa(Addr), $:, integer_to_list(Port)]. ejabberd-21.12/src/mod_adhoc_opt.erl0000644000232200023220000000062414154362354017726 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_adhoc_opt). -export([report_commands_node/1]). -spec report_commands_node(gen_mod:opts() | global | binary()) -> boolean(). report_commands_node(Opts) when is_map(Opts) -> gen_mod:get_opt(report_commands_node, Opts); report_commands_node(Host) -> gen_mod:get_module_opt(Host, mod_adhoc, report_commands_node). ejabberd-21.12/src/ejabberd_piefxis.erl0000644000232200023220000005701514154362354020422 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_piefxis.erl %%% Author : Pablo Polvorin, Vidal Santiago Martinez, Evgeniy Khramtsov %%% Purpose : XEP-0227: Portable Import/Export Format for XMPP-IM Servers %%% Created : 17 Jul 2008 by Pablo Polvorin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% Not implemented: %%% - PEP nodes export/import %%% - message archives export/import %%% - write mod_piefxis with ejabberdctl commands %%% - Other schemas of XInclude are not tested, and may not be imported correctly. %%% - If a host has many users, split that host in XML files with 50 users each. -module(ejabberd_piefxis). -protocol({xep, 227, '1.1'}). -export([import_file/1, export_server/1, export_host/2]). -define(CHUNK_SIZE, 1024*20). %20k -include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("mod_roster.hrl"). %%-include_lib("exmpp/include/exmpp.hrl"). %%-include_lib("exmpp/include/exmpp_client.hrl"). %% Copied from exmpp header files: %% Copied from mod_private.erl %%-define(ERROR_MSG(M,Args),io:format(M,Args)). %%-define(INFO_MSG(M,Args),ok). %%%================================== %%%% Import file -define(NS_PIE, <<"urn:xmpp:pie:0">>). -define(NS_PIEFXIS, <<"http://www.xmpp.org/extensions/xep-0227.html#ns">>). -define(NS_XI, <<"http://www.w3.org/2001/XInclude">>). -record(state, {xml_stream_state :: fxml_stream:xml_stream_state() | undefined, user = <<"">> :: binary(), server = <<"">> :: binary(), fd = self() :: file:io_device(), dir = <<"">> :: binary()}). -type state() :: #state{}. %%File could be large.. we read it in chunks %%%=================================================================== %%% API %%%=================================================================== import_file(FileName) -> import_file(FileName, #state{}). -spec import_file(binary(), state()) -> ok | {error, atom()}. import_file(FileName, State) -> case file:open(FileName, [read, binary]) of {ok, Fd} -> Dir = filename:dirname(FileName), XMLStreamState = fxml_stream:new(self(), infinity), Res = process(State#state{xml_stream_state = XMLStreamState, fd = Fd, dir = Dir}), file:close(Fd), Res; {error, Reason} -> ErrTxt = file:format_error(Reason), ?ERROR_MSG("Failed to open file '~ts': ~ts", [FileName, ErrTxt]), {error, Reason} end. -spec export_server(binary()) -> any(). export_server(Dir) -> export_hosts(ejabberd_option:hosts(), Dir). -spec export_host(binary(), binary()) -> any(). export_host(Dir, Host) -> export_hosts([Host], Dir). %%%=================================================================== %%% Internal functions %%%=================================================================== export_hosts(Hosts, Dir) -> FnT = make_filename_template(), DFn = make_main_basefilename(Dir, FnT), case file:open(DFn, [raw, write]) of {ok, Fd} -> print(Fd, make_piefxis_xml_head()), print(Fd, make_piefxis_server_head()), FilesAndHosts = [{make_host_filename(FnT, Host), Host} || Host <- Hosts], lists:foreach( fun({FnH, _}) -> print(Fd, make_xinclude(FnH)) end, FilesAndHosts), print(Fd, make_piefxis_server_tail()), print(Fd, make_piefxis_xml_tail()), file:close(Fd), lists:foldl( fun({FnH, Host}, ok) -> export_host(Dir, FnH, Host); (_, Err) -> Err end, ok, FilesAndHosts); {error, Reason} -> ErrTxt = file:format_error(Reason), ?ERROR_MSG("Failed to open file '~ts': ~ts", [DFn, ErrTxt]), {error, Reason} end. export_host(Dir, FnH, Host) -> DFn = make_host_basefilename(Dir, FnH), case file:open(DFn, [raw, write]) of {ok, Fd} -> print(Fd, make_piefxis_xml_head()), print(Fd, make_piefxis_host_head(Host)), Users = ejabberd_auth:get_users(Host), case export_users(Users, Host, Fd) of ok -> print(Fd, make_piefxis_host_tail()), print(Fd, make_piefxis_xml_tail()), file:close(Fd), ok; Err -> file:close(Fd), file:delete(DFn), Err end; {error, Reason} -> ErrTxt = file:format_error(Reason), ?ERROR_MSG("Failed to open file '~ts': ~ts", [DFn, ErrTxt]), {error, Reason} end. export_users([{User, _S}|Users], Server, Fd) -> case export_user(User, Server, Fd) of ok -> export_users(Users, Server, Fd); Err -> Err end; export_users([], _Server, _Fd) -> ok. export_user(User, Server, Fd) -> Password = ejabberd_auth:get_password_s(User, Server), LServer = jid:nameprep(Server), {PassPlain, PassScram} = case ejabberd_auth:password_format(LServer) of scram -> {[], [format_scram_password(Password)]}; _ -> {[{<<"password">>, Password}], []} end, Els = PassScram ++ get_offline(User, Server) ++ get_vcard(User, Server) ++ get_privacy(User, Server) ++ get_roster(User, Server) ++ get_private(User, Server), print(Fd, fxml:element_to_binary( #xmlel{name = <<"user">>, attrs = [{<<"name">>, User} | PassPlain], children = Els})). format_scram_password(#scram{hash = Hash, storedkey = StoredKey, serverkey = ServerKey, salt = Salt, iterationcount = IterationCount}) -> StoredKeyB64 = base64:encode(StoredKey), ServerKeyB64 = base64:encode(ServerKey), SaltB64 = base64:encode(Salt), IterationCountBin = (integer_to_binary(IterationCount)), MechanismB = case Hash of sha -> <<"SCRAM-SHA-1">>; sha256 -> <<"SCRAM-SHA-256">>; sha512 -> <<"SCRAM-SHA-512">> end, Children = [ #xmlel{name = <<"iter-count">>, children = [{xmlcdata, IterationCountBin}]}, #xmlel{name = <<"salt">>, children = [{xmlcdata, SaltB64}]}, #xmlel{name = <<"server-key">>, children = [{xmlcdata, ServerKeyB64}]}, #xmlel{name = <<"stored-key">>, children = [{xmlcdata, StoredKeyB64}]} ], #xmlel{name = <<"scram-credentials">>, attrs = [{<<"xmlns">>, <>}, {<<"mechanism">>, MechanismB}], children = Children}. parse_scram_password(#xmlel{attrs = Attrs} = El) -> Hash = case fxml:get_attr_s(<<"mechanism">>, Attrs) of <<"SCRAM-SHA-1">> -> sha; <<"SCRAM-SHA-256">> -> sha256; <<"SCRAM-SHA-512">> -> sha512 end, StoredKeyB64 = fxml:get_path_s(El, [{elem, <<"stored-key">>}, cdata]), ServerKeyB64 = fxml:get_path_s(El, [{elem, <<"server-key">>}, cdata]), IterationCountBin = fxml:get_path_s(El, [{elem, <<"iter-count">>}, cdata]), SaltB64 = fxml:get_path_s(El, [{elem, <<"salt">>}, cdata]), #scram{ storedkey = base64:decode(StoredKeyB64), serverkey = base64:decode(ServerKeyB64), salt = base64:decode(SaltB64), hash = Hash, iterationcount = (binary_to_integer(IterationCountBin)) }; parse_scram_password(PassData) -> Split = binary:split(PassData, <<",">>, [global]), [Hash, StoredKeyB64, ServerKeyB64, SaltB64, IterationCountBin] = case Split of [K1, K2, K3, K4] -> [sha, K1, K2, K3, K4]; [<<"sha256">>, K1, K2, K3, K4] -> [sha256, K1, K2, K3, K4]; [<<"sha512">>, K1, K2, K3, K4] -> [sha512, K1, K2, K3, K4] end, #scram{ storedkey = base64:decode(StoredKeyB64), serverkey = base64:decode(ServerKeyB64), salt = base64:decode(SaltB64), hash = Hash, iterationcount = (binary_to_integer(IterationCountBin)) }. -spec get_vcard(binary(), binary()) -> [xmlel()]. get_vcard(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), try mod_vcard:get_vcard(LUser, LServer) of error -> []; Els -> Els catch error:{module_not_loaded, _, _} -> [] end. -spec get_offline(binary(), binary()) -> [xmlel()]. get_offline(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), try mod_offline:get_offline_els(LUser, LServer) of [] -> []; Els -> NewEls = lists:map(fun xmpp:encode/1, Els), [#xmlel{name = <<"offline-messages">>, children = NewEls}] catch error:{module_not_loaded, _, _} -> [] end. -spec get_privacy(binary(), binary()) -> [xmlel()]. get_privacy(User, Server) -> try mod_privacy:get_user_lists(User, Server) of {ok, #privacy{default = Default, lists = [_|_] = Lists}} -> XLists = lists:map( fun({Name, Items}) -> XItems = lists:map( fun mod_privacy:encode_list_item/1, Items), #privacy_list{name = Name, items = XItems} end, Lists), [xmpp:encode(#privacy_query{default = Default, lists = XLists})]; _ -> [] catch error:{module_not_loaded, _, _} -> [] end. -spec get_roster(binary(), binary()) -> [xmlel()]. get_roster(User, Server) -> JID = jid:make(User, Server), try mod_roster:get_roster(User, Server) of [_|_] = Items -> Subs = lists:flatmap( fun(#roster{ask = Ask, askmessage = Msg} = R) when Ask == in; Ask == both -> Status = if is_binary(Msg) -> (Msg); true -> <<"">> end, [xmpp:encode( #presence{from = jid:make(R#roster.jid), to = JID, type = subscribe, status = xmpp:mk_text(Status)})]; (_) -> [] end, Items), Rs = lists:flatmap( fun(#roster{ask = in, subscription = none}) -> []; (R) -> [mod_roster:encode_item(R)] end, Items), [xmpp:encode(#roster_query{items = Rs}) | Subs]; _ -> [] catch error:{module_not_loaded, _, _} -> [] end. -spec get_private(binary(), binary()) -> [xmlel()]. get_private(User, Server) -> try mod_private:get_data(User, Server) of [_|_] = Els -> [xmpp:encode(#private{sub_els = Els})]; _ -> [] catch error:{module_not_loaded, _, _} -> [] end. process(#state{xml_stream_state = XMLStreamState, fd = Fd} = State) -> case file:read(Fd, ?CHUNK_SIZE) of {ok, Data} -> NewXMLStreamState = fxml_stream:parse(XMLStreamState, Data), case process_els(State#state{xml_stream_state = NewXMLStreamState}) of {ok, NewState} -> process(NewState); Err -> fxml_stream:close(NewXMLStreamState), Err end; eof -> fxml_stream:close(XMLStreamState), ok end. process_els(State) -> Els = gather_els(State, []), process_els(State, lists:reverse(Els)). gather_els(State, List) -> receive {'$gen_event', El} -> gather_els(State, [El | List]) after 0 -> List end. process_els(State, [El | Tail]) -> case process_el(El, State) of {ok, NewState} -> process_els(NewState, Tail); Err -> Err end; process_els(State, []) -> {ok, State}. process_el({xmlstreamstart, <<"server-data">>, Attrs}, State) -> case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_PIEFXIS -> {ok, State}; ?NS_PIE -> {ok, State}; NS -> stop("Unknown 'server-data' namespace = ~ts", [NS]) end; process_el({xmlstreamend, _}, State) -> {ok, State}; process_el({xmlstreamcdata, _}, State) -> {ok, State}; process_el({xmlstreamelement, #xmlel{name = <<"xi:include">>, attrs = Attrs}}, #state{dir = Dir, user = <<"">>} = State) -> FileName = fxml:get_attr_s(<<"href">>, Attrs), case import_file(filename:join([Dir, FileName]), State) of ok -> {ok, State}; Err -> Err end; process_el({xmlstreamstart, <<"host">>, Attrs}, State) -> process_el({xmlstreamelement, #xmlel{name = <<"host">>, attrs = Attrs}}, State); process_el({xmlstreamelement, #xmlel{name = <<"host">>, attrs = Attrs, children = Els}}, State) -> JIDS = fxml:get_attr_s(<<"jid">>, Attrs), try jid:decode(JIDS) of #jid{lserver = S} -> case ejabberd_router:is_my_host(S) of true -> process_users(Els, State#state{server = S}); false -> stop("Unknown host: ~ts", [S]) end catch _:{bad_jid, _} -> stop("Invalid 'jid': ~ts", [JIDS]) end; process_el({xmlstreamstart, <<"user">>, Attrs}, State = #state{server = S}) when S /= <<"">> -> process_el({xmlstreamelement, #xmlel{name = <<"user">>, attrs = Attrs}}, State); process_el({xmlstreamelement, #xmlel{name = <<"user">>} = El}, State = #state{server = S}) when S /= <<"">> -> process_user(El, State); process_el({xmlstreamelement, El}, State = #state{server = S, user = U}) when S /= <<"">>, U /= <<"">> -> process_user_el(El, State); process_el({xmlstreamelement, El}, _State) -> stop("Unexpected tag: ~p", [El]); process_el({xmlstreamstart, El, Attrs}, _State) -> stop("Unexpected payload: ~p", [{El, Attrs}]); process_el({xmlstreamerror, Err}, _State) -> stop("Failed to process element = ~p", [Err]). process_users([#xmlel{} = El|Els], State) -> case process_user(El, State) of {ok, NewState} -> process_users(Els, NewState); Err -> Err end; process_users([_|Els], State) -> process_users(Els, State); process_users([], State) -> {ok, State}. process_user(#xmlel{name = <<"user">>, attrs = Attrs, children = Els} = El, #state{server = LServer} = State) -> Name = fxml:get_attr_s(<<"name">>, Attrs), Pass = process_password(El, LServer), case jid:nodeprep(Name) of error -> stop("Invalid 'user': ~ts", [Name]); LUser -> case ejabberd_auth:try_register(LUser, LServer, Pass) of ok -> process_user_els(Els, State#state{user = LUser}); {error, invalid_password} when (Pass == <<>>) -> process_user_els(Els, State#state{user = LUser}); {error, Err} -> stop("Failed to create user '~ts': ~p", [Name, Err]) end end. process_password(#xmlel{name = <<"user">>, attrs = Attrs} = El, LServer) -> {PassPlain, PassOldScram} = case fxml:get_attr_s(<<"password">>, Attrs) of <<"scram:", PassData/binary>> -> {<<"">>, PassData}; P -> {P, false} end, ScramCred = fxml:get_subtag(El, <<"scram-credentials">>), PasswordFormat = ejabberd_auth:password_format(LServer), case {PassPlain, PassOldScram, ScramCred, PasswordFormat} of {PassPlain, false, false, plain} -> PassPlain; {<<"">>, false, ScramCred, plain} -> parse_scram_password(ScramCred); {<<"">>, PassOldScram, false, plain} -> parse_scram_password(PassOldScram); {PassPlain, false, false, scram} -> PassPlain; {<<"">>, false, ScramCred, scram} -> parse_scram_password(ScramCred); {<<"">>, PassOldScram, false, scram} -> parse_scram_password(PassOldScram) end. process_user_els([#xmlel{} = El|Els], State) -> case process_user_el(El, State) of {ok, NewState} -> process_user_els(Els, NewState); Err -> Err end; process_user_els([_|Els], State) -> process_user_els(Els, State); process_user_els([], State) -> {ok, State}. process_user_el(#xmlel{name = Name, attrs = Attrs, children = Els} = El, State) -> try case {Name, fxml:get_attr_s(<<"xmlns">>, Attrs)} of {<<"query">>, ?NS_ROSTER} -> process_roster(xmpp:decode(El), State); {<<"query">>, ?NS_PRIVACY} -> %% Make sure elements go before and process_privacy(xmpp:decode(El), State); {<<"query">>, ?NS_PRIVATE} -> process_private(xmpp:decode(El), State); {<<"vCard">>, ?NS_VCARD} -> process_vcard(xmpp:decode(El), State); {<<"offline-messages">>, NS} -> Msgs = [xmpp:decode(E, NS, [ignore_els]) || E <- Els], process_offline_msgs(Msgs, State); {<<"presence">>, ?NS_CLIENT} -> process_presence(xmpp:decode(El, ?NS_CLIENT, [ignore_els]), State); _ -> {ok, State} end catch _:{xmpp_codec, Why} -> ErrTxt = xmpp:format_error(Why), stop("failed to decode XML '~ts': ~ts", [fxml:element_to_binary(El), ErrTxt]) end. -spec process_offline_msgs([stanza()], state()) -> {ok, state()} | {error, _}. process_offline_msgs([#message{} = Msg|Msgs], State) -> case process_offline_msg(Msg, State) of {ok, NewState} -> process_offline_msgs(Msgs, NewState); Err -> Err end; process_offline_msgs([_|Msgs], State) -> process_offline_msgs(Msgs, State); process_offline_msgs([], State) -> {ok, State}. -spec process_roster(roster_query(), state()) -> {ok, state()} | {error, _}. process_roster(RosterQuery, State = #state{user = U, server = S}) -> case mod_roster:set_items(U, S, RosterQuery) of {atomic, _} -> {ok, State}; Err -> stop("Failed to write roster: ~p", [Err]) end. -spec process_privacy(privacy_query(), state()) -> {ok, state()} | {error, _}. process_privacy(#privacy_query{lists = Lists, default = Default, active = Active}, State = #state{user = U, server = S}) -> JID = jid:make(U, S), if Lists /= undefined -> process_privacy2(JID, #privacy_query{lists = Lists}); true -> ok end, if Active /= undefined -> process_privacy2(JID, #privacy_query{active = Active}); true -> ok end, if Default /= undefined -> process_privacy2(JID, #privacy_query{default = Default}); true -> ok end, {ok, State}. process_privacy2(JID, PQ) -> case mod_privacy:process_iq(#iq{type = set, id = p1_rand:get_string(), from = JID, to = JID, sub_els = [PQ]}) of #iq{type = error} = ResIQ -> #stanza_error{reason = Reason} = xmpp:get_error(ResIQ), if Reason /= 'item-not-found' -> %% Failed to set default list because there is no %% list with such name. We shouldn't stop here. stop("Failed to write default privacy: ~p", [Reason]); true -> ok end; _ -> ok end. -spec process_private(private(), state()) -> {ok, state()} | {error, _}. process_private(Private, State = #state{user = U, server = S}) -> JID = jid:make(U, S), IQ = #iq{type = set, id = p1_rand:get_string(), from = JID, to = JID, sub_els = [Private]}, case mod_private:process_sm_iq(IQ) of #iq{type = result} -> {ok, State}; Err -> stop("Failed to write private: ~p", [Err]) end. -spec process_vcard(xmpp_element(), state()) -> {ok, state()} | {error, _}. process_vcard(El, State = #state{user = U, server = S}) -> JID = jid:make(U, S), IQ = #iq{type = set, id = p1_rand:get_string(), from = JID, to = JID, sub_els = [El]}, case mod_vcard:process_sm_iq(IQ) of #iq{type = result} -> {ok, State}; Err -> stop("Failed to write vcard: ~p", [Err]) end. -spec process_offline_msg(message(), state()) -> {ok, state()} | {error, _}. process_offline_msg(#message{from = undefined}, _State) -> stop("No 'from' attribute found", []); process_offline_msg(Msg, State = #state{user = U, server = S}) -> To = jid:make(U, S), ejabberd_hooks:run_fold( offline_message_hook, To#jid.lserver, {pass, xmpp:set_to(Msg, To)}, []), {ok, State}. -spec process_presence(presence(), state()) -> {ok, state()} | {error, _}. process_presence(#presence{from = undefined}, _State) -> stop("No 'from' attribute found", []); process_presence(Pres, #state{user = U, server = S} = State) -> To = jid:make(U, S), NewPres = xmpp:set_to(Pres, To), ejabberd_router:route(NewPres), {ok, State}. stop(Fmt, Args) -> ?ERROR_MSG(Fmt, Args), {error, import_failed}. make_filename_template() -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(), str:format("~4..0w~2..0w~2..0w-~2..0w~2..0w~2..0w", [Year, Month, Day, Hour, Minute, Second]). make_main_basefilename(Dir, FnT) -> Filename2 = <>, filename:join([Dir, Filename2]). %% @doc Make the filename for the host. %% Example: ``(<<"20080804-231550">>, <<"xmpp.domain.tld">>) -> %% <<"20080804-231550_xmpp_domain_tld.xml">>'' make_host_filename(FnT, Host) -> Host2 = str:join(str:tokens(Host, <<".">>), <<"_">>), <>. %%%================================== %%%% PIEFXIS formatting make_host_basefilename(Dir, FnT) -> filename:join([Dir, FnT]). %% @spec () -> string() make_piefxis_xml_head() -> "". %% @spec () -> string() make_piefxis_xml_tail() -> "". %% @spec () -> string() make_piefxis_server_head() -> io_lib:format("", [?NS_PIE, ?NS_XI]). %% @spec () -> string() make_piefxis_server_tail() -> "". %% @spec (Host::string()) -> string() make_piefxis_host_head(Host) -> io_lib:format("", [?NS_PIE, ?NS_XI, Host]). %% @spec () -> string() make_piefxis_host_tail() -> "". %% @spec (Fn::string()) -> string() make_xinclude(Fn) -> Base = filename:basename(Fn), io_lib:format("", [Base]). print(Fd, String) -> file:write(Fd, String). ejabberd-21.12/src/mod_announce_opt.erl0000644000232200023220000000314214154362354020454 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_announce_opt). -export([access/1]). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_announce, access). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_announce, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_announce, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_announce, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_announce, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_announce, use_cache). ejabberd-21.12/src/jd2ejd.erl0000644000232200023220000001206014154362354016266 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : jd2ejd.erl %%% Author : Alexey Shchepin %%% Purpose : Import of jabberd14 user spool file %%% Created : 2 Feb 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(jd2ejd). -author('alexey@process-one.net'). %% External exports -export([import_file/1, import_dir/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- import_file(File) -> User = filename:rootname(filename:basename(File)), Server = filename:basename(filename:dirname(File)), case jid:nodeprep(User) /= error andalso jid:nameprep(Server) /= error of true -> case file:read_file(File) of {ok, Text} -> case fxml_stream:parse_element(Text) of El when is_record(El, xmlel) -> case catch process_xdb(User, Server, El) of {'EXIT', Reason} -> ?ERROR_MSG("Error while processing file \"~ts\": " "~p~n", [File, Reason]), {error, Reason}; _ -> ok end; {error, Reason} -> ?ERROR_MSG("Can't parse file \"~ts\": ~p~n", [File, Reason]), {error, Reason} end; {error, Reason} -> ?ERROR_MSG("Can't read file \"~ts\": ~p~n", [File, Reason]), {error, Reason} end; false -> ?ERROR_MSG("Illegal user/server name in file \"~ts\"~n", [File]), {error, <<"illegal user/server">>} end. import_dir(Dir) -> {ok, Files} = file:list_dir(Dir), MsgFiles = lists:filter(fun (FN) -> case length(FN) > 4 of true -> string:substr(FN, length(FN) - 3) == ".xml"; _ -> false end end, Files), lists:foldl(fun (FN, A) -> Res = import_file(filename:join([Dir, FN])), case {A, Res} of {ok, ok} -> ok; {ok, _} -> {error, <<"see ejabberd log for details">>}; _ -> A end end, ok, MsgFiles). %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- process_xdb(User, Server, #xmlel{name = Name, children = Els}) -> case Name of <<"xdb">> -> lists:foreach(fun (El) -> xdb_data(User, Server, El) end, Els); _ -> ok end. xdb_data(_User, _Server, {xmlcdata, _CData}) -> ok; xdb_data(User, Server, #xmlel{attrs = Attrs} = El) -> From = jid:make(User, Server), LServer = From#jid.lserver, case fxml:get_attr_s(<<"xmlns">>, Attrs) of ?NS_AUTH -> Password = fxml:get_tag_cdata(El), ejabberd_auth:set_password(User, Server, Password), ok; ?NS_ROSTER -> catch mod_roster:set_items(User, Server, xmpp:decode(El)), ok; ?NS_LAST -> TimeStamp = fxml:get_attr_s(<<"last">>, Attrs), Status = fxml:get_tag_cdata(El), catch mod_last:store_last_info(User, Server, binary_to_integer(TimeStamp), Status), ok; ?NS_VCARD -> catch mod_vcard:set_vcard(User, LServer, El), ok; <<"jabber:x:offline">> -> process_offline(Server, From, El), ok; XMLNS -> case fxml:get_attr_s(<<"j_private_flag">>, Attrs) of <<"1">> -> NewAttrs = lists:filter( fun({<<"j_private_flag">>, _}) -> false; ({<<"xdbns">>, _}) -> false; (_) -> true end, Attrs), catch mod_private:set_data( From, [{XMLNS, El#xmlel{attrs = NewAttrs}}]); _ -> ?DEBUG("Unknown namespace \"~ts\"~n", [XMLNS]) end, ok end. process_offline(Server, To, #xmlel{children = Els}) -> LServer = jid:nameprep(Server), lists:foreach( fun(#xmlel{} = El) -> try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of #message{from = JID} = Msg -> From = case JID of undefined -> jid:make(Server); _ -> JID end, ejabberd_hooks:run_fold( offline_message_hook, LServer, {pass, xmpp:set_from_to(Msg, From, To)}, []); _ -> ok catch _:{xmpp_codec, Why} -> Txt = xmpp:format_error(Why), ?ERROR_MSG("Failed to decode XML '~ts': ~ts", [fxml:element_to_binary(El), Txt]) end end, Els). ejabberd-21.12/src/mod_private_sql.erl0000644000232200023220000001026014154362354020314 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_private_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_private_sql). -behaviour(mod_private). %% API -export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, import/3, export/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_private.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. set_data(LUser, LServer, Data) -> F = fun() -> lists:foreach( fun({XMLNS, El}) -> SData = fxml:element_to_binary(El), ?SQL_UPSERT_T( "private_storage", ["!username=%(LUser)s", "!server_host=%(LServer)s", "!namespace=%(XMLNS)s", "data=%(SData)s"]) end, Data) end, case ejabberd_sql:sql_transaction(LServer, F) of {atomic, ok} -> ok; _ -> {error, db_failure} end. get_data(LUser, LServer, XMLNS) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(data)s from private_storage" " where username=%(LUser)s and %(LServer)H" " and namespace=%(XMLNS)s")) of {selected, [{SData}]} -> parse_element(LUser, LServer, SData); {selected, []} -> error; _ -> {error, db_failure} end. get_all_data(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(namespace)s, @(data)s from private_storage" " where username=%(LUser)s and %(LServer)H")) of {selected, []} -> error; {selected, Res} -> {ok, lists:flatmap( fun({_, SData}) -> case parse_element(LUser, LServer, SData) of {ok, El} -> [El]; error -> [] end end, Res)}; _ -> {error, db_failure} end. del_data(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from private_storage" " where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; _ -> {error, db_failure} end. export(_Server) -> [{private_storage, fun(Host, #private_storage{usns = {LUser, LServer, XMLNS}, xml = Data}) when LServer == Host -> SData = fxml:element_to_binary(Data), [?SQL("delete from private_storage where" " username=%(LUser)s and %(LServer)H and namespace=%(XMLNS)s;"), ?SQL_INSERT( "private_storage", ["username=%(LUser)s", "server_host=%(LServer)s", "namespace=%(XMLNS)s", "data=%(SData)s"])]; (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== parse_element(LUser, LServer, XML) -> case fxml_stream:parse_element(XML) of El when is_record(El, xmlel) -> {ok, El}; _ -> ?ERROR_MSG("Malformed XML element in SQL table " "'private_storage' for user ~ts@~ts: ~ts", [LUser, LServer, XML]), error end. ejabberd-21.12/src/extauth_sup.erl0000644000232200023220000000734614154362354017510 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(extauth_sup). -behaviour(supervisor). %% API -export([start/1, stop/1, reload/1, start_link/3]). %% Supervisor callbacks -export([init/1]). -include("logger.hrl"). %%%=================================================================== %%% API functions %%%=================================================================== start(Host) -> case extauth:prog_name(Host) of undefined -> ?ERROR_MSG("Option 'extauth_program' is not set for '~ts'", [Host]), ignore; Prog -> Pool = extauth:pool_name(Host), ChildSpec = {Pool, {?MODULE, start_link, [Host, Prog, Pool]}, permanent, infinity, supervisor, [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec) end. stop(Host) -> Pool = extauth:pool_name(Host), supervisor:terminate_child(ejabberd_backend_sup, Pool), supervisor:delete_child(ejabberd_backend_sup, Pool). reload(Host) -> Pool = extauth:pool_name(Host), Prog = extauth:prog_name(Host), PoolSize = extauth:pool_size(Host), try process_info(whereis(Pool), dictionary) of {dictionary, Dict} -> case proplists:get_value(extauth_program, Dict) of Prog -> OldPoolSize = try supervisor:which_children(Pool) of Children -> length(Children) catch _:_ -> PoolSize end, if OldPoolSize > PoolSize -> lists:foreach( fun(I) -> Worker = extauth:worker_name(Pool, I), supervisor:terminate_child(Pool, Worker), supervisor:delete_child(Pool, Worker) end, lists:seq(PoolSize+1, OldPoolSize)); OldPoolSize < PoolSize -> lists:foreach( fun(I) -> Spec = worker_spec(Pool, Prog, I), supervisor:start_child(Pool, Spec) end, lists:seq(OldPoolSize+1, PoolSize)); OldPoolSize == PoolSize -> ok end; _ -> stop(Host), start(Host) end catch _:badarg -> ok end. start_link(Host, Prog, Pool) -> supervisor:start_link({local, Pool}, ?MODULE, [Host, Prog, Pool]). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([Host, Prog, Pool]) -> PoolSize = extauth:pool_size(Host), Children = lists:map( fun(I) -> worker_spec(Pool, Prog, I) end, lists:seq(1, PoolSize)), put(extauth_program, Prog), {ok, {{one_for_one, PoolSize, 1}, Children}}. %%%=================================================================== %%% Internal functions %%%=================================================================== worker_spec(Pool, Prog, I) -> Worker = extauth:worker_name(Pool, I), {Worker, {extauth, start_link, [Worker, Prog]}, permanent, 5000, worker, [extauth]}. ejabberd-21.12/src/mod_register.erl0000644000232200023220000006163114154362354017617 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_register.erl %%% Author : Alexey Shchepin %%% Purpose : Inband registration support %%% Created : 8 Dec 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_register). -author('alexey@process-one.net'). -protocol({xep, 77, '2.4'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, stream_feature_register/2, c2s_unauthenticated_packet/2, try_register/4, try_register/5, process_iq/1, send_registration_notifications/3, mod_opt_type/1, mod_options/1, depends/2, format_error/1, mod_doc/0]). -deprecated({try_register, 4}). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER, ?MODULE, process_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_REGISTER, ?MODULE, process_iq), ejabberd_hooks:add(c2s_pre_auth_features, Host, ?MODULE, stream_feature_register, 50), ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), ejabberd_mnesia:create(?MODULE, mod_register_ip, [{ram_copies, [node()]}, {local_content, true}, {attributes, [key, value]}]), ok. stop(Host) -> ejabberd_hooks:delete(c2s_pre_auth_features, Host, ?MODULE, stream_feature_register, 50), ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_REGISTER). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. -spec stream_feature_register([xmpp_element()], binary()) -> [xmpp_element()]. stream_feature_register(Acc, Host) -> case {mod_register_opt:access(Host), mod_register_opt:ip_access(Host), mod_register_opt:redirect_url(Host)} of {none, _, undefined} -> Acc; {_, none, undefined} -> Acc; {_, _, _} -> [#feature_register{}|Acc] end. c2s_unauthenticated_packet(#{ip := IP, server := Server} = State, #iq{type = T, sub_els = [_]} = IQ) when T == set; T == get -> try xmpp:try_subtag(IQ, #register{}) of #register{} = Register -> {Address, _} = IP, IQ1 = xmpp:set_els(IQ, [Register]), IQ2 = xmpp:set_from_to(IQ1, jid:make(<<>>), jid:make(Server)), ResIQ = process_iq(IQ2, Address), ResIQ1 = xmpp:set_from_to(ResIQ, jid:make(Server), undefined), {stop, ejabberd_c2s:send(State, ResIQ1)}; false -> State catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Lang = maps:get(lang, State), Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)), {stop, ejabberd_c2s:send(State, Err)} end; c2s_unauthenticated_packet(State, _) -> State. process_iq(#iq{from = From} = IQ) -> process_iq(IQ, jid:tolower(From)). process_iq(#iq{from = From, to = To} = IQ, Source) -> IsCaptchaEnabled = case mod_register_opt:captcha_protected(To#jid.lserver) of true -> true; false -> false end, Server = To#jid.lserver, Access = mod_register_opt:access_remove(Server), Remove = case {acl:match_rule(Server, Access, From), From#jid.lserver} of {allow, Server} -> allow; {_, _} -> deny end, process_iq(IQ, Source, IsCaptchaEnabled, Remove == allow). process_iq(#iq{type = set, lang = Lang, sub_els = [#register{remove = true}]} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove = false) -> Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); process_iq(#iq{type = set, lang = Lang, to = To, from = From, sub_els = [#register{remove = true, username = User, password = Password}]} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove = true) -> Server = To#jid.lserver, if is_binary(User) -> case From of #jid{user = User, lserver = Server} -> ResIQ = xmpp:make_iq_result(IQ), ejabberd_router:route(ResIQ), ejabberd_auth:remove_user(User, Server), ignore; _ -> if is_binary(Password) -> case ejabberd_auth:check_password( User, <<"">>, Server, Password) of true -> ResIQ = xmpp:make_iq_result(IQ), ejabberd_router:route(ResIQ), ejabberd_auth:remove_user(User, Server), ignore; false -> Txt = ?T("Incorrect password"), xmpp:make_error( IQ, xmpp:err_forbidden(Txt, Lang)) end; true -> Txt = ?T("No 'password' found in this query"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end end; true -> case From of #jid{luser = LUser, lserver = Server} -> ResIQ = xmpp:make_iq_result(IQ), ejabberd_router:route(xmpp:set_from_to(ResIQ, From, From)), ejabberd_auth:remove_user(LUser, Server), ignore; _ -> Txt = ?T("The query is only allowed from local users"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end end; process_iq(#iq{type = set, to = To, sub_els = [#register{username = User, password = Password}]} = IQ, Source, IsCaptchaEnabled, _AllowRemove) when is_binary(User), is_binary(Password) -> Server = To#jid.lserver, try_register_or_set_password( User, Server, Password, IQ, Source, not IsCaptchaEnabled); process_iq(#iq{type = set, to = To, lang = Lang, sub_els = [#register{xdata = #xdata{} = X}]} = IQ, Source, true, _AllowRemove) -> Server = To#jid.lserver, XdataC = xmpp_util:set_xdata_field( #xdata_field{ var = <<"FORM_TYPE">>, type = hidden, values = [?NS_CAPTCHA]}, X), case ejabberd_captcha:process_reply(XdataC) of ok -> case process_xdata_submit(X) of {ok, User, Password} -> try_register_or_set_password( User, Server, Password, IQ, Source, true); _ -> Txt = ?T("Incorrect data form"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; {error, malformed} -> Txt = ?T("Incorrect CAPTCHA submit"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); _ -> ErrText = ?T("The CAPTCHA verification has failed"), xmpp:make_error(IQ, xmpp:err_not_allowed(ErrText, Lang)) end; process_iq(#iq{type = set} = IQ, _Source, _IsCaptchaEnabled, _AllowRemove) -> xmpp:make_error(IQ, xmpp:err_bad_request()); process_iq(#iq{type = get, from = From, to = To, id = ID, lang = Lang} = IQ, Source, IsCaptchaEnabled, _AllowRemove) -> Server = To#jid.lserver, {IsRegistered, Username} = case From of #jid{user = User, lserver = Server} -> case ejabberd_auth:user_exists(User, Server) of true -> {true, User}; false -> {false, User} end; _ -> {false, <<"">>} end, Instr = translate:translate( Lang, ?T("Choose a username and password to register " "with this server")), URL = mod_register_opt:redirect_url(Server), if (URL /= undefined) and not IsRegistered -> Desc = str:translate_and_format(Lang, ?T("To register, visit ~s"), [URL]), xmpp:make_iq_result( IQ, #register{instructions = Desc, sub_els = [#oob_x{url = URL}]}); IsCaptchaEnabled and not IsRegistered -> TopInstr = translate:translate( Lang, ?T("You need a client that supports x:data " "and CAPTCHA to register")), UField = #xdata_field{type = 'text-single', label = translate:translate(Lang, ?T("User")), var = <<"username">>, required = true}, PField = #xdata_field{type = 'text-private', label = translate:translate(Lang, ?T("Password")), var = <<"password">>, required = true}, X = #xdata{type = form, instructions = [Instr], fields = [UField, PField]}, case ejabberd_captcha:create_captcha_x(ID, To, Lang, Source, X) of {ok, CaptchaEls} -> {value, XdataC, CaptchaEls2} = lists:keytake(xdata, 1, CaptchaEls), Xdata = xmpp_util:set_xdata_field( #xdata_field{ var = <<"FORM_TYPE">>, type = hidden, values = [?NS_REGISTER]}, XdataC), xmpp:make_iq_result( IQ, #register{instructions = TopInstr, sub_els = [Xdata | CaptchaEls2]}); {error, limit} -> ErrText = ?T("Too many CAPTCHA requests"), xmpp:make_error( IQ, xmpp:err_resource_constraint(ErrText, Lang)); _Err -> ErrText = ?T("Unable to generate a CAPTCHA"), xmpp:make_error( IQ, xmpp:err_internal_server_error(ErrText, Lang)) end; true -> xmpp:make_iq_result( IQ, #register{instructions = Instr, username = Username, password = <<"">>, registered = IsRegistered}) end. try_register_or_set_password(User, Server, Password, #iq{from = From, lang = Lang} = IQ, Source, CaptchaSucceed) -> case From of #jid{user = User, lserver = Server} -> try_set_password(User, Server, Password, IQ); _ when CaptchaSucceed -> case check_from(From, Server) of allow -> case try_register(User, Server, Password, Source, ?MODULE, Lang) of ok -> xmpp:make_iq_result(IQ); {error, Error} -> xmpp:make_error(IQ, Error) end; deny -> Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; _ -> xmpp:make_error(IQ, xmpp:err_not_allowed()) end. try_set_password(User, Server, Password) -> case is_strong_password(Server, Password) of true -> ejabberd_auth:set_password(User, Server, Password); error_preparing_password -> {error, invalid_password}; false -> {error, weak_password} end. try_set_password(User, Server, Password, #iq{lang = Lang, meta = M} = IQ) -> case try_set_password(User, Server, Password) of ok -> ?INFO_MSG("~ts has changed password from ~ts", [jid:encode({User, Server, <<"">>}), ejabberd_config:may_hide_data( misc:ip_to_list(maps:get(ip, M, {0,0,0,0})))]), xmpp:make_iq_result(IQ); {error, not_allowed} -> Txt = ?T("Changing password is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); {error, invalid_jid = Why} -> xmpp:make_error(IQ, xmpp:err_jid_malformed(format_error(Why), Lang)); {error, invalid_password = Why} -> xmpp:make_error(IQ, xmpp:err_not_allowed(format_error(Why), Lang)); {error, weak_password = Why} -> xmpp:make_error(IQ, xmpp:err_not_acceptable(format_error(Why), Lang)); {error, db_failure = Why} -> xmpp:make_error(IQ, xmpp:err_internal_server_error(format_error(Why), Lang)) end. try_register(User, Server, Password, SourceRaw, Module) -> Modules = mod_register_opt:allow_modules(Server), case (Modules == all) orelse lists:member(Module, Modules) of true -> try_register(User, Server, Password, SourceRaw); false -> {error, eaccess} end. try_register(User, Server, Password, SourceRaw) -> case jid:is_nodename(User) of false -> {error, invalid_jid}; true -> case check_access(User, Server, SourceRaw) of deny -> {error, eaccess}; allow -> Source = may_remove_resource(SourceRaw), case check_timeout(Source) of true -> case is_strong_password(Server, Password) of true -> case ejabberd_auth:try_register( User, Server, Password) of ok -> ok; {error, _} = Err -> remove_timeout(Source), Err end; false -> remove_timeout(Source), {error, weak_password}; error_preparing_password -> remove_timeout(Source), {error, invalid_password} end; false -> {error, wait} end end end. try_register(User, Server, Password, SourceRaw, Module, Lang) -> case try_register(User, Server, Password, SourceRaw, Module) of ok -> JID = jid:make(User, Server), Source = may_remove_resource(SourceRaw), ?INFO_MSG("The account ~ts was registered from IP address ~ts", [jid:encode({User, Server, <<"">>}), ejabberd_config:may_hide_data(ip_to_string(Source))]), send_welcome_message(JID), send_registration_notifications(?MODULE, JID, Source); {error, invalid_jid = Why} -> {error, xmpp:err_jid_malformed(format_error(Why), Lang)}; {error, eaccess = Why} -> {error, xmpp:err_forbidden(format_error(Why), Lang)}; {error, wait = Why} -> {error, xmpp:err_resource_constraint(format_error(Why), Lang)}; {error, weak_password = Why} -> {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; {error, invalid_password = Why} -> {error, xmpp:err_not_acceptable(format_error(Why), Lang)}; {error, not_allowed = Why} -> {error, xmpp:err_not_allowed(format_error(Why), Lang)}; {error, exists = Why} -> {error, xmpp:err_conflict(format_error(Why), Lang)}; {error, db_failure = Why} -> {error, xmpp:err_internal_server_error(format_error(Why), Lang)} end. format_error(invalid_jid) -> ?T("Malformed username"); format_error(eaccess) -> ?T("Access denied by service policy"); format_error(wait) -> ?T("Users are not allowed to register accounts so quickly"); format_error(weak_password) -> ?T("The password is too weak"); format_error(invalid_password) -> ?T("The password contains unacceptable characters"); format_error(not_allowed) -> ?T("Not allowed"); format_error(exists) -> ?T("User already exists"); format_error(db_failure) -> ?T("Database failure"); format_error(Unexpected) -> list_to_binary(io_lib:format(?T("Unexpected error condition: ~p"), [Unexpected])). send_welcome_message(JID) -> Host = JID#jid.lserver, case mod_register_opt:welcome_message(Host) of {<<"">>, <<"">>} -> ok; {Subj, Body} -> ejabberd_router:route( #message{from = jid:make(Host), to = JID, subject = xmpp:mk_text(Subj), body = xmpp:mk_text(Body)}) end. send_registration_notifications(Mod, UJID, Source) -> Host = UJID#jid.lserver, case mod_register_opt:registration_watchers(Host) of [] -> ok; JIDs when is_list(JIDs) -> Body = (str:format("[~s] The account ~s was registered from " "IP address ~s on node ~w using ~p.", [get_time_string(), jid:encode(UJID), ejabberd_config:may_hide_data( ip_to_string(Source)), node(), Mod])), lists:foreach( fun(JID) -> ejabberd_router:route( #message{from = jid:make(Host), to = JID, type = chat, body = xmpp:mk_text(Body)}) end, JIDs) end. check_from(#jid{user = <<"">>, server = <<"">>}, _Server) -> allow; check_from(JID, Server) -> Access = mod_register_opt:access_from(Server), acl:match_rule(Server, Access, JID). check_timeout(undefined) -> true; check_timeout(Source) -> Timeout = ejabberd_option:registration_timeout(), if is_integer(Timeout) -> Priority = -erlang:system_time(millisecond), CleanPriority = Priority + Timeout, F = fun () -> Treap = case mnesia:read(mod_register_ip, treap, write) of [] -> treap:empty(); [{mod_register_ip, treap, T}] -> T end, Treap1 = clean_treap(Treap, CleanPriority), case treap:lookup(Source, Treap1) of error -> Treap2 = treap:insert(Source, Priority, [], Treap1), mnesia:write({mod_register_ip, treap, Treap2}), true; {ok, _, _} -> mnesia:write({mod_register_ip, treap, Treap1}), false end end, case mnesia:transaction(F) of {atomic, Res} -> Res; {aborted, Reason} -> ?ERROR_MSG("timeout check error: ~p~n", [Reason]), true end; true -> true end. clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; false -> {_Key, Priority, _Value} = treap:get_root(Treap), if Priority > CleanPriority -> clean_treap(treap:delete_root(Treap), CleanPriority); true -> Treap end end. remove_timeout(undefined) -> true; remove_timeout(Source) -> Timeout = ejabberd_option:registration_timeout(), if is_integer(Timeout) -> F = fun () -> Treap = case mnesia:read(mod_register_ip, treap, write) of [] -> treap:empty(); [{mod_register_ip, treap, T}] -> T end, Treap1 = treap:delete(Source, Treap), mnesia:write({mod_register_ip, treap, Treap1}), ok end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mod_register: timeout remove error: " "~p~n", [Reason]), ok end; true -> ok end. ip_to_string({_, _, _} = USR) -> jid:encode(USR); ip_to_string(Source) when is_tuple(Source) -> misc:ip_to_list(Source); ip_to_string(undefined) -> <<"undefined">>; ip_to_string(_) -> <<"unknown">>. get_time_string() -> write_time(erlang:localtime()). %% Function copied from ejabberd_logger_h.erl and customized write_time({{Y, Mo, D}, {H, Mi, S}}) -> io_lib:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Y, Mo, D, H, Mi, S]). process_xdata_submit(X) -> case {xmpp_util:get_xdata_values(<<"username">>, X), xmpp_util:get_xdata_values(<<"password">>, X)} of {[User], [Pass]} -> {ok, User, Pass}; _ -> error end. is_strong_password(Server, Password) -> case jid:resourceprep(Password) of PP when is_binary(PP) -> is_strong_password2(Server, Password); error -> error_preparing_password end. is_strong_password2(Server, Password) -> LServer = jid:nameprep(Server), case mod_register_opt:password_strength(LServer) of 0 -> true; Entropy -> ejabberd_auth:entropy(Password) >= Entropy end. %%% %%% ip_access management %%% may_remove_resource({_, _, _} = From) -> jid:remove_resource(From); may_remove_resource(From) -> From. get_ip_access(Host) -> mod_register_opt:ip_access(Host). check_ip_access({User, Server, Resource}, IPAccess) -> case ejabberd_sm:get_user_ip(User, Server, Resource) of {IPAddress, _PortNumber} -> check_ip_access(IPAddress, IPAccess); _ -> deny end; check_ip_access(undefined, _IPAccess) -> deny; check_ip_access(IPAddress, IPAccess) -> acl:match_rule(global, IPAccess, IPAddress). check_access(User, Server, Source) -> JID = jid:make(User, Server), Access = mod_register_opt:access(Server), IPAccess = get_ip_access(Server), case acl:match_rule(Server, Access, JID) of allow -> check_ip_access(Source, IPAccess); deny -> deny end. mod_opt_type(access) -> econf:acl(); mod_opt_type(access_from) -> econf:acl(); mod_opt_type(access_remove) -> econf:acl(); mod_opt_type(allow_modules) -> econf:either(all, econf:list(econf:atom())); mod_opt_type(captcha_protected) -> econf:bool(); mod_opt_type(ip_access) -> econf:acl(); mod_opt_type(password_strength) -> econf:number(0); mod_opt_type(registration_watchers) -> econf:list(econf:jid()); mod_opt_type(welcome_message) -> econf:and_then( econf:options( #{subject => econf:binary(), body => econf:binary()}), fun(Opts) -> {proplists:get_value(subject, Opts, <<>>), proplists:get_value(body, Opts, <<>>)} end); mod_opt_type(redirect_url) -> econf:url(). -spec mod_options(binary()) -> [{welcome_message, {binary(), binary()}} | {atom(), term()}]. mod_options(_Host) -> [{access, all}, {access_from, none}, {access_remove, all}, {allow_modules, all}, {captcha_protected, false}, {ip_access, all}, {password_strength, 0}, {registration_watchers, []}, {redirect_url, undefined}, {welcome_message, {<<>>, <<>>}}]. mod_doc() -> #{desc => [?T("This module adds support for https://xmpp.org/extensions/xep-0077.html" "[XEP-0077: In-Band Registration]. " "This protocol enables end users to use an XMPP client to:"), "", ?T("* Register a new account on the server."), "", ?T("* Change the password from an existing account on the server."), "", ?T("* Delete an existing account on the server."), "", ?T("This module reads also the top-level _`registration_timeout`_ " "option defined globally for the server, " "so please check that option documentation too.")], opts => [{access, #{value => ?T("AccessName"), desc => ?T("Specify rules to restrict what usernames can be registered. " "If a rule returns 'deny' on the requested username, " "registration of that user name is denied. There are no " "restrictions by default.")}}, {access_from, #{value => ?T("AccessName"), desc => ?T("By default, 'ejabberd' doesn't allow to register new accounts " "from s2s or existing c2s sessions. You can change it by defining " "access rule in this option. Use with care: allowing registration " "from s2s leads to uncontrolled massive accounts creation by rogue users.")}}, {access_remove, #{value => ?T("AccessName"), desc => ?T("Specify rules to restrict access for user unregistration. " "By default any user is able to unregister their account.")}}, {allow_modules, #{value => "all | [Module, ...]", note => "added in 21.12", desc => ?T("List of modules that can register accounts, or 'all'. " "The default value is 'all', which is equivalent to " "something like '[mod_register, mod_register_web]'.")}}, {captcha_protected, #{value => "true | false", desc => ?T("Protect registrations with http://../basic/#captcha[CAPTCHA]. " "The default is 'false'.")}}, {ip_access, #{value => ?T("AccessName"), desc => ?T("Define rules to allow or deny account registration depending " "on the IP address of the XMPP client. The 'AccessName' should " "be of type 'ip'. The default value is 'all'.")}}, {password_strength, #{value => "Entropy", desc => ?T("This option sets the minimum " "https://en.wikipedia.org/wiki/Entropy_(information_theory)" "[Shannon entropy] for passwords. The value 'Entropy' is a " "number of bits of entropy. The recommended minimum is 32 bits. " "The default is '0', i.e. no checks are performed.")}}, {registration_watchers, #{value => "[JID, ...]", desc => ?T("This option defines a list of JIDs which will be notified each " "time a new account is registered.")}}, {redirect_url, #{value => ?T("URL"), desc => ?T("This option enables registration redirection as described in " "https://xmpp.org/extensions/xep-0077.html#redirect" "[XEP-0077: In-Band Registration: Redirection].")}}, {welcome_message, #{value => "{subject: Subject, body: Body}", desc => ?T("Set a welcome message that is sent to each newly registered account. " "The message will have subject 'Subject' and text 'Body'.")}}]}. ejabberd-21.12/src/ejabberd_cluster.erl0000644000232200023220000001652314154362354020433 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 5 Jul 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_cluster). -behaviour(gen_server). %% API -export([start_link/0, call/4, call/5, multicall/3, multicall/4, multicall/5, eval_everywhere/3, eval_everywhere/4]). %% Backend dependent API -export([get_nodes/0, get_known_nodes/0, join/1, leave/1, subscribe/0, subscribe/1, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% hooks -export([set_ticktime/0]). -include("logger.hrl"). -type dst() :: pid() | atom() | {atom(), node()}. -callback init() -> ok | {error, any()}. -callback get_nodes() -> [node()]. -callback get_known_nodes() -> [node()]. -callback join(node()) -> ok | {error, any()}. -callback leave(node()) -> ok | {error, any()}. -callback node_id() -> binary(). -callback get_node_by_id(binary()) -> node(). -callback send({atom(), node()}, term()) -> boolean(). -callback wait_for_sync(timeout()) -> ok | {error, any()}. -callback subscribe(dst()) -> ok. -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec call(node(), module(), atom(), [any()]) -> any(). call(Node, Module, Function, Args) -> call(Node, Module, Function, Args, rpc_timeout()). -spec call(node(), module(), atom(), [any()], timeout()) -> any(). call(Node, Module, Function, Args, Timeout) -> rpc:call(Node, Module, Function, Args, Timeout). -spec multicall(module(), atom(), [any()]) -> {list(), [node()]}. multicall(Module, Function, Args) -> multicall(get_nodes(), Module, Function, Args). -spec multicall([node()], module(), atom(), list()) -> {list(), [node()]}. multicall(Nodes, Module, Function, Args) -> multicall(Nodes, Module, Function, Args, rpc_timeout()). -spec multicall([node()], module(), atom(), list(), timeout()) -> {list(), [node()]}. multicall(Nodes, Module, Function, Args, Timeout) -> rpc:multicall(Nodes, Module, Function, Args, Timeout). -spec eval_everywhere(module(), atom(), [any()]) -> ok. eval_everywhere(Module, Function, Args) -> eval_everywhere(get_nodes(), Module, Function, Args), ok. -spec eval_everywhere([node()], module(), atom(), [any()]) -> ok. eval_everywhere(Nodes, Module, Function, Args) -> rpc:eval_everywhere(Nodes, Module, Function, Args), ok. %%%=================================================================== %%% Backend dependent API %%%=================================================================== -spec get_nodes() -> [node()]. get_nodes() -> Mod = get_mod(), Mod:get_nodes(). -spec get_known_nodes() -> [node()]. get_known_nodes() -> Mod = get_mod(), Mod:get_known_nodes(). -spec join(node()) -> ok | {error, any()}. join(Node) -> Mod = get_mod(), Mod:join(Node). -spec leave(node()) -> ok | {error, any()}. leave(Node) -> Mod = get_mod(), Mod:leave(Node). -spec node_id() -> binary(). node_id() -> Mod = get_mod(), Mod:node_id(). -spec get_node_by_id(binary()) -> node(). get_node_by_id(ID) -> Mod = get_mod(), Mod:get_node_by_id(ID). %% Note that false positive returns are possible, while false negatives are not. %% In other words: positive return value (i.e. 'true') doesn't guarantee %% successful delivery, while negative return value ('false') means %% the delivery has definitely failed. -spec send(dst(), term()) -> boolean(). send({Name, Node}, Msg) when Node == node() -> send(Name, Msg); send(undefined, _Msg) -> false; send(Name, Msg) when is_atom(Name) -> send(whereis(Name), Msg); send(Pid, Msg) when is_pid(Pid) andalso node(Pid) == node() -> case erlang:is_process_alive(Pid) of true -> erlang:send(Pid, Msg), true; false -> false end; send(Dst, Msg) -> Mod = get_mod(), Mod:send(Dst, Msg). -spec wait_for_sync(timeout()) -> ok | {error, any()}. wait_for_sync(Timeout) -> Mod = get_mod(), Mod:wait_for_sync(Timeout). -spec subscribe() -> ok. subscribe() -> subscribe(self()). -spec subscribe(dst()) -> ok. subscribe(Proc) -> Mod = get_mod(), Mod:subscribe(Proc). %%%=================================================================== %%% Hooks %%%=================================================================== set_ticktime() -> Ticktime = ejabberd_option:net_ticktime() div 1000, case net_kernel:set_net_ticktime(Ticktime) of {ongoing_change_to, Time} when Time /= Ticktime -> ?ERROR_MSG("Failed to set new net_ticktime because " "the net kernel is busy changing it to the " "previously configured value. Please wait for " "~B seconds and retry", [Time]); _ -> ok end. %%%=================================================================== %%% gen_server API %%%=================================================================== init([]) -> set_ticktime(), Nodes = ejabberd_option:cluster_nodes(), lists:foreach(fun(Node) -> net_kernel:connect_node(Node) end, Nodes), Mod = get_mod(), case Mod:init() of ok -> ejabberd_hooks:add(config_reloaded, ?MODULE, set_ticktime, 50), Mod:subscribe(?MODULE), {ok, #state{}}; {error, Reason} -> {stop, Reason} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({node_up, Node}, State) -> ?INFO_MSG("Node ~ts has joined", [Node]), {noreply, State}; handle_info({node_down, Node}, State) -> ?INFO_MSG("Node ~ts has left", [Node]), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, set_ticktime, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== get_mod() -> Backend = ejabberd_option:cluster_backend(), list_to_existing_atom("ejabberd_cluster_" ++ atom_to_list(Backend)). rpc_timeout() -> ejabberd_option:rpc_timeout(). ejabberd-21.12/src/mod_caps_sql.erl0000644000232200023220000000613214154362354017573 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_caps_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_caps_sql). -behaviour(mod_caps). %% API -export([init/2, caps_read/2, caps_write/3, export/1, import/3]). -include("mod_caps.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. caps_read(LServer, {Node, SubNode}) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(feature)s from caps_features where" " node=%(Node)s and subnode=%(SubNode)s")) of {selected, [{H}|_] = Fs} -> case catch binary_to_integer(H) of Int when is_integer(Int), Int>=0 -> {ok, Int}; _ -> {ok, [F || {F} <- Fs]} end; _ -> error end. caps_write(LServer, NodePair, Features) -> case ejabberd_sql:sql_transaction( LServer, sql_write_features_t(NodePair, Features)) of {atomic, _} -> ok; {aborted, _Reason} -> {error, db_failure} end. export(_Server) -> [{caps_features, fun(_Host, #caps_features{node_pair = NodePair, features = Features}) -> sql_write_features_t(NodePair, Features); (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== sql_write_features_t({Node, SubNode}, Features) -> NewFeatures = if is_integer(Features) -> [integer_to_binary(Features)]; true -> Features end, [?SQL("delete from caps_features where node=%(Node)s" " and subnode=%(SubNode)s;") | [?SQL("insert into caps_features(node, subnode, feature)" " values (%(Node)s, %(SubNode)s, %(F)s);") || F <- NewFeatures]]. ejabberd-21.12/src/mod_push_mnesia.erl0000644000232200023220000001511614154362354020303 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_push_mnesia.erl %%% Author : Holger Weiss %%% Purpose : Mnesia backend for Push Notifications (XEP-0357) %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_push_mnesia). -author('holger@zedat.fu-berlin.de'). -behaviour(mod_push). %% API -export([init/2, store_session/6, lookup_session/4, lookup_session/3, lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, delete_session/3, delete_old_sessions/2, transform/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_push.hrl"). %%%------------------------------------------------------------------- %%% API %%%------------------------------------------------------------------- init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, push_session, [{disc_only_copies, [node()]}, {type, bag}, {attributes, record_info(fields, push_session)}]). store_session(LUser, LServer, TS, PushJID, Node, XData) -> US = {LUser, LServer}, PushLJID = jid:tolower(PushJID), MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), F = fun() -> enforce_max_sessions(US, MaxSessions), mnesia:write(#push_session{us = US, timestamp = TS, service = PushLJID, node = Node, xml = encode_xdata(XData)}) end, case mnesia:transaction(F) of {atomic, ok} -> {ok, {TS, PushLJID, Node, XData}}; {aborted, E} -> ?ERROR_MSG("Cannot store push session for ~ts@~ts: ~p", [LUser, LServer, E]), {error, db_failure} end. lookup_session(LUser, LServer, PushJID, Node) -> PushLJID = jid:tolower(PushJID), MatchSpec = ets:fun2ms( fun(#push_session{us = {U, S}, service = P, node = N} = Rec) when U == LUser, S == LServer, P == PushLJID, N == Node -> Rec end), case mnesia:dirty_select(push_session, MatchSpec) of [#push_session{timestamp = TS, xml = El}] -> {ok, {TS, PushLJID, Node, decode_xdata(El)}}; [] -> ?DEBUG("No push session found for ~ts@~ts (~p, ~ts)", [LUser, LServer, PushJID, Node]), {error, notfound} end. lookup_session(LUser, LServer, TS) -> MatchSpec = ets:fun2ms( fun(#push_session{us = {U, S}, timestamp = T} = Rec) when U == LUser, S == LServer, T == TS -> Rec end), case mnesia:dirty_select(push_session, MatchSpec) of [#push_session{service = PushLJID, node = Node, xml = El}] -> {ok, {TS, PushLJID, Node, decode_xdata(El)}}; [] -> ?DEBUG("No push session found for ~ts@~ts (~p)", [LUser, LServer, TS]), {error, notfound} end. lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), MatchSpec = ets:fun2ms( fun(#push_session{us = {U, S}, service = P} = Rec) when U == LUser, S == LServer, P == PushLJID -> Rec end), Records = mnesia:dirty_select(push_session, MatchSpec), {ok, records_to_sessions(Records)}. lookup_sessions(LUser, LServer) -> Records = mnesia:dirty_read(push_session, {LUser, LServer}), {ok, records_to_sessions(Records)}. lookup_sessions(LServer) -> MatchSpec = ets:fun2ms( fun(#push_session{us = {_U, S}} = Rec) when S == LServer -> Rec end), Records = mnesia:dirty_select(push_session, MatchSpec), {ok, records_to_sessions(Records)}. delete_session(LUser, LServer, TS) -> MatchSpec = ets:fun2ms( fun(#push_session{us = {U, S}, timestamp = T} = Rec) when U == LUser, S == LServer, T == TS -> Rec end), F = fun() -> Recs = mnesia:select(push_session, MatchSpec), lists:foreach(fun mnesia:delete_object/1, Recs) end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, E} -> ?ERROR_MSG("Cannot delete push session of ~ts@~ts: ~p", [LUser, LServer, E]), {error, db_failure} end. delete_old_sessions(_LServer, Time) -> DelIfOld = fun(#push_session{timestamp = T} = Rec, ok) when T < Time -> mnesia:delete_object(Rec); (_Rec, ok) -> ok end, F = fun() -> mnesia:foldl(DelIfOld, ok, push_session) end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, E} -> ?ERROR_MSG("Cannot delete old push sessions: ~p", [E]), {error, db_failure} end. transform({push_session, US, TS, Service, Node, XData}) -> ?INFO_MSG("Transforming push_session Mnesia table", []), #push_session{us = US, timestamp = TS, service = Service, node = Node, xml = encode_xdata(XData)}. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec enforce_max_sessions({binary(), binary()}, non_neg_integer() | infinity) -> ok. enforce_max_sessions(_US, infinity) -> ok; enforce_max_sessions({U, S} = US, MaxSessions) -> case mnesia:wread({push_session, US}) of Recs when length(Recs) >= MaxSessions -> Recs1 = lists:sort(fun(#push_session{timestamp = TS1}, #push_session{timestamp = TS2}) -> TS1 >= TS2 end, Recs), OldRecs = lists:nthtail(MaxSessions - 1, Recs1), ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [U, S]), lists:foreach(fun(Rec) -> mnesia:delete_object(Rec) end, OldRecs); _ -> ok end. decode_xdata(undefined) -> undefined; decode_xdata(El) -> xmpp:decode(El). encode_xdata(undefined) -> undefined; encode_xdata(XData) -> xmpp:encode(XData). records_to_sessions(Records) -> [{TS, PushLJID, Node, decode_xdata(El)} || #push_session{timestamp = TS, service = PushLJID, node = Node, xml = El} <- Records]. ejabberd-21.12/src/mod_vcard_xupdate_opt.erl0000644000232200023220000000225314154362354021501 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_vcard_xupdate_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, cache_size). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_vcard_xupdate, use_cache). ejabberd-21.12/src/mod_mix_pam_sql.erl0000644000232200023220000000724714154362354020307 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 4 Dec 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix_pam_sql). -behaviour(mod_mix_pam). %% API -export([init/2, add_channel/3, get_channel/2, get_channels/1, del_channel/2, del_channels/1]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> %% TODO ok. add_channel(User, Channel, ID) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case ?SQL_UPSERT(LServer, "mix_pam", ["!channel=%(Chan)s", "!service=%(Service)s", "!username=%(LUser)s", "!server_host=%(LServer)s", "id=%(ID)s"]) of ok -> ok; _Err -> {error, db_failure} end. get_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case ejabberd_sql:sql_query( LServer, ?SQL("select @(id)s from mix_pam where " "channel=%(Chan)s and service=%(Service)s " "and username=%(LUser)s and %(LServer)H")) of {selected, [{ID}]} -> {ok, ID}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. get_channels(User) -> {LUser, LServer, _} = jid:tolower(User), SQL = ?SQL("select @(channel)s, @(service)s, @(id)s from mix_pam " "where username=%(LUser)s and %(LServer)H"), case ejabberd_sql:sql_query(LServer, SQL) of {selected, Ret} -> {ok, lists:filtermap( fun({Chan, Service, ID}) -> case jid:make(Chan, Service) of error -> report_corrupted(SQL), false; JID -> {true, {JID, ID}} end end, Ret)}; _Err -> {error, db_failure} end. del_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case ejabberd_sql:sql_query( LServer, ?SQL("delete from mix_pam where " "channel=%(Chan)s and service=%(Service)s " "and username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; _Err -> {error, db_failure} end. del_channels(User) -> {LUser, LServer, _} = jid:tolower(User), case ejabberd_sql:sql_query( LServer, ?SQL("delete from mix_pam where " "username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; _Err -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec report_corrupted(#sql_query{}) -> ok. report_corrupted(SQL) -> ?ERROR_MSG("Corrupted values returned by SQL request: ~ts", [SQL#sql_query.hash]). ejabberd-21.12/src/mod_avatar.erl0000644000232200023220000004075114154362354017251 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 13 Sep 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_avatar). -behaviour(gen_mod). -protocol({xep, 398, '0.2.0'}). %% gen_mod API -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% Hooks -export([pubsub_publish_item/6, vcard_iq_convert/1, vcard_iq_publish/1, get_sm_features/5]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("pubsub.hrl"). -include("translate.hrl"). -type avatar_id_meta() :: #{avatar_meta => {binary(), avatar_meta()}}. -opaque convert_rule() :: {default | eimp:img_type(), eimp:img_type()}. -export_type([convert_rule/0]). %%%=================================================================== %%% API %%%=================================================================== start(Host, _Opts) -> ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30), ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50). stop(Host) -> ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_convert, 30), ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_iq_publish, 100), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_vcard, hard}, {mod_vcard_xupdate, hard}, {mod_pubsub, hard}]. %%%=================================================================== %%% Hooks %%%=================================================================== -spec pubsub_publish_item(binary(), binary(), jid(), jid(), binary(), [xmlel()]) -> ok. pubsub_publish_item(LServer, ?NS_AVATAR_METADATA, #jid{luser = LUser, lserver = LServer} = From, #jid{luser = LUser, lserver = LServer} = Host, ItemId, [Payload|_]) -> try xmpp:decode(Payload) of #avatar_meta{info = []} -> delete_vcard_avatar(From); #avatar_meta{info = Info} -> Rules = mod_avatar_opt:convert(LServer), case get_meta_info(Info, Rules) of #avatar_info{type = MimeType, id = ID, url = <<"">>} = I -> case get_avatar_data(Host, ID) of {ok, Data} -> Meta = #avatar_meta{info = [I]}, Photo = #vcard_photo{type = MimeType, binval = Data}, set_vcard_avatar(From, Photo, #{avatar_meta => {ID, Meta}}); {error, _} -> ok end; #avatar_info{type = MimeType, url = URL} -> Photo = #vcard_photo{type = MimeType, extval = URL}, set_vcard_avatar(From, Photo, #{}) end; _ -> ?WARNING_MSG("Invalid avatar metadata of ~ts@~ts published " "with item id ~ts", [LUser, LServer, ItemId]) catch _:{xmpp_codec, Why} -> ?WARNING_MSG("Failed to decode avatar metadata of ~ts@~ts: ~ts", [LUser, LServer, xmpp:format_error(Why)]) end; pubsub_publish_item(_, _, _, _, _, _) -> ok. -spec vcard_iq_convert(iq()) -> iq() | {stop, stanza_error()}. vcard_iq_convert(#iq{from = From, lang = Lang, sub_els = [VCard]} = IQ) -> #jid{luser = LUser, lserver = LServer} = From, case convert_avatar(LUser, LServer, VCard) of {ok, MimeType, Data} -> VCard1 = VCard#vcard_temp{ photo = #vcard_photo{type = MimeType, binval = Data}}, IQ#iq{sub_els = [VCard1]}; pass -> IQ; {error, Reason} -> stop_with_error(Lang, Reason) end; vcard_iq_convert(Acc) -> Acc. -spec vcard_iq_publish(iq()) -> iq() | {stop, stanza_error()}. vcard_iq_publish(#iq{sub_els = [#vcard_temp{photo = undefined}]} = IQ) -> publish_avatar(IQ, #avatar_meta{}, <<>>, <<>>, <<>>); vcard_iq_publish(#iq{sub_els = [#vcard_temp{ photo = #vcard_photo{ type = MimeType, binval = Data}}]} = IQ) when is_binary(Data), Data /= <<>> -> SHA1 = str:sha(Data), M = get_avatar_meta(IQ), case M of {ok, SHA1, _} -> IQ; {ok, _ItemID, #avatar_meta{info = Info} = Meta} -> case lists:keyfind(SHA1, #avatar_info.id, Info) of #avatar_info{} -> IQ; false -> Info1 = lists:filter( fun(#avatar_info{url = URL}) -> URL /= <<"">> end, Info), Meta1 = Meta#avatar_meta{info = Info1}, publish_avatar(IQ, Meta1, MimeType, Data, SHA1) end; {error, _} -> publish_avatar(IQ, #avatar_meta{}, MimeType, Data, SHA1) end; vcard_iq_publish(Acc) -> Acc. -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | empty | {result, [binary()]}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_features(Acc, _From, _To, <<"">>, _Lang) -> {result, [?NS_PEP_VCARD_CONVERSION_0 | case Acc of {result, Features} -> Features; empty -> [] end]}; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_meta_info([avatar_info()], [convert_rule()]) -> avatar_info(). get_meta_info(Info, Rules) -> case lists:foldl( fun(_, #avatar_info{} = Acc) -> Acc; (#avatar_info{url = URL}, Acc) when URL /= <<"">> -> Acc; (#avatar_info{} = I, _) when Rules == [] -> I; (#avatar_info{type = MimeType} = I, Acc) -> T = decode_mime_type(MimeType), case lists:keymember(T, 2, Rules) of true -> I; false -> case convert_to_type(T, Rules) of undefined -> Acc; _ -> [I|Acc] end end end, [], Info) of #avatar_info{} = I -> I; [] -> hd(Info); Is -> hd(lists:reverse(Is)) end. -spec get_avatar_data(jid(), binary()) -> {ok, binary()} | {error, notfound | invalid_data | internal_error}. get_avatar_data(JID, ItemID) -> {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case mod_pubsub:get_item(LBJID, ?NS_AVATAR_DATA, ItemID) of #pubsub_item{payload = [Payload|_]} -> try xmpp:decode(Payload) of #avatar_data{data = Data} -> {ok, Data}; _ -> ?WARNING_MSG("Invalid avatar data detected " "for ~ts@~ts with item id ~ts", [LUser, LServer, ItemID]), {error, invalid_data} catch _:{xmpp_codec, Why} -> ?WARNING_MSG("Failed to decode avatar data for " "~ts@~ts with item id ~ts: ~ts", [LUser, LServer, ItemID, xmpp:format_error(Why)]), {error, invalid_data} end; #pubsub_item{payload = []} -> ?WARNING_MSG("Empty avatar data detected " "for ~ts@~ts with item id ~ts", [LUser, LServer, ItemID]), {error, invalid_data}; {error, #stanza_error{reason = 'item-not-found'}} -> {error, notfound}; {error, Reason} -> ?WARNING_MSG("Failed to get item for ~ts@~ts at node ~ts " "with item id ~ts: ~p", [LUser, LServer, ?NS_AVATAR_METADATA, ItemID, Reason]), {error, internal_error} end. -spec get_avatar_meta(iq()) -> {ok, binary(), avatar_meta()} | {error, notfound | invalid_metadata | internal_error}. get_avatar_meta(#iq{meta = #{avatar_meta := {ItemID, Meta}}}) -> {ok, ItemID, Meta}; get_avatar_meta(#iq{from = JID}) -> {LUser, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case mod_pubsub:get_items(LBJID, ?NS_AVATAR_METADATA) of [#pubsub_item{itemid = {ItemID, _}, payload = [Payload|_]}|_] -> try xmpp:decode(Payload) of #avatar_meta{} = Meta -> {ok, ItemID, Meta}; _ -> ?WARNING_MSG("Invalid metadata payload detected " "for ~ts@~ts with item id ~ts", [LUser, LServer, ItemID]), {error, invalid_metadata} catch _:{xmpp_codec, Why} -> ?WARNING_MSG("Failed to decode metadata for " "~ts@~ts with item id ~ts: ~ts", [LUser, LServer, ItemID, xmpp:format_error(Why)]), {error, invalid_metadata} end; {error, #stanza_error{reason = 'item-not-found'}} -> {error, notfound}; {error, Reason} -> ?WARNING_MSG("Failed to get items for ~ts@~ts at node ~ts: ~p", [LUser, LServer, ?NS_AVATAR_METADATA, Reason]), {error, internal_error} end. -spec publish_avatar(iq(), avatar_meta(), binary(), binary(), binary()) -> iq() | {stop, stanza_error()}. publish_avatar(#iq{from = JID} = IQ, Meta, <<>>, <<>>, <<>>) -> {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case mod_pubsub:publish_item( LBJID, LServer, ?NS_AVATAR_METADATA, JID, <<>>, [xmpp:encode(Meta)]) of {result, _} -> IQ; {error, StanzaErr} -> {stop, StanzaErr} end; publish_avatar(#iq{from = JID} = IQ, Meta, MimeType, Data, ItemID) -> #avatar_meta{info = Info} = Meta, {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), Payload = xmpp:encode(#avatar_data{data = Data}), case mod_pubsub:publish_item( LBJID, LServer, ?NS_AVATAR_DATA, JID, ItemID, [Payload]) of {result, _} -> {W, H} = case eimp:identify(Data) of {ok, ImgInfo} -> {proplists:get_value(width, ImgInfo), proplists:get_value(height, ImgInfo)}; _ -> {undefined, undefined} end, I = #avatar_info{id = ItemID, width = W, height = H, type = MimeType, bytes = size(Data)}, Meta1 = Meta#avatar_meta{info = [I|Info]}, case mod_pubsub:publish_item( LBJID, LServer, ?NS_AVATAR_METADATA, JID, ItemID, [xmpp:encode(Meta1)]) of {result, _} -> IQ; {error, StanzaErr} -> ?ERROR_MSG("Failed to publish avatar metadata for ~ts: ~p", [jid:encode(JID), StanzaErr]), {stop, StanzaErr} end; {error, #stanza_error{reason = 'not-acceptable'} = StanzaErr} -> ?WARNING_MSG("Failed to publish avatar data for ~ts: ~p", [jid:encode(JID), StanzaErr]), {stop, StanzaErr}; {error, StanzaErr} -> ?ERROR_MSG("Failed to publish avatar data for ~ts: ~p", [jid:encode(JID), StanzaErr]), {stop, StanzaErr} end. -spec convert_avatar(binary(), binary(), vcard_temp()) -> {ok, binary(), binary()} | {error, eimp:error_reason() | base64_error} | pass. convert_avatar(LUser, LServer, VCard) -> case mod_avatar_opt:convert(LServer) of [] -> pass; Rules -> case VCard#vcard_temp.photo of #vcard_photo{binval = Data} when is_binary(Data) -> convert_avatar(LUser, LServer, Data, Rules); _ -> pass end end. -spec convert_avatar(binary(), binary(), binary(), [convert_rule()]) -> {ok, binary(), binary()} | {error, eimp:error_reason()} | pass. convert_avatar(LUser, LServer, Data, Rules) -> Type = get_type(Data), NewType = convert_to_type(Type, Rules), if NewType == undefined -> pass; true -> ?DEBUG("Converting avatar of ~ts@~ts: ~ts -> ~ts", [LUser, LServer, Type, NewType]), RateLimit = mod_avatar_opt:rate_limit(LServer), Opts = [{limit_by, {LUser, LServer}}, {rate_limit, RateLimit}], case eimp:convert(Data, NewType, Opts) of {ok, NewData} -> {ok, encode_mime_type(NewType), NewData}; {error, Reason} = Err -> ?ERROR_MSG("Failed to convert avatar of " "~ts@~ts (~ts -> ~ts): ~ts", [LUser, LServer, Type, NewType, eimp:format_error(Reason)]), Err end end. -spec set_vcard_avatar(jid(), vcard_photo() | undefined, avatar_id_meta()) -> ok. set_vcard_avatar(JID, VCardPhoto, Meta) -> case get_vcard(JID) of {ok, #vcard_temp{photo = VCardPhoto}} -> ok; {ok, VCard} -> VCard1 = VCard#vcard_temp{photo = VCardPhoto}, IQ = #iq{from = JID, to = JID, id = p1_rand:get_string(), type = set, sub_els = [VCard1], meta = Meta}, LServer = JID#jid.lserver, ejabberd_hooks:run_fold(vcard_iq_set, LServer, IQ, []), ok; {error, _} -> ok end. -spec delete_vcard_avatar(jid()) -> ok. delete_vcard_avatar(JID) -> set_vcard_avatar(JID, undefined, #{}). -spec get_vcard(jid()) -> {ok, vcard_temp()} | {error, invalid_vcard}. get_vcard(#jid{luser = LUser, lserver = LServer}) -> VCardEl = case mod_vcard:get_vcard(LUser, LServer) of [El] -> El; _ -> #vcard_temp{} end, try xmpp:decode(VCardEl, ?NS_VCARD, []) of #vcard_temp{} = VCard -> {ok, VCard}; _ -> ?ERROR_MSG("Invalid vCard of ~ts@~ts in the database", [LUser, LServer]), {error, invalid_vcard} catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode vCard of ~ts@~ts: ~ts", [LUser, LServer, xmpp:format_error(Why)]), {error, invalid_vcard} end. -spec stop_with_error(binary(), eimp:error_reason()) -> {stop, stanza_error()}. stop_with_error(Lang, Reason) -> Txt = eimp:format_error(Reason), {stop, xmpp:err_internal_server_error(Txt, Lang)}. -spec get_type(binary()) -> eimp:img_type() | unknown. get_type(Data) -> eimp:get_type(Data). -spec convert_to_type(eimp:img_type() | unknown, [convert_rule()]) -> eimp:img_type() | undefined. convert_to_type(unknown, _Rules) -> undefined; convert_to_type(Type, Rules) -> case proplists:get_value(Type, Rules) of undefined -> proplists:get_value(default, Rules); Type -> undefined; T -> T end. -spec decode_mime_type(binary()) -> eimp:img_type() | unknown. decode_mime_type(MimeType) -> case str:to_lower(MimeType) of <<"image/jpeg">> -> jpeg; <<"image/png">> -> png; <<"image/webp">> -> webp; <<"image/gif">> -> gif; _ -> unknown end. -spec encode_mime_type(eimp:img_type()) -> binary(). encode_mime_type(Type) -> <<"image/", (atom_to_binary(Type, latin1))/binary>>. mod_opt_type(convert) -> case eimp:supported_formats() of [] -> fun(_) -> econf:fail(eimp_error) end; Formats -> econf:options( maps:from_list( [{Type, econf:enum(Formats)} || Type <- [default|Formats]])) end; mod_opt_type(rate_limit) -> econf:pos_int(). -spec mod_options(binary()) -> [{convert, [?MODULE:convert_rule()]} | {atom(), any()}]. mod_options(_) -> [{rate_limit, 10}, {convert, []}]. mod_doc() -> #{desc => [?T("The purpose of the module is to cope with legacy and modern " "XMPP clients posting avatars. The process is described in " "https://xmpp.org/extensions/xep-0398.html" "[XEP-0398: User Avatar to vCard-Based Avatars Conversion]."), "", ?T("Also, the module supports conversion between avatar " "image formats on the fly."), "", ?T("The module depends on _`mod_vcard`_, _`mod_vcard_xupdate`_ and " "_`mod_pubsub`_.")], opts => [{convert, #{value => "{From: To}", desc => ?T("Defines image convertion rules: the format in 'From' " "will be converted to format in 'To'. The value of 'From' " "can also be 'default', which is match-all rule. NOTE: " "the list of supported formats is detected at compile time " "depending on the image libraries installed in the system."), example => ["convert:", " webp: jpg", " default: png"]}}, {rate_limit, #{value => ?T("Number"), desc => ?T("Limit any given JID by the number of avatars it is able " "to convert per minute. This is to protect the server from " "image convertion DoS. The default value is '10'.")}}]}. ejabberd-21.12/src/ejabberd_commands.erl0000644000232200023220000002700014154362354020543 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_commands.erl %%% Author : Badlop %%% Purpose : Management of ejabberd commands %%% Created : 20 May 2008 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_commands). -author('badlop@process-one.net'). -behaviour(gen_server). -define(DEFAULT_VERSION, 1000000). -export([start_link/0, list_commands/0, list_commands/1, get_command_format/1, get_command_format/2, get_command_format/3, get_command_definition/1, get_command_definition/2, get_tags_commands/0, get_tags_commands/1, register_commands/1, register_commands/2, unregister_commands/1, get_commands_spec/0, get_commands_definition/0, get_commands_definition/1, execute_command2/3, execute_command2/4]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -define(POLICY_ACCESS, '$policy'). -type auth() :: {binary(), binary(), binary() | {oauth, binary()}, boolean()} | map(). -record(state, {}). get_commands_spec() -> [ #ejabberd_commands{name = gen_html_doc_for_commands, tags = [documentation], desc = "Generates html documentation for ejabberd_commands", module = ejabberd_commands_doc, function = generate_html_output, args = [{file, binary}, {regexp, binary}, {examples, binary}], result = {res, rescode}, args_desc = ["Path to file where generated " "documentation should be stored", "Regexp matching names of commands or modules " "that will be included inside generated document", "Comma separated list of languages (chosen from java, perl, xmlrpc, json)" "that will have example invocation include in markdown document"], result_desc = "0 if command failed, 1 when succeeded", args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], result_example = ok}, #ejabberd_commands{name = gen_markdown_doc_for_commands, tags = [documentation], desc = "Generates markdown documentation for ejabberd_commands", module = ejabberd_commands_doc, function = generate_md_output, args = [{file, binary}, {regexp, binary}, {examples, binary}], result = {res, rescode}, args_desc = ["Path to file where generated " "documentation should be stored", "Regexp matching names of commands or modules " "that will be included inside generated document", "Comma separated list of languages (chosen from java, perl, xmlrpc, json)" "that will have example invocation include in markdown document"], result_desc = "0 if command failed, 1 when succeeded", args_example = ["/home/me/docs/api.html", "mod_admin", "java,json"], result_example = ok}, #ejabberd_commands{name = gen_markdown_doc_for_tags, tags = [documentation], desc = "Generates markdown documentation for ejabberd_commands", note = "added in 21.12", module = ejabberd_commands_doc, function = generate_tags_md, args = [{file, binary}], result = {res, rescode}, args_desc = ["Path to file where generated " "documentation should be stored"], result_desc = "0 if command failed, 1 when succeeded", args_example = ["/home/me/docs/tags.md"], result_example = ok}]. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> try mnesia:transform_table(ejabberd_commands, ignore, record_info(fields, ejabberd_commands)) catch exit:{aborted, {no_exists, _}} -> ok end, ejabberd_mnesia:create(?MODULE, ejabberd_commands, [{ram_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, ejabberd_commands)}, {type, bag}]), register_commands(get_commands_spec()), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec register_commands([ejabberd_commands()]) -> ok. register_commands(Commands) -> register_commands(unknown, Commands). register_commands(Definer, Commands) -> lists:foreach( fun(Command) -> %% XXX check if command exists mnesia:dirty_write(Command#ejabberd_commands{definer = Definer}) %% ?DEBUG("This command is already defined:~n~p", [Command]) end, Commands), ejabberd_access_permissions:invalidate(), ok. -spec unregister_commands([ejabberd_commands()]) -> ok. unregister_commands(Commands) -> lists:foreach( fun(Command) -> mnesia:dirty_delete_object(Command) end, Commands), ejabberd_access_permissions:invalidate(). -spec list_commands() -> [{atom(), [aterm()], string()}]. list_commands() -> list_commands(?DEFAULT_VERSION). -spec list_commands(integer()) -> [{atom(), [aterm()], string()}]. list_commands(Version) -> Commands = get_commands_definition(Version), [{Name, Args, Desc} || #ejabberd_commands{name = Name, args = Args, desc = Desc} <- Commands]. -spec get_command_format(atom()) -> {[aterm()], [{atom(),atom()}], rterm()}. get_command_format(Name) -> get_command_format(Name, noauth, ?DEFAULT_VERSION). get_command_format(Name, Version) when is_integer(Version) -> get_command_format(Name, noauth, Version); get_command_format(Name, Auth) -> get_command_format(Name, Auth, ?DEFAULT_VERSION). -spec get_command_format(atom(), noauth | admin | auth(), integer()) -> {[aterm()], [{atom(),atom()}], rterm()}. get_command_format(Name, Auth, Version) -> Admin = is_admin(Name, Auth, #{}), #ejabberd_commands{args = Args, result = Result, args_rename = Rename, policy = Policy} = get_command_definition(Name, Version), case Policy of user when Admin; Auth == noauth -> {[{user, binary}, {host, binary} | Args], Rename, Result}; _ -> {Args, Rename, Result} end. -spec get_command_definition(atom()) -> ejabberd_commands(). get_command_definition(Name) -> get_command_definition(Name, ?DEFAULT_VERSION). -spec get_command_definition(atom(), integer()) -> ejabberd_commands(). get_command_definition(Name, Version) -> case lists:reverse( lists:sort( mnesia:dirty_select( ejabberd_commands, ets:fun2ms( fun(#ejabberd_commands{name = N, version = V} = C) when N == Name, V =< Version -> {V, C} end)))) of [{_, Command} | _ ] -> Command; _E -> throw({error, unknown_command}) end. get_commands_definition() -> get_commands_definition(?DEFAULT_VERSION). -spec get_commands_definition(integer()) -> [ejabberd_commands()]. get_commands_definition(Version) -> L = lists:reverse( lists:sort( mnesia:dirty_select( ejabberd_commands, ets:fun2ms( fun(#ejabberd_commands{name = Name, version = V} = C) when V =< Version -> {Name, V, C} end)))), F = fun({_Name, _V, Command}, []) -> [Command]; ({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) -> Acc; ({_Name, _V, Command}, Acc) -> [Command | Acc] end, lists:foldl(F, [], L). execute_command2(Name, Arguments, CallerInfo) -> execute_command2(Name, Arguments, CallerInfo, ?DEFAULT_VERSION). execute_command2(Name, Arguments, CallerInfo, Version) -> Command = get_command_definition(Name, Version), case ejabberd_access_permissions:can_access(Name, CallerInfo) of allow -> do_execute_command(Command, Arguments); _ -> throw({error, access_rules_unauthorized}) end. do_execute_command(Command, Arguments) -> Module = Command#ejabberd_commands.module, Function = Command#ejabberd_commands.function, ?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]), ejabberd_hooks:run(api_call, [Module, Function, Arguments]), apply(Module, Function, Arguments). -spec get_tags_commands() -> [{string(), [string()]}]. get_tags_commands() -> get_tags_commands(?DEFAULT_VERSION). -spec get_tags_commands(integer()) -> [{string(), [string()]}]. get_tags_commands(Version) -> CommandTags = [{Name, Tags} || #ejabberd_commands{name = Name, tags = Tags} <- get_commands_definition(Version)], Dict = lists:foldl( fun({CommandNameAtom, CTags}, D) -> CommandName = atom_to_list(CommandNameAtom), case CTags of [] -> orddict:append("untagged", CommandName, D); _ -> lists:foldl( fun(TagAtom, DD) -> Tag = atom_to_list(TagAtom), orddict:append(Tag, CommandName, DD) end, D, CTags) end end, orddict:new(), CommandTags), orddict:to_list(Dict). %% ----------------------------- %% Access verification %% ----------------------------- -spec is_admin(atom(), admin | noauth | auth(), map()) -> boolean(). is_admin(_Name, admin, _Extra) -> true; is_admin(_Name, {_User, _Server, _, false}, _Extra) -> false; is_admin(_Name, Map, _extra) when is_map(Map) -> true; is_admin(_Name, _Auth, _Extra) -> false. ejabberd-21.12/src/pubsub_subscription_sql.erl0000644000232200023220000002677714154362354022133 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : pubsub_subscription_sql.erl %%% Author : Pablo Polvorin %%% Purpose : Handle pubsub subscriptions options with ODBC backend %%% based on pubsub_subscription.erl by Brian Cully %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(pubsub_subscription_sql). -author("pablo.polvorin@process-one.net"). %% API -export([init/3, subscribe_node/3, unsubscribe_node/3, get_subscription/3, set_subscription/4, make_subid/0, get_options_xform/2, parse_options_xform/1]). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(PUBSUB_DELIVER, <<"pubsub#deliver">>). -define(PUBSUB_DIGEST, <<"pubsub#digest">>). -define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). -define(PUBSUB_EXPIRE, <<"pubsub#expire">>). -define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). -define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). -define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). -define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). -define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). -define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " "(aggregations) of notifications or all notifications individually">>). -define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " "sending any two notification digests">>). -define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). -define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " "XMPP message body in addition to the payload format">>). -define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). -define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). -define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). -define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). -define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). -define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). -define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). -define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). -define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). -define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). -define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). -define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). -define(DB_MOD, pubsub_db_sql). %%==================================================================== %% API %%==================================================================== init(_Host, _ServerHost, _Opts) -> ok = create_table(). -spec subscribe_node(_JID :: _, _NodeId :: _, Options :: [] | mod_pubsub:subOptions()) -> {result, mod_pubsub:subId()}. subscribe_node(_JID, _NodeId, Options) -> SubID = make_subid(), (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, options = Options}), {result, SubID}. -spec unsubscribe_node(_JID :: _, _NodeId :: _, SubID :: mod_pubsub:subId()) -> {result, mod_pubsub:subscription()} | {error, notfound}. unsubscribe_node(_JID, _NodeId, SubID) -> case (?DB_MOD):read_subscription(SubID) of {ok, Sub} -> (?DB_MOD):delete_subscription(SubID), {result, Sub}; notfound -> {error, notfound} end. -spec get_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> {result, mod_pubsub:subscription()} | {error, notfound}. get_subscription(_JID, _NodeId, SubID) -> case (?DB_MOD):read_subscription(SubID) of {ok, Sub} -> {result, Sub}; notfound -> {error, notfound} end. -spec set_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId(), Options :: mod_pubsub:subOptions()) -> {result, ok}. set_subscription(_JID, _NodeId, SubID, Options) -> case (?DB_MOD):read_subscription(SubID) of {ok, _} -> (?DB_MOD):update_subscription(#pubsub_subscription{subid = SubID, options = Options}), {result, ok}; notfound -> (?DB_MOD):add_subscription(#pubsub_subscription{subid = SubID, options = Options}), {result, ok} end. get_options_xform(Lang, Options) -> Keys = [deliver, show_values, subscription_type, subscription_depth], XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], {result, #xdata{type = form, fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, values = [?NS_PUBSUB_SUB_OPTIONS]}| XFields]}}. parse_options_xform(XFields) -> Opts = set_xoption(XFields, []), {result, Opts}. %%==================================================================== %% Internal functions %%==================================================================== create_table() -> ok. -spec make_subid() -> mod_pubsub:subId(). make_subid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). %% %% Subscription XForm processing. %% %% Return processed options, with types converted and so forth, using %% Opts as defaults. set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of {error, _} -> Opts; Key -> Val = val_xfield(Key, Value), lists:keystore(Key, 1, Opts, {Key, Val}) end, set_xoption(T, NewOpts). %% Return the options list's key for an XForm var. %% Convert Values for option list's Key. var_xfield(?PUBSUB_DELIVER) -> deliver; var_xfield(?PUBSUB_DIGEST) -> digest; var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; var_xfield(?PUBSUB_EXPIRE) -> expire; var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest_frequency = Opt, [Val]) -> case catch binary_to_integer(Val) of N when is_integer(N) -> N; _ -> Txt = {?T("Value of '~s' should be integer"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(expire = Opt, [Val]) -> try xmpp_util:decode_timestamp(Val) catch _:{bad_timestamp, _} -> Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; val_xfield(subscription_type, [<<"items">>]) -> items; val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; val_xfield(subscription_depth = Opt, [Depth]) -> case catch binary_to_integer(Depth) of N when is_integer(N) -> N; _ -> Txt = {?T("Value of '~s' should be integer"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end. %% Convert XForm booleans to Erlang booleans. xopt_to_bool(_, <<"0">>) -> false; xopt_to_bool(_, <<"1">>) -> true; xopt_to_bool(_, <<"false">>) -> false; xopt_to_bool(_, <<"true">>) -> true; xopt_to_bool(Option, _) -> Txt = {?T("Value of '~s' should be boolean"), [Option]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. get_option_xfield(Lang, Key, Options) -> Var = xfield_var(Key), Label = xfield_label(Key), {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of {value, {_, Val}} -> [xfield_val(Key, Val)]; false -> [] end, #xdata_field{type = Type, var = Var, label = translate:translate(Lang, Label), values = Vals, options = OptEls}. type_and_options({Type, Options}, Lang) -> {Type, [tr_xfield_options(O, Lang) || O <- Options]}; type_and_options(Type, _Lang) -> {Type, []}. tr_xfield_options({Value, Label}, Lang) -> #xdata_option{label = translate:translate(Lang, Label), value = Value}. xfield_var(deliver) -> ?PUBSUB_DELIVER; %xfield_var(digest) -> ?PUBSUB_DIGEST; %xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; %xfield_var(expire) -> ?PUBSUB_EXPIRE; %xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. xfield_type(deliver) -> boolean; %xfield_type(digest) -> boolean; %xfield_type(digest_frequency) -> 'text-single'; %xfield_type(expire) -> 'text-single'; %xfield_type(include_body) -> boolean; xfield_type(show_values) -> {'list-multi', [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; xfield_type(subscription_type) -> {'list-single', [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; xfield_type(subscription_depth) -> {'list-single', [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. %% Return the XForm variable label for a subscription option key. xfield_label(deliver) -> ?DELIVER_LABEL; %xfield_label(digest) -> ?DIGEST_LABEL; %xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; %xfield_label(expire) -> ?EXPIRE_LABEL; %xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; xfield_label(show_values) -> ?SHOW_VALUES_LABEL; %% Return the XForm value for a subscription option key. %% Convert erlang booleans to XForms. xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest_frequency, Val) -> % [integer_to_binary(Val))]; %xfield_val(expire, Val) -> % [jlib:now_to_utc_string(Val)]; %xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; xfield_val(show_values, Val) -> Val; xfield_val(subscription_type, items) -> [<<"items">>]; xfield_val(subscription_type, nodes) -> [<<"nodes">>]; xfield_val(subscription_depth, all) -> [<<"all">>]; xfield_val(subscription_depth, N) -> [integer_to_binary(N)]. bool_to_xopt(false) -> <<"false">>; bool_to_xopt(true) -> <<"true">>. ejabberd-21.12/src/mod_version_opt.erl0000644000232200023220000000051214154362354020331 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_version_opt). -export([show_os/1]). -spec show_os(gen_mod:opts() | global | binary()) -> boolean(). show_os(Opts) when is_map(Opts) -> gen_mod:get_opt(show_os, Opts); show_os(Host) -> gen_mod:get_module_opt(Host, mod_version, show_os). ejabberd-21.12/src/ejabberd_auth_ldap.erl0000644000232200023220000002466614154362354020722 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_ldap.erl %%% Author : Alexey Shchepin %%% Purpose : Authentication via LDAP %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_ldap). -author('alexey@process-one.net'). -behaviour(gen_server). -behaviour(ejabberd_auth). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -export([start/1, stop/1, start_link/1, set_password/3, check_password/4, user_exists/2, get_users/2, count_users/2, store_type/1, plain_password_required/1, reload/1]). -include("logger.hrl"). -include("eldap.hrl"). -record(state, {host = <<"">> :: binary(), eldap_id = <<"">> :: binary(), bind_eldap_id = <<"">> :: binary(), servers = [] :: [binary()], backups = [] :: [binary()], port = ?LDAP_PORT :: inet:port_number(), tls_options = [] :: list(), dn = <<"">> :: binary(), password = <<"">> :: binary(), base = <<"">> :: binary(), uids = [] :: [{binary()} | {binary(), binary()}], ufilter = <<"">> :: binary(), sfilter = <<"">> :: binary(), deref_aliases = never :: never | searching | finding | always, dn_filter :: binary() | undefined, dn_filter_attrs = [] :: [binary()]}). handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -define(LDAP_SEARCH_TIMEOUT, 5). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), ChildSpec = {Proc, {?MODULE, start_link, [Host]}, transient, 1000, worker, [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, ChildSpec). stop(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), case supervisor:terminate_child(ejabberd_backend_sup, Proc) of ok -> supervisor:delete_child(ejabberd_backend_sup, Proc); Err -> Err end. start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:start_link({local, Proc}, ?MODULE, Host, []). terminate(_Reason, _State) -> ok. init(Host) -> process_flag(trap_exit, true), State = parse_options(Host), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), eldap_pool:start_link(State#state.bind_eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), {ok, State}. reload(Host) -> stop(Host), start(Host). plain_password_required(_) -> true. store_type(_) -> external. check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> {nocache, false}; Password == <<"">> -> {nocache, false}; true -> case catch check_password_ldap(User, Server, Password) of {'EXIT', _} -> {nocache, false}; Result -> {cache, Result} end end. set_password(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of false -> {cache, {error, db_failure}}; DN -> case eldap_pool:modify_passwd(State#state.eldap_id, DN, Password) of ok -> {cache, {ok, Password}}; _Err -> {nocache, {error, db_failure}} end end. get_users(Server, []) -> case catch get_users_ldap(Server) of {'EXIT', _} -> []; Result -> Result end. count_users(Server, Opts) -> length(get_users(Server, Opts)). %% @spec (User, Server) -> true | false | {error, Error} user_exists(User, Server) -> case catch user_exists_ldap(User, Server) of {'EXIT', _Error} -> {nocache, {error, db_failure}}; Result -> {cache, Result} end. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- check_password_ldap(User, Server, Password) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of false -> false; DN -> case eldap_pool:bind(State#state.bind_eldap_id, DN, Password) of ok -> true; _ -> false end end. get_users_ldap(Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), UIDs = State#state.uids, Eldap_ID = State#state.eldap_id, Server = State#state.host, ResAttrs = result_attrs(State), case eldap_filter:parse(State#state.sfilter) of {ok, EldapFilter} -> case eldap_pool:search(Eldap_ID, [{base, State#state.base}, {filter, EldapFilter}, {timeout, ?LDAP_SEARCH_TIMEOUT}, {deref_aliases, State#state.deref_aliases}, {attributes, ResAttrs}]) of #eldap_search_result{entries = Entries} -> lists:flatmap(fun (#eldap_entry{attributes = Attrs, object_name = DN}) -> case is_valid_dn(DN, Attrs, State) of false -> []; _ -> case eldap_utils:find_ldap_attrs(UIDs, Attrs) of <<"">> -> []; {User, UIDFormat} -> case eldap_utils:get_user_part(User, UIDFormat) of {ok, U} -> case jid:nodeprep(U) of error -> []; LU -> [{LU, jid:nameprep(Server)}] end; _ -> [] end end end end, Entries); _ -> [] end; _ -> [] end. user_exists_ldap(User, Server) -> {ok, State} = eldap_utils:get_state(Server, ?MODULE), case find_user_dn(User, State) of false -> false; _DN -> true end. handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. find_user_dn(User, State) -> ResAttrs = result_attrs(State), case eldap_filter:parse(State#state.ufilter, [{<<"%u">>, User}]) of {ok, Filter} -> case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, Filter}, {deref_aliases, State#state.deref_aliases}, {attributes, ResAttrs}]) of #eldap_search_result{entries = [#eldap_entry{attributes = Attrs, object_name = DN} | _]} -> is_valid_dn(DN, Attrs, State); _ -> false end; _ -> false end. %% Check that the DN is valid, based on the dn filter is_valid_dn(DN, _, #state{dn_filter = undefined}) -> DN; is_valid_dn(DN, Attrs, State) -> DNAttrs = State#state.dn_filter_attrs, UIDs = State#state.uids, Values = [{<<"%s">>, eldap_utils:get_ldap_attr(Attr, Attrs), 1} || Attr <- DNAttrs], SubstValues = case eldap_utils:find_ldap_attrs(UIDs, Attrs) of <<"">> -> Values; {S, UAF} -> case eldap_utils:get_user_part(S, UAF) of {ok, U} -> [{<<"%u">>, U} | Values]; _ -> Values end end ++ [{<<"%d">>, State#state.host}, {<<"%D">>, DN}], case eldap_filter:parse(State#state.dn_filter, SubstValues) of {ok, EldapFilter} -> case eldap_pool:search(State#state.eldap_id, [{base, State#state.base}, {filter, EldapFilter}, {deref_aliases, State#state.deref_aliases}, {attributes, [<<"dn">>]}]) of #eldap_search_result{entries = [_ | _]} -> DN; _ -> false end; _ -> false end. result_attrs(#state{uids = UIDs, dn_filter_attrs = DNFilterAttrs}) -> lists:foldl(fun ({UID}, Acc) -> [UID | Acc]; ({UID, _}, Acc) -> [UID | Acc] end, DNFilterAttrs, UIDs). %%%---------------------------------------------------------------------- %%% Auxiliary functions %%%---------------------------------------------------------------------- parse_options(Host) -> Cfg = ?eldap_config(ejabberd_option, Host), Eldap_ID = misc:atom_to_binary(gen_mod:get_module_proc(Host, ?MODULE)), Bind_Eldap_ID = misc:atom_to_binary( gen_mod:get_module_proc(Host, bind_ejabberd_auth_ldap)), UIDsTemp = ejabberd_option:ldap_uids(Host), UIDs = eldap_utils:uids_domain_subst(Host, UIDsTemp), SubFilter = eldap_utils:generate_subfilter(UIDs), UserFilter = case ejabberd_option:ldap_filter(Host) of <<"">> -> SubFilter; F -> <<"(&", SubFilter/binary, F/binary, ")">> end, SearchFilter = eldap_filter:do_sub(UserFilter, [{<<"%u">>, <<"*">>}]), {DNFilter, DNFilterAttrs} = ejabberd_option:ldap_dn_filter(Host), #state{host = Host, eldap_id = Eldap_ID, bind_eldap_id = Bind_Eldap_ID, servers = Cfg#eldap_config.servers, backups = Cfg#eldap_config.backups, port = Cfg#eldap_config.port, tls_options = Cfg#eldap_config.tls_options, dn = Cfg#eldap_config.dn, password = Cfg#eldap_config.password, base = Cfg#eldap_config.base, deref_aliases = Cfg#eldap_config.deref_aliases, uids = UIDs, ufilter = UserFilter, sfilter = SearchFilter, dn_filter = DNFilter, dn_filter_attrs = DNFilterAttrs}. ejabberd-21.12/src/nodetree_tree_sql.erl0000644000232200023220000002706414154362354020641 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : nodetree_tree_sql.erl %%% Author : Christophe Romain %%% Purpose : Standard node tree plugin with ODBC backend %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the default PubSub node tree plugin. %%%

It is used as a default for all unknown PubSub node type. It can serve %%% as a developer basis and reference to build its own custom pubsub node tree %%% types.

%%%

PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

%%%

The API isn't stabilized yet. The pubsub plugin %%% development is still a work in progress. However, the system is already %%% usable and useful as is. Please, send us comments, feedback and %%% improvements.

-module(nodetree_tree_sql). -behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_sql_pt.hrl"). -include("translate.hrl"). -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, get_nodes/1, get_all_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). -export([raw_to_node/2]). init(_Host, _ServerHost, _Opts) -> ok. terminate(_Host, _ServerHost) -> ok. options() -> [{sql, true} | nodetree_tree:options()]. set_node(Record) when is_record(Record, pubsub_node) -> {Host, Node} = Record#pubsub_node.nodeid, Parent = case Record#pubsub_node.parents of [] -> <<>>; [First | _] -> First end, Type = Record#pubsub_node.type, H = node_flat_sql:encode_host(Host), Nidx = case nodeidx(Host, Node) of {result, OldNidx} -> catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_node_option " "where nodeid=%(OldNidx)d")), catch ejabberd_sql:sql_query_t( ?SQL("update pubsub_node set" " host=%(H)s, node=%(Node)s," " parent=%(Parent)s, plugin=%(Type)s " "where nodeid=%(OldNidx)d")), OldNidx; _ -> catch ejabberd_sql:sql_query_t( ?SQL("insert into pubsub_node(host, node, parent, plugin) " "values(%(H)s, %(Node)s, %(Parent)s, %(Type)s)")), case nodeidx(Host, Node) of {result, NewNidx} -> NewNidx; _ -> none % this should not happen end end, case Nidx of none -> Txt = ?T("Node index not found"), {error, xmpp:err_internal_server_error(Txt, ejabberd_option:language())}; _ -> lists:foreach(fun ({Key, Value}) -> SKey = iolist_to_binary(atom_to_list(Key)), SValue = misc:term_to_expr(Value), catch ejabberd_sql:sql_query_t( ?SQL("insert into pubsub_node_option(nodeid, name, val) " "values (%(Nidx)d, %(SKey)s, %(SValue)s)")) end, Record#pubsub_node.options), {result, Nidx} end. get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> H = node_flat_sql:encode_host(Host), case catch ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " "where host=%(H)s and node=%(Node)s")) of {selected, [RItem]} -> raw_to_node(Host, RItem); {'EXIT', _Reason} -> {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. get_node(Nidx) -> case catch ejabberd_sql:sql_query_t( ?SQL("select @(host)s, @(node)s, @(parent)s, @(plugin)s from pubsub_node " "where nodeid=%(Nidx)d")) of {selected, [{Host, Node, Parent, Type}]} -> raw_to_node(Host, {Node, Parent, Type, Nidx}); {'EXIT', _Reason} -> {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())}; _ -> {error, xmpp:err_item_not_found(?T("Node not found"), ejabberd_option:language())} end. get_nodes(Host) -> get_nodes(Host, infinity). get_nodes(Host, Limit) -> H = node_flat_sql:encode_host(Host), Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> ejabberd_sql:sql_query_t( ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s")); (_, _) when is_integer(Limit), Limit>=0 -> ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s limit %(Limit)d")); (_, _) -> ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s")) end, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> [raw_to_node(Host, Item) || Item <- RItems]; _ -> [] end. get_all_nodes({_U, _S, _R} = JID) -> SubKey = jid:tolower(JID), GenKey = jid:remove_resource(SubKey), EncKey = node_flat_sql:encode_jid(GenKey), Pattern = <<(node_flat_sql:encode_jid_like(GenKey))/binary, "/%">>, case ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(EncKey)s " "or host like %(Pattern)s %ESCAPE")) of {selected, RItems} -> [raw_to_node(GenKey, Item) || Item <- RItems]; _ -> [] end; get_all_nodes(Host) -> Pattern1 = <<"%@", Host/binary>>, Pattern2 = <<"%@", Host/binary, "/%">>, case ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(Host)s " "or host like %(Pattern1)s " "or host like %(Pattern2)s %ESCAPE")) of {selected, RItems} -> [raw_to_node(Host, Item) || Item <- RItems]; _ -> [] end. get_parentnodes(Host, Node, _From) -> case get_node(Host, Node) of Record when is_record(Record, pubsub_node) -> Record#pubsub_node.parents; _ -> [] end. get_parentnodes_tree(Host, Node, _From) -> get_parentnodes_tree(Host, Node, 0, []). get_parentnodes_tree(Host, Node, Level, Acc) -> case get_node(Host, Node) of Record when is_record(Record, pubsub_node) -> Tree = [{Level, [Record]}|Acc], case Record#pubsub_node.parents of [Parent] -> get_parentnodes_tree(Host, Parent, Level+1, Tree); _ -> Tree end; _ -> Acc end. get_subnodes(Host, Node, Limit) -> H = node_flat_sql:encode_host(Host), Query = fun(mssql, _) when is_integer(Limit), Limit>=0 -> ejabberd_sql:sql_query_t( ?SQL("select top %(Limit)d @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s and parent=%(Node)s")); (_, _) when is_integer(Limit), Limit>=0 -> ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s and parent=%(Node)s " "limit %(Limit)d")); (_, _) -> ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d " "from pubsub_node where host=%(H)s and parent=%(Node)s")) end, case ejabberd_sql:sql_query_t(Query) of {selected, RItems} -> [raw_to_node(Host, Item) || Item <- RItems]; _ -> [] end. get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(Host, Node) -> case get_node(Host, Node) of {error, _} -> []; Rec -> Type = Rec#pubsub_node.type, H = node_flat_sql:encode_host(Host), N = <<(ejabberd_sql:escape_like_arg(Node))/binary, "/%">>, Sub = case catch ejabberd_sql:sql_query_t( ?SQL("select @(node)s, @(parent)s, @(plugin)s, @(nodeid)d from pubsub_node " "where host=%(H)s and plugin=%(Type)s and" " (parent=%(Node)s or parent like %(N)s %ESCAPE)")) of {selected, RItems} -> [raw_to_node(Host, Item) || Item <- RItems]; _ -> [] end, [Rec|Sub] end. create_node(Host, Node, Type, Owner, Options, Parents) -> BJID = jid:tolower(jid:remove_resource(Owner)), case nodeidx(Host, Node) of {error, not_found} -> ParentExists = case Host of {_U, _S, _R} -> %% This is special case for PEP handling %% PEP does not uses hierarchy true; _ -> case Parents of [] -> true; [Parent | _] -> case nodeidx(Host, Parent) of {result, PNode} -> case nodeowners(PNode) of [{<<>>, Host, <<>>}] -> true; Owners -> lists:member(BJID, Owners) end; _ -> false end; _ -> false end end, case ParentExists of true -> case set_node(#pubsub_node{nodeid = {Host, Node}, parents = Parents, type = Type, options = Options}) of {result, Nidx} -> {ok, Nidx}; Other -> Other end; false -> {error, xmpp:err_forbidden()} end; {result, _} -> {error, xmpp:err_conflict(?T("Node already exists"), ejabberd_option:language())}; {error, db_fail} -> {error, xmpp:err_internal_server_error(?T("Database failure"), ejabberd_option:language())} end. delete_node(Host, Node) -> lists:map( fun(Rec) -> Nidx = Rec#pubsub_node.id, catch ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_node where nodeid=%(Nidx)d")), Rec end, get_subnodes_tree(Host, Node)). %% helpers raw_to_node(Host, [Node, Parent, Type, Nidx]) -> raw_to_node(Host, {Node, Parent, Type, binary_to_integer(Nidx)}); raw_to_node(Host, {Node, Parent, Type, Nidx}) -> Options = case catch ejabberd_sql:sql_query_t( ?SQL("select @(name)s, @(val)s from pubsub_node_option " "where nodeid=%(Nidx)d")) of {selected, ROptions} -> DbOpts = lists:map(fun ({Key, Value}) -> RKey = misc:binary_to_atom(Key), Tokens = element(2, erl_scan:string(binary_to_list(<>))), RValue = element(2, erl_parse:parse_term(Tokens)), {RKey, RValue} end, ROptions), Module = misc:binary_to_atom(<<"node_", Type/binary, "_sql">>), StdOpts = Module:options(), lists:foldl(fun ({Key, Value}, Acc) -> lists:keystore(Key, 1, Acc, {Key, Value}) end, StdOpts, DbOpts); _ -> [] end, Parents = case Parent of <<>> -> []; _ -> [Parent] end, #pubsub_node{nodeid = {Host, Node}, id = Nidx, parents = Parents, type = Type, options = Options}. nodeidx(Host, Node) -> H = node_flat_sql:encode_host(Host), case catch ejabberd_sql:sql_query_t( ?SQL("select @(nodeid)d from pubsub_node " "where host=%(H)s and node=%(Node)s")) of {selected, [{Nidx}]} -> {result, Nidx}; {'EXIT', _Reason} -> {error, db_fail}; _ -> {error, not_found} end. nodeowners(Nidx) -> {result, Res} = node_flat_sql:get_node_affiliations(Nidx), [LJID || {LJID, Aff} <- Res, Aff =:= owner]. ejabberd-21.12/src/gen_pubsub_nodetree.erl0000644000232200023220000000622114154362354021144 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : gen_pubsub_nodetree.erl %%% Author : Christophe Romain %%% Purpose : Define the pubsub node tree plugin behaviour %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(gen_pubsub_nodetree). -type(host() :: mod_pubsub:host()). -type(nodeId() :: mod_pubsub:nodeId()). -type(nodeIdx() :: mod_pubsub:nodeIdx()). -type(pubsubNode() :: mod_pubsub:pubsubNode()). -type(nodeOptions() :: mod_pubsub:nodeOptions()). -callback init(Host :: host(), ServerHost :: binary(), Opts :: [any()]) -> atom(). -include_lib("xmpp/include/xmpp.hrl"). -callback terminate(Host :: host(), ServerHost :: binary()) -> atom(). -callback options() -> nodeOptions(). -callback set_node(PubsubNode :: pubsubNode()) -> ok | {result, NodeIdx::nodeIdx()} | {error, stanza_error()}. -callback get_node(Host :: host(), NodeId :: nodeId(), From :: jid:jid()) -> pubsubNode() | {error, stanza_error()}. -callback get_node(Host :: host(), NodeId :: nodeId()) -> pubsubNode() | {error, stanza_error()}. -callback get_node(NodeIdx :: nodeIdx()) -> pubsubNode() | {error, stanza_error()}. -callback get_nodes(Host :: host(), Limit :: non_neg_integer() | infinity)-> [pubsubNode()]. -callback get_nodes(Host :: host())-> [pubsubNode()]. -callback get_all_nodes(Host :: host()) -> [pubsubNode()]. -callback get_parentnodes(Host :: host(), NodeId :: nodeId(), From :: jid:jid()) -> [pubsubNode()] | {error, stanza_error()}. -callback get_parentnodes_tree(Host :: host(), NodeId :: nodeId(), From :: jid:jid()) -> [{0, [pubsubNode(),...]}]. -callback get_subnodes(Host :: host(), NodeId :: nodeId(), Limit :: non_neg_integer() | infinity) -> [pubsubNode()]. -callback get_subnodes_tree(Host :: host(), NodeId :: nodeId(), From :: jid:jid()) -> [pubsubNode()]. -callback create_node(Host :: host(), NodeId :: nodeId(), Type :: binary(), Owner :: jid:jid(), Options :: nodeOptions(), Parents :: [nodeId()]) -> {ok, NodeIdx::nodeIdx()} | {error, stanza_error()} | {error, {virtual, {host(), nodeId()} | nodeId()}}. -callback delete_node(Host :: host(), NodeId :: nodeId()) -> [pubsubNode()]. ejabberd-21.12/src/mod_mqtt_session.erl0000644000232200023220000015247114154362354020526 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mod_mqtt_session). -behaviour(p1_server). -define(VSN, 2). -vsn(?VSN). %% API -export([start/3, start_link/3, accept/1, route/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include("mqtt.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -record(state, {vsn = ?VSN :: integer(), version :: undefined | mqtt_version(), socket :: undefined | socket(), peername :: undefined | peername(), timeout = infinity :: timer(), jid :: undefined | jid:jid(), session_expiry = 0 :: milli_seconds(), will :: undefined | publish(), will_delay = 0 :: milli_seconds(), stop_reason :: undefined | error_reason(), acks = #{} :: acks(), subscriptions = #{} :: subscriptions(), topic_aliases = #{} :: topic_aliases(), id = 0 :: non_neg_integer(), in_flight :: undefined | publish() | pubrel(), codec :: mqtt_codec:state(), queue :: undefined | p1_queue:queue(publish()), tls :: boolean()}). -type acks() :: #{non_neg_integer() => pubrec()}. -type subscriptions() :: #{binary() => {sub_opts(), non_neg_integer()}}. -type topic_aliases() :: #{non_neg_integer() => binary()}. -type error_reason() :: {auth, reason_code()} | {code, reason_code()} | {peer_disconnected, reason_code(), binary()} | {socket, socket_error_reason()} | {codec, mqtt_codec:error_reason()} | {unexpected_packet, atom()} | {tls, inet:posix() | atom() | binary()} | {replaced, pid()} | {resumed, pid()} | subscribe_forbidden | publish_forbidden | will_topic_forbidden | internal_server_error | session_expired | idle_connection | queue_full | shutdown | db_failure | {payload_format_invalid, will | publish} | session_expiry_non_zero | unknown_topic_alias. -type state() :: #state{}. -type socket() :: {gen_tcp, inet:socket()} | {fast_tls, fast_tls:tls_socket()} | {mod_mqtt_ws, mod_mqtt_ws:socket()}. -type peername() :: {inet:ip_address(), inet:port_number()}. -type seconds() :: non_neg_integer(). -type milli_seconds() :: non_neg_integer(). -type timer() :: infinity | {milli_seconds(), integer()}. -type socket_error_reason() :: closed | timeout | inet:posix(). -define(CALL_TIMEOUT, timer:minutes(1)). -define(RELAY_TIMEOUT, timer:minutes(1)). -define(MAX_UINT32, 4294967295). %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Socket, ListenOpts) -> p1_server:start(?MODULE, [SockMod, Socket, ListenOpts], ejabberd_config:fsm_limit_opts(ListenOpts)). start_link(SockMod, Socket, ListenOpts) -> p1_server:start_link(?MODULE, [SockMod, Socket, ListenOpts], ejabberd_config:fsm_limit_opts(ListenOpts)). -spec accept(pid()) -> ok. accept(Pid) -> p1_server:cast(Pid, accept). -spec route(pid(), term()) -> boolean(). route(Pid, Term) -> ejabberd_cluster:send(Pid, Term). -spec format_error(error_reason()) -> string(). format_error(session_expired) -> "Disconnected session is expired"; format_error(idle_connection) -> "Idle connection"; format_error(queue_full) -> "Message queue is overloaded"; format_error(internal_server_error) -> "Internal server error"; format_error(db_failure) -> "Database failure"; format_error(shutdown) -> "System shutting down"; format_error(subscribe_forbidden) -> "Subscribing to this topic is forbidden by service policy"; format_error(publish_forbidden) -> "Publishing to this topic is forbidden by service policy"; format_error(will_topic_forbidden) -> "Publishing to this will topic is forbidden by service policy"; format_error(session_expiry_non_zero) -> "Session Expiry Interval in DISCONNECT packet should have been zero"; format_error(unknown_topic_alias) -> "No mapping found for this Topic Alias"; format_error({payload_format_invalid, will}) -> "Will payload format doesn't match its indicator"; format_error({payload_format_invalid, publish}) -> "PUBLISH payload format doesn't match its indicator"; format_error({peer_disconnected, Code, <<>>}) -> format("Peer disconnected with reason: ~ts", [mqtt_codec:format_reason_code(Code)]); format_error({peer_disconnected, Code, Reason}) -> format("Peer disconnected with reason: ~ts (~ts)", [Reason, Code]); format_error({replaced, Pid}) -> format("Replaced by ~p at ~ts", [Pid, node(Pid)]); format_error({resumed, Pid}) -> format("Resumed by ~p at ~ts", [Pid, node(Pid)]); format_error({unexpected_packet, Name}) -> format("Unexpected ~ts packet", [string:to_upper(atom_to_list(Name))]); format_error({tls, Reason}) -> format("TLS failed: ~ts", [format_tls_error(Reason)]); format_error({socket, A}) -> format("Connection failed: ~ts", [format_inet_error(A)]); format_error({code, Code}) -> format("Protocol error: ~ts", [mqtt_codec:format_reason_code(Code)]); format_error({auth, Code}) -> format("Authentication failed: ~ts", [mqtt_codec:format_reason_code(Code)]); format_error({codec, CodecError}) -> format("Protocol error: ~ts", [mqtt_codec:format_error(CodecError)]); format_error(A) when is_atom(A) -> atom_to_list(A); format_error(Reason) -> format("Unrecognized error: ~w", [Reason]). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([SockMod, Socket, ListenOpts]) -> MaxSize = proplists:get_value(max_payload_size, ListenOpts, infinity), State1 = #state{socket = {SockMod, Socket}, id = p1_rand:uniform(65535), tls = proplists:get_bool(tls, ListenOpts), codec = mqtt_codec:new(MaxSize)}, Timeout = timer:seconds(30), State2 = set_timeout(State1, Timeout), {ok, State2, Timeout}. handle_call({get_state, _}, From, #state{stop_reason = {resumed, Pid}} = State) -> p1_server:reply(From, {error, {resumed, Pid}}), noreply(State); handle_call({get_state, Pid}, From, State) -> case stop(State, {resumed, Pid}) of {stop, Status, State1} -> {stop, Status, State1#state{stop_reason = {replaced, Pid}}}; {noreply, State1, _} -> ?DEBUG("Transferring MQTT session state to ~p at ~ts", [Pid, node(Pid)]), Q1 = p1_queue:file_to_ram(State1#state.queue), p1_server:reply(From, {ok, State1#state{queue = Q1}}), SessionExpiry = State1#state.session_expiry, State2 = set_timeout(State1, min(SessionExpiry, ?RELAY_TIMEOUT)), State3 = State2#state{queue = undefined, stop_reason = {resumed, Pid}, acks = #{}, will = undefined, session_expiry = 0, topic_aliases = #{}, subscriptions = #{}}, noreply(State3) end; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), noreply(State). handle_cast(accept, #state{socket = {_, Sock}} = State) -> case peername(State) of {ok, IPPort} -> State1 = State#state{peername = IPPort}, case starttls(State) of {ok, Socket1} -> State2 = State1#state{socket = Socket1}, handle_info({tcp, Sock, <<>>}, State2); {error, Why} -> stop(State1, Why) end; {error, Why} -> stop(State, {socket, Why}) end; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), noreply(State). handle_info(Msg, #state{stop_reason = {resumed, Pid} = Reason} = State) -> case Msg of {#publish{}, _} -> ?DEBUG("Relaying delayed publish to ~p at ~ts", [Pid, node(Pid)]), ejabberd_cluster:send(Pid, Msg), noreply(State); timeout -> stop(State, Reason); _ -> noreply(State) end; handle_info({#publish{meta = Meta} = Pkt, ExpiryTime}, State) -> ID = next_id(State#state.id), Meta1 = Meta#{expiry_time => ExpiryTime}, Pkt1 = Pkt#publish{id = ID, meta = Meta1}, State1 = State#state{id = ID}, case send(State1, Pkt1) of {ok, State2} -> noreply(State2); {error, State2, Reason} -> stop(State2, Reason) end; handle_info({tcp, TCPSock, TCPData}, #state{codec = Codec, socket = Socket} = State) -> case recv_data(Socket, TCPData) of {ok, Data} -> case mqtt_codec:decode(Codec, Data) of {ok, Pkt, Codec1} -> ?DEBUG("Got MQTT packet:~n~ts", [pp(Pkt)]), State1 = State#state{codec = Codec1}, case handle_packet(Pkt, State1) of {ok, State2} -> handle_info({tcp, TCPSock, <<>>}, State2); {error, State2, Reason} -> stop(State2, Reason) end; {more, Codec1} -> State1 = State#state{codec = Codec1}, State2 = reset_keep_alive(State1), activate(Socket), noreply(State2); {error, Why} -> stop(State, {codec, Why}) end; {error, Why} -> stop(State, Why) end; handle_info({tcp_closed, _Sock}, State) -> ?DEBUG("MQTT connection reset by peer", []), stop(State, {socket, closed}); handle_info({tcp_error, _Sock, Reason}, State) -> ?DEBUG("MQTT connection error: ~ts", [format_inet_error(Reason)]), stop(State, {socket, Reason}); handle_info(timeout, #state{socket = Socket} = State) -> case Socket of undefined -> ?DEBUG("MQTT session expired", []), stop(State#state{session_expiry = 0}, session_expired); _ -> ?DEBUG("MQTT connection timed out", []), stop(State, idle_connection) end; handle_info({replaced, Pid}, State) -> stop(State#state{session_expiry = 0}, {replaced, Pid}); handle_info({timeout, _TRef, publish_will}, State) -> noreply(publish_will(State)); handle_info({Ref, badarg}, State) when is_reference(Ref) -> %% TODO: figure out from where this messages comes from noreply(State); handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), noreply(State). -spec handle_packet(mqtt_packet(), state()) -> {ok, state()} | {error, state(), error_reason()}. handle_packet(#connect{proto_level = Version} = Pkt, State) -> handle_connect(Pkt, State#state{version = Version}); handle_packet(#publish{} = Pkt, State) -> handle_publish(Pkt, State); handle_packet(#puback{id = ID}, #state{in_flight = #publish{qos = 1, id = ID}} = State) -> resend(State#state{in_flight = undefined}); handle_packet(#puback{id = ID, code = Code}, State) -> ?DEBUG("Ignoring unexpected PUBACK with id=~B and code '~ts'", [ID, Code]), {ok, State}; handle_packet(#pubrec{id = ID, code = Code}, #state{in_flight = #publish{qos = 2, id = ID}} = State) -> case mqtt_codec:is_error_code(Code) of true -> ?DEBUG("Got PUBREC with error code '~ts', " "aborting acknowledgement", [Code]), resend(State#state{in_flight = undefined}); false -> Pubrel = #pubrel{id = ID}, send(State#state{in_flight = Pubrel}, Pubrel) end; handle_packet(#pubrec{id = ID, code = Code}, State) -> case mqtt_codec:is_error_code(Code) of true -> ?DEBUG("Ignoring unexpected PUBREC with id=~B and code '~ts'", [ID, Code]), {ok, State}; false -> Code1 = 'packet-identifier-not-found', ?DEBUG("Unexpected PUBREC with id=~B, " "sending PUBREL with error code '~ts'", [ID, Code1]), send(State, #pubrel{id = ID, code = Code1}) end; handle_packet(#pubcomp{id = ID}, #state{in_flight = #pubrel{id = ID}} = State) -> resend(State#state{in_flight = undefined}); handle_packet(#pubcomp{id = ID}, State) -> ?DEBUG("Ignoring unexpected PUBCOMP with id=~B: most likely " "it's a repeated response to duplicated PUBREL", [ID]), {ok, State}; handle_packet(#pubrel{id = ID}, State) -> case maps:take(ID, State#state.acks) of {_, Acks} -> send(State#state{acks = Acks}, #pubcomp{id = ID}); error -> Code = 'packet-identifier-not-found', ?DEBUG("Unexpected PUBREL with id=~B, " "sending PUBCOMP with error code '~ts'", [ID, Code]), Pubcomp = #pubcomp{id = ID, code = Code}, send(State, Pubcomp) end; handle_packet(#subscribe{} = Pkt, State) -> handle_subscribe(Pkt, State); handle_packet(#unsubscribe{} = Pkt, State) -> handle_unsubscribe(Pkt, State); handle_packet(#pingreq{}, State) -> send(State, #pingresp{}); handle_packet(#disconnect{properties = #{session_expiry_interval := SE}}, #state{session_expiry = 0} = State) when SE>0 -> %% Protocol violation {error, State, session_expiry_non_zero}; handle_packet(#disconnect{code = Code, properties = Props}, #state{jid = #jid{lserver = Server}} = State) -> Reason = maps:get(reason_string, Props, <<>>), Expiry = case maps:get(session_expiry_interval, Props, undefined) of undefined -> State#state.session_expiry; SE -> min(timer:seconds(SE), session_expiry(Server)) end, State1 = State#state{session_expiry = Expiry}, State2 = case Code of 'normal-disconnection' -> State1#state{will = undefined}; _ -> State1 end, {error, State2, {peer_disconnected, Code, Reason}}; handle_packet(Pkt, State) -> ?WARNING_MSG("Unexpected packet:~n~ts~n** when state:~n~ts", [pp(Pkt), pp(State)]), {error, State, {unexpected_packet, element(1, Pkt)}}. terminate(_, #state{peername = undefined}) -> ok; terminate(Reason, State) -> Reason1 = case Reason of shutdown -> shutdown; {shutdown, _} -> shutdown; normal -> State#state.stop_reason; {process_limit, _} -> queue_full; _ -> internal_server_error end, case State#state.jid of #jid{} -> unregister_session(State, Reason1); undefined -> log_disconnection(State, Reason1) end, State1 = disconnect(State, Reason1), publish_will(State1). code_change(_OldVsn, State, _Extra) -> {ok, upgrade_state(State)}. %%%=================================================================== %%% State transitions %%%=================================================================== -spec noreply(state()) -> {noreply, state(), non_neg_integer() | infinity}. noreply(#state{timeout = infinity} = State) -> {noreply, State, infinity}; noreply(#state{timeout = {MSecs, StartTime}} = State) -> CurrentTime = current_time(), Timeout = max(0, MSecs - CurrentTime + StartTime), {noreply, State, Timeout}. -spec stop(state(), error_reason()) -> {noreply, state(), infinity} | {stop, normal, state()}. stop(#state{session_expiry = 0} = State, Reason) -> {stop, normal, State#state{stop_reason = Reason}}; stop(#state{session_expiry = SessExp} = State, Reason) -> case State#state.socket of undefined -> noreply(State); _ -> WillDelay = State#state.will_delay, log_disconnection(State, Reason), State1 = disconnect(State, Reason), State2 = if WillDelay == 0 -> publish_will(State1); WillDelay < SessExp -> erlang:start_timer(WillDelay, self(), publish_will), State1; true -> State1 end, State3 = set_timeout(State2, SessExp), State4 = State3#state{stop_reason = Reason}, noreply(State4) end. %% Here is the code upgrading state between different %% code versions. This is needed when doing session resumption from %% remote node running the version of the code with incompatible #state{} %% record fields. Also used by code_change/3 callback. -spec upgrade_state(tuple()) -> state(). upgrade_state(State) -> case element(2, State) of ?VSN -> State; VSN when VSN > ?VSN -> erlang:error({downgrade_not_supported, State}); VSN -> State1 = upgrade_state(State, VSN), upgrade_state(setelement(2, State1, VSN+1)) end. -spec upgrade_state(tuple(), 1..?VSN) -> tuple(). upgrade_state(OldState, 1) -> %% Appending 'tls' field erlang:append_element(OldState, false); upgrade_state(State, _VSN) -> State. %%%=================================================================== %%% Session management %%%=================================================================== -spec open_session(state(), jid(), boolean()) -> {ok, boolean(), state()} | {error, state(), error_reason()}. open_session(State, JID, _CleanStart = false) -> USR = {_, S, _} = jid:tolower(JID), case mod_mqtt:lookup_session(USR) of {ok, Pid} -> try p1_server:call(Pid, {get_state, self()}, ?CALL_TIMEOUT) of {ok, State1} -> State2 = upgrade_state(State1), Q1 = case queue_type(S) of ram -> State2#state.queue; _ -> p1_queue:ram_to_file(State2#state.queue) end, Q2 = p1_queue:set_limit(Q1, queue_limit(S)), State3 = State#state{queue = Q2, acks = State2#state.acks, subscriptions = State2#state.subscriptions, id = State2#state.id, in_flight = State2#state.in_flight}, ?DEBUG("Resumed state from ~p at ~ts:~n~ts", [Pid, node(Pid), pp(State3)]), register_session(State3, JID, Pid); {error, Why} -> {error, State, Why} catch exit:{Why, {p1_server, _, _}} -> ?WARNING_MSG("Failed to copy session state from ~p at ~ts: ~ts", [Pid, node(Pid), format_exit_reason(Why)]), register_session(State, JID, undefined) end; {error, notfound} -> register_session(State, JID, undefined); {error, Why} -> {error, State, Why} end; open_session(State, JID, _CleanStart = true) -> register_session(State, JID, undefined). -spec register_session(state(), jid(), undefined | pid()) -> {ok, boolean(), state()} | {error, state(), error_reason()}. register_session(#state{peername = IP} = State, JID, Parent) -> USR = {_, S, _} = jid:tolower(JID), case mod_mqtt:open_session(USR) of ok -> case resubscribe(USR, State#state.subscriptions) of ok -> ?INFO_MSG("~ts for ~ts from ~ts", [if is_pid(Parent) -> io_lib:format( "Reopened MQTT session via ~p", [Parent]); true -> "Opened MQTT session" end, jid:encode(JID), ejabberd_config:may_hide_data( misc:ip_to_list(IP))]), Q = case State#state.queue of undefined -> p1_queue:new(queue_type(S), queue_limit(S)); Q1 -> Q1 end, {ok, is_pid(Parent), State#state{jid = JID, queue = Q}}; {error, Why} -> mod_mqtt:close_session(USR), {error, State#state{session_expiry = 0}, Why} end; {error, Reason} -> ?ERROR_MSG("Failed to register MQTT session for ~ts from ~ts: ~ts", err_args(JID, IP, Reason)), {error, State, Reason} end. -spec unregister_session(state(), error_reason()) -> ok. unregister_session(#state{jid = #jid{} = JID, peername = IP} = State, Reason) -> Msg = "Closing MQTT session for ~ts from ~ts: ~ts", case Reason of {Tag, _} when Tag == replaced; Tag == resumed -> ?DEBUG(Msg, err_args(JID, IP, Reason)); {socket, _} -> ?INFO_MSG(Msg, err_args(JID, IP, Reason)); Tag when Tag == idle_connection; Tag == session_expired; Tag == shutdown -> ?INFO_MSG(Msg, err_args(JID, IP, Reason)); {peer_disconnected, Code, _} -> case mqtt_codec:is_error_code(Code) of true -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)); false -> ?INFO_MSG(Msg, err_args(JID, IP, Reason)) end; _ -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)) end, USR = jid:tolower(JID), unsubscribe(maps:keys(State#state.subscriptions), USR, #{}), case mod_mqtt:close_session(USR) of ok -> ok; {error, Why} -> ?ERROR_MSG( "Failed to close MQTT session for ~ts from ~ts: ~ts", err_args(JID, IP, Why)) end; unregister_session(_, _) -> ok. %%%=================================================================== %%% CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers %%%=================================================================== -spec handle_connect(connect(), state()) -> {ok, state()} | {error, state(), error_reason()}. handle_connect(#connect{clean_start = CleanStart} = Pkt, #state{jid = undefined, peername = IP} = State) -> case authenticate(Pkt, IP) of {ok, JID} -> case validate_will(Pkt, JID) of ok -> case open_session(State, JID, CleanStart) of {ok, SessionPresent, State1} -> State2 = set_session_properties(State1, Pkt), ConnackProps = get_connack_properties(State2, Pkt), Connack = #connack{session_present = SessionPresent, properties = ConnackProps}, case send(State2, Connack) of {ok, State3} -> resend(State3); {error, _, _} = Err -> Err end; {error, _, _} = Err -> Err end; {error, Reason} -> {error, State, Reason} end; {error, Code} -> {error, State, {auth, Code}} end. -spec handle_publish(publish(), state()) -> {ok, state()} | {error, state(), error_reason()}. handle_publish(#publish{qos = QoS, id = ID} = Publish, State) -> case QoS == 2 andalso maps:is_key(ID, State#state.acks) of true -> send(State, maps:get(ID, State#state.acks)); false -> case validate_publish(Publish, State) of ok -> State1 = store_topic_alias(State, Publish), Ret = publish(State1, Publish), {Code, Props} = get_publish_code_props(Ret), case Ret of {ok, _} when QoS == 2 -> Pkt = #pubrec{id = ID, code = Code, properties = Props}, Acks = maps:put(ID, Pkt, State1#state.acks), State2 = State1#state{acks = Acks}, send(State2, Pkt); {error, _} when QoS == 2 -> Pkt = #pubrec{id = ID, code = Code, properties = Props}, send(State1, Pkt); _ when QoS == 1 -> Pkt = #puback{id = ID, code = Code, properties = Props}, send(State1, Pkt); _ -> {ok, State1} end; {error, Why} -> {error, State, Why} end end. -spec handle_subscribe(subscribe(), state()) -> {ok, state()} | {error, state(), error_reason()}. handle_subscribe(#subscribe{id = ID, filters = TopicFilters} = Pkt, State) -> case validate_subscribe(Pkt) of ok -> USR = jid:tolower(State#state.jid), SubID = maps:get(subscription_identifier, Pkt#subscribe.properties, 0), OldSubs = State#state.subscriptions, {Codes, NewSubs, Props} = subscribe(TopicFilters, USR, SubID), Subs = maps:merge(OldSubs, NewSubs), State1 = State#state{subscriptions = Subs}, Suback = #suback{id = ID, codes = Codes, properties = Props}, case send(State1, Suback) of {ok, State2} -> Pubs = select_retained(USR, NewSubs, OldSubs), send_retained(State2, Pubs); {error, _, _} = Err -> Err end; {error, Why} -> {error, State, Why} end. -spec handle_unsubscribe(unsubscribe(), state()) -> {ok, state()} | {error, state(), error_reason()}. handle_unsubscribe(#unsubscribe{id = ID, filters = TopicFilters}, State) -> USR = jid:tolower(State#state.jid), {Codes, Subs, Props} = unsubscribe(TopicFilters, USR, State#state.subscriptions), State1 = State#state{subscriptions = Subs}, Unsuback = #unsuback{id = ID, codes = Codes, properties = Props}, send(State1, Unsuback). %%%=================================================================== %%% Aux functions for CONNECT/PUBLISH/SUBSCRIBE/UNSUBSCRIBE handlers %%%=================================================================== -spec set_session_properties(state(), connect()) -> state(). set_session_properties(#state{version = Version, jid = #jid{lserver = Server}} = State, #connect{clean_start = CleanStart, keep_alive = KeepAlive, properties = Props} = Pkt) -> SEMin = case CleanStart of false when Version == ?MQTT_VERSION_4 -> infinity; _ -> timer:seconds(maps:get(session_expiry_interval, Props, 0)) end, SEConfig = session_expiry(Server), State1 = State#state{session_expiry = min(SEMin, SEConfig)}, State2 = set_will_properties(State1, Pkt), set_keep_alive(State2, KeepAlive). -spec set_will_properties(state(), connect()) -> state(). set_will_properties(State, #connect{will = #publish{} = Will, will_properties = Props}) -> {WillDelay, Props1} = case maps:take(will_delay_interval, Props) of error -> {0, Props}; Ret -> Ret end, State#state{will = Will#publish{properties = Props1}, will_delay = timer:seconds(WillDelay)}; set_will_properties(State, _) -> State. -spec get_connack_properties(state(), connect()) -> properties(). get_connack_properties(#state{session_expiry = SessExp, jid = JID}, #connect{client_id = ClientID, keep_alive = KeepAlive, properties = Props}) -> Props1 = case ClientID of <<>> -> #{assigned_client_identifier => JID#jid.lresource}; _ -> #{} end, Props2 = case maps:find(authentication_method, Props) of {ok, Method} -> Props1#{authentication_method => Method}; error -> Props1 end, Props2#{session_expiry_interval => SessExp div 1000, shared_subscription_available => false, topic_alias_maximum => topic_alias_maximum(JID#jid.lserver), server_keep_alive => KeepAlive}. -spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer()) -> {[reason_code()], subscriptions(), properties()}. subscribe(TopicFilters, USR, SubID) -> subscribe(TopicFilters, USR, SubID, [], #{}, ok). -spec subscribe([{binary(), sub_opts()}], jid:ljid(), non_neg_integer(), [reason_code()], subscriptions(), ok | {error, error_reason()}) -> {[reason_code()], subscriptions(), properties()}. subscribe([{TopicFilter, SubOpts}|TopicFilters], USR, SubID, Codes, Subs, Err) -> case mod_mqtt:subscribe(USR, TopicFilter, SubOpts, SubID) of ok -> Code = subscribe_reason_code(SubOpts#sub_opts.qos), subscribe(TopicFilters, USR, SubID, [Code|Codes], maps:put(TopicFilter, {SubOpts, SubID}, Subs), Err); {error, Why} = Err1 -> Code = subscribe_reason_code(Why), subscribe(TopicFilters, USR, SubID, [Code|Codes], Subs, Err1) end; subscribe([], _USR, _SubID, Codes, Subs, Err) -> Props = case Err of ok -> #{}; {error, Why} -> #{reason_string => format_reason_string(Why)} end, {lists:reverse(Codes), Subs, Props}. -spec unsubscribe([binary()], jid:ljid(), subscriptions()) -> {[reason_code()], subscriptions(), properties()}. unsubscribe(TopicFilters, USR, Subs) -> unsubscribe(TopicFilters, USR, [], Subs, ok). -spec unsubscribe([binary()], jid:ljid(), [reason_code()], subscriptions(), ok | {error, error_reason()}) -> {[reason_code()], subscriptions(), properties()}. unsubscribe([TopicFilter|TopicFilters], USR, Codes, Subs, Err) -> case mod_mqtt:unsubscribe(USR, TopicFilter) of ok -> unsubscribe(TopicFilters, USR, [success|Codes], maps:remove(TopicFilter, Subs), Err); {error, notfound} -> unsubscribe(TopicFilters, USR, ['no-subscription-existed'|Codes], maps:remove(TopicFilter, Subs), Err); {error, Why} = Err1 -> Code = unsubscribe_reason_code(Why), unsubscribe(TopicFilters, USR, [Code|Codes], Subs, Err1) end; unsubscribe([], _USR, Codes, Subs, Err) -> Props = case Err of ok -> #{}; {error, Why} -> #{reason_string => format_reason_string(Why)} end, {lists:reverse(Codes), Subs, Props}. -spec select_retained(jid:ljid(), subscriptions(), subscriptions()) -> [{publish(), seconds()}]. select_retained(USR, NewSubs, OldSubs) -> lists:flatten( maps:fold( fun(_Filter, {#sub_opts{retain_handling = 2}, _SubID}, Acc) -> Acc; (Filter, {#sub_opts{retain_handling = 1, qos = QoS}, SubID}, Acc) -> case maps:is_key(Filter, OldSubs) of true -> Acc; false -> [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] end; (Filter, {#sub_opts{qos = QoS}, SubID}, Acc) -> [mod_mqtt:select_retained(USR, Filter, QoS, SubID)|Acc] end, [], NewSubs)). -spec send_retained(state(), [{publish(), seconds()}]) -> {ok, state()} | {error, state(), error_reason()}. send_retained(State, [{#publish{meta = Meta} = Pub, Expiry}|Pubs]) -> I = next_id(State#state.id), Meta1 = Meta#{expiry_time => Expiry}, Pub1 = Pub#publish{id = I, retain = true, meta = Meta1}, case send(State#state{id = I}, Pub1) of {ok, State1} -> send_retained(State1, Pubs); Err -> Err end; send_retained(State, []) -> {ok, State}. -spec publish(state(), publish()) -> {ok, non_neg_integer()} | {error, error_reason()}. publish(State, #publish{topic = Topic, properties = Props} = Pkt) -> MessageExpiry = maps:get(message_expiry_interval, Props, ?MAX_UINT32), ExpiryTime = min(unix_time() + MessageExpiry, ?MAX_UINT32), USR = jid:tolower(State#state.jid), Props1 = maps:filter( fun(payload_format_indicator, _) -> true; (content_type, _) -> true; (response_topic, _) -> true; (correlation_data, _) -> true; (user_property, _) -> true; (_, _) -> false end, Props), Topic1 = case Topic of <<>> -> Alias = maps:get(topic_alias, Props), maps:get(Alias, State#state.topic_aliases); _ -> Topic end, Pkt1 = Pkt#publish{topic = Topic1, properties = Props1}, mod_mqtt:publish(USR, Pkt1, ExpiryTime). -spec store_topic_alias(state(), publish()) -> state(). store_topic_alias(State, #publish{topic = <<_, _/binary>> = Topic, properties = #{topic_alias := Alias}}) -> Aliases = maps:put(Alias, Topic, State#state.topic_aliases), State#state{topic_aliases = Aliases}; store_topic_alias(State, _) -> State. %%%=================================================================== %%% Socket management %%%=================================================================== -spec send(state(), mqtt_packet()) -> {ok, state()} | {error, state(), error_reason()}. send(State, #publish{} = Pkt) -> case is_expired(Pkt) of {false, Pkt1} -> case State#state.in_flight == undefined andalso p1_queue:is_empty(State#state.queue) of true -> Dup = case Pkt1#publish.qos of 0 -> undefined; _ -> Pkt1 end, State1 = State#state{in_flight = Dup}, {ok, do_send(State1, Pkt1)}; false -> ?DEBUG("Queueing packet:~n~ts~n** when state:~n~ts", [pp(Pkt), pp(State)]), try p1_queue:in(Pkt, State#state.queue) of Q -> State1 = State#state{queue = Q}, {ok, State1} catch error:full -> Q = p1_queue:clear(State#state.queue), State1 = State#state{queue = Q, session_expiry = 0}, {error, State1, queue_full} end end; true -> {ok, State} end; send(State, Pkt) -> {ok, do_send(State, Pkt)}. -spec resend(state()) -> {ok, state()} | {error, state(), error_reason()}. resend(#state{in_flight = undefined} = State) -> case p1_queue:out(State#state.queue) of {{value, #publish{qos = QoS} = Pkt}, Q} -> case is_expired(Pkt) of true -> resend(State#state{queue = Q}); {false, Pkt1} when QoS > 0 -> State1 = State#state{in_flight = Pkt1, queue = Q}, {ok, do_send(State1, Pkt1)}; {false, Pkt1} -> State1 = do_send(State#state{queue = Q}, Pkt1), resend(State1) end; {empty, _} -> {ok, State} end; resend(#state{in_flight = Pkt} = State) -> {ok, do_send(State, set_dup_flag(Pkt))}. -spec do_send(state(), mqtt_packet()) -> state(). do_send(#state{socket = {SockMod, Sock} = Socket} = State, Pkt) -> ?DEBUG("Send MQTT packet:~n~ts", [pp(Pkt)]), Data = mqtt_codec:encode(State#state.version, Pkt), Res = SockMod:send(Sock, Data), check_sock_result(Socket, Res), State; do_send(State, _Pkt) -> State. -spec activate(socket()) -> ok. activate({SockMod, Sock} = Socket) -> Res = case SockMod of gen_tcp -> inet:setopts(Sock, [{active, once}]); _ -> SockMod:setopts(Sock, [{active, once}]) end, check_sock_result(Socket, Res). -spec peername(state()) -> {ok, peername()} | {error, socket_error_reason()}. peername(#state{socket = {SockMod, Sock}}) -> case SockMod of gen_tcp -> inet:peername(Sock); _ -> SockMod:peername(Sock) end. -spec disconnect(state(), error_reason()) -> state(). disconnect(#state{socket = {SockMod, Sock}} = State, Err) -> State1 = case Err of {auth, Code} -> do_send(State, #connack{code = Code}); {codec, {Tag, _, _}} when Tag == unsupported_protocol_version; Tag == unsupported_protocol_name -> do_send(State#state{version = ?MQTT_VERSION_4}, #connack{code = connack_reason_code(Err)}); _ when State#state.version == undefined -> State; {Tag, _} when Tag == socket; Tag == tls -> State; {peer_disconnected, _, _} -> State; _ -> Props = #{reason_string => format_reason_string(Err)}, case State#state.jid of undefined -> Code = connack_reason_code(Err), Pkt = #connack{code = Code, properties = Props}, do_send(State, Pkt); _ when State#state.version == ?MQTT_VERSION_5 -> Code = disconnect_reason_code(Err), Pkt = #disconnect{code = Code, properties = Props}, do_send(State, Pkt); _ -> State end end, SockMod:close(Sock), State1#state{socket = undefined, version = undefined, codec = mqtt_codec:renew(State#state.codec)}; disconnect(State, _) -> State. -spec check_sock_result(socket(), ok | {error, inet:posix()}) -> ok. check_sock_result(_, ok) -> ok; check_sock_result({_, Sock}, {error, Why}) -> self() ! {tcp_closed, Sock}, ?DEBUG("MQTT socket error: ~p", [format_inet_error(Why)]). -spec starttls(state()) -> {ok, socket()} | {error, error_reason()}. starttls(#state{socket = {gen_tcp, Socket}, tls = true}) -> case ejabberd_pkix:get_certfile() of {ok, Cert} -> case fast_tls:tcp_to_tls(Socket, [{certfile, Cert}]) of {ok, TLSSock} -> {ok, {fast_tls, TLSSock}}; {error, Why} -> {error, {tls, Why}} end; error -> {error, {tls, no_certfile}} end; starttls(#state{socket = Socket}) -> {ok, Socket}. -spec recv_data(socket(), binary()) -> {ok, binary()} | {error, error_reason()}. recv_data({fast_tls, Sock}, Data) -> case fast_tls:recv_data(Sock, Data) of {ok, _} = OK -> OK; {error, E} when is_atom(E) -> {error, {socket, E}}; {error, E} when is_binary(E) -> {error, {tls, E}} end; recv_data(_, Data) -> {ok, Data}. %%%=================================================================== %%% Formatters %%%=================================================================== -spec pp(any()) -> iolist(). pp(Term) -> io_lib_pretty:print(Term, fun pp/2). -spec format_inet_error(socket_error_reason()) -> string(). format_inet_error(closed) -> "connection closed"; format_inet_error(timeout) -> format_inet_error(etimedout); format_inet_error(Reason) -> case inet:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); Txt -> Txt end. -spec format_tls_error(atom() | binary()) -> string() | binary(). format_tls_error(no_certfile) -> "certificate not configured"; format_tls_error(Reason) when is_atom(Reason) -> format_inet_error(Reason); format_tls_error(Reason) -> Reason. -spec format_exit_reason(term()) -> string(). format_exit_reason(noproc) -> "process is dead"; format_exit_reason(normal) -> "process has exited"; format_exit_reason(killed) -> "process has been killed"; format_exit_reason(timeout) -> "remote call to process timed out"; format_exit_reason(Why) -> format("unexpected error: ~p", [Why]). %% Same as format_error/1, but hides sensitive data %% and returns result as binary -spec format_reason_string(error_reason()) -> binary(). format_reason_string({resumed, _}) -> <<"Resumed by another connection">>; format_reason_string({replaced, _}) -> <<"Replaced by another connection">>; format_reason_string(Err) -> list_to_binary(format_error(Err)). -spec format(io:format(), list()) -> string(). format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). -spec pp(atom(), non_neg_integer()) -> [atom()] | no. pp(state, 17) -> record_info(fields, state); pp(Rec, Size) -> mqtt_codec:pp(Rec, Size). -spec publish_reason_code(error_reason()) -> reason_code(). publish_reason_code(publish_forbidden) -> 'topic-name-invalid'; publish_reason_code(_) -> 'implementation-specific-error'. -spec subscribe_reason_code(qos() | error_reason()) -> reason_code(). subscribe_reason_code(0) -> 'granted-qos-0'; subscribe_reason_code(1) -> 'granted-qos-1'; subscribe_reason_code(2) -> 'granted-qos-2'; subscribe_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; subscribe_reason_code(_) -> 'implementation-specific-error'. -spec unsubscribe_reason_code(error_reason()) -> reason_code(). unsubscribe_reason_code(_) -> 'implementation-specific-error'. -spec disconnect_reason_code(error_reason()) -> reason_code(). disconnect_reason_code({code, Code}) -> Code; disconnect_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); disconnect_reason_code({unexpected_packet, _}) -> 'protocol-error'; disconnect_reason_code({replaced, _}) -> 'session-taken-over'; disconnect_reason_code({resumed, _}) -> 'session-taken-over'; disconnect_reason_code(internal_server_error) -> 'implementation-specific-error'; disconnect_reason_code(db_failure) -> 'implementation-specific-error'; disconnect_reason_code(idle_connection) -> 'keep-alive-timeout'; disconnect_reason_code(queue_full) -> 'quota-exceeded'; disconnect_reason_code(shutdown) -> 'server-shutting-down'; disconnect_reason_code(subscribe_forbidden) -> 'topic-filter-invalid'; disconnect_reason_code(publish_forbidden) -> 'topic-name-invalid'; disconnect_reason_code(will_topic_forbidden) -> 'topic-name-invalid'; disconnect_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; disconnect_reason_code(session_expiry_non_zero) -> 'protocol-error'; disconnect_reason_code(unknown_topic_alias) -> 'protocol-error'; disconnect_reason_code(_) -> 'unspecified-error'. -spec connack_reason_code(error_reason()) -> reason_code(). connack_reason_code({Tag, Code}) when Tag == auth; Tag == code -> Code; connack_reason_code({codec, Err}) -> mqtt_codec:error_reason_code(Err); connack_reason_code({unexpected_packet, _}) -> 'protocol-error'; connack_reason_code(internal_server_error) -> 'implementation-specific-error'; connack_reason_code(db_failure) -> 'implementation-specific-error'; connack_reason_code(idle_connection) -> 'keep-alive-timeout'; connack_reason_code(queue_full) -> 'quota-exceeded'; connack_reason_code(shutdown) -> 'server-shutting-down'; connack_reason_code(will_topic_forbidden) -> 'topic-name-invalid'; connack_reason_code({payload_format_invalid, _}) -> 'payload-format-invalid'; connack_reason_code(session_expiry_non_zero) -> 'protocol-error'; connack_reason_code(_) -> 'unspecified-error'. %%%=================================================================== %%% Configuration processing %%%=================================================================== -spec queue_type(binary()) -> ram | file. queue_type(Host) -> mod_mqtt_opt:queue_type(Host). -spec queue_limit(binary()) -> non_neg_integer() | unlimited. queue_limit(Host) -> mod_mqtt_opt:max_queue(Host). -spec session_expiry(binary()) -> milli_seconds(). session_expiry(Host) -> mod_mqtt_opt:session_expiry(Host). -spec topic_alias_maximum(binary()) -> non_neg_integer(). topic_alias_maximum(Host) -> mod_mqtt_opt:max_topic_aliases(Host). %%%=================================================================== %%% Timings %%%=================================================================== -spec current_time() -> milli_seconds(). current_time() -> erlang:monotonic_time(millisecond). -spec unix_time() -> seconds(). unix_time() -> erlang:system_time(second). -spec set_keep_alive(state(), seconds()) -> state(). set_keep_alive(State, 0) -> ?DEBUG("Disabling MQTT keep-alive", []), State#state{timeout = infinity}; set_keep_alive(State, Secs) -> Secs1 = round(Secs * 1.5), ?DEBUG("Setting MQTT keep-alive to ~B seconds", [Secs1]), set_timeout(State, timer:seconds(Secs1)). -spec reset_keep_alive(state()) -> state(). reset_keep_alive(#state{timeout = {MSecs, _}, jid = #jid{}} = State) -> set_timeout(State, MSecs); reset_keep_alive(State) -> State. -spec set_timeout(state(), milli_seconds()) -> state(). set_timeout(State, MSecs) -> Time = current_time(), State#state{timeout = {MSecs, Time}}. -spec is_expired(publish()) -> true | {false, publish()}. is_expired(#publish{meta = Meta, properties = Props} = Pkt) -> case maps:get(expiry_time, Meta, ?MAX_UINT32) of ?MAX_UINT32 -> {false, Pkt}; ExpiryTime -> Left = ExpiryTime - unix_time(), if Left > 0 -> Props1 = Props#{message_expiry_interval => Left}, {false, Pkt#publish{properties = Props1}}; true -> ?DEBUG("Dropping expired packet:~n~ts", [pp(Pkt)]), true end end. %%%=================================================================== %%% Authentication %%%=================================================================== -spec parse_credentials(connect()) -> {ok, jid:jid()} | {error, reason_code()}. parse_credentials(#connect{client_id = <<>>}) -> parse_credentials(#connect{client_id = p1_rand:get_string()}); parse_credentials(#connect{username = <<>>, client_id = ClientID}) -> Host = ejabberd_config:get_myname(), JID = case jid:make(ClientID, Host) of error -> jid:make(str:sha(ClientID), Host); J -> J end, parse_credentials(JID, ClientID); parse_credentials(#connect{username = User} = Pkt) -> try jid:decode(User) of #jid{luser = <<>>} -> case jid:make(User, ejabberd_config:get_myname()) of error -> {error, 'bad-user-name-or-password'}; JID -> parse_credentials(JID, Pkt#connect.client_id) end; JID -> parse_credentials(JID, Pkt#connect.client_id) catch _:{bad_jid, _} -> {error, 'bad-user-name-or-password'} end. -spec parse_credentials(jid:jid(), binary()) -> {ok, jid:jid()} | {error, reason_code()}. parse_credentials(JID, ClientID) -> case gen_mod:is_loaded(JID#jid.lserver, mod_mqtt) of false -> {error, 'server-unavailable'}; true -> case jid:replace_resource(JID, ClientID) of error -> {error, 'client-identifier-not-valid'}; JID1 -> {ok, JID1} end end. -spec authenticate(connect(), peername()) -> {ok, jid:jid()} | {error, reason_code()}. authenticate(Pkt, IP) -> case authenticate(Pkt) of {ok, JID, AuthModule} -> ?INFO_MSG("Accepted MQTT authentication for ~ts by ~s backend from ~s", [jid:encode(JID), ejabberd_auth:backend_type(AuthModule), ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), {ok, JID}; {error, _} = Err -> Err end. -spec authenticate(connect()) -> {ok, jid:jid(), module()} | {error, reason_code()}. authenticate(#connect{password = Pass, properties = Props} = Pkt) -> case parse_credentials(Pkt) of {ok, #jid{luser = LUser, lserver = LServer} = JID} -> case maps:find(authentication_method, Props) of {ok, <<"X-OAUTH2">>} -> Token = maps:get(authentication_data, Props, <<>>), case ejabberd_oauth:check_token( LUser, LServer, [<<"sasl_auth">>], Token) of true -> {ok, JID, ejabberd_oauth}; _ -> {error, 'not-authorized'} end; {ok, _} -> {error, 'bad-authentication-method'}; error -> case ejabberd_auth:check_password_with_authmodule( LUser, <<>>, LServer, Pass) of {true, AuthModule} -> {ok, JID, AuthModule}; false -> {error, 'not-authorized'} end end; {error, _} = Err -> Err end. %%%=================================================================== %%% Validators %%%=================================================================== -spec validate_will(connect(), jid:jid()) -> ok | {error, error_reason()}. validate_will(#connect{will = undefined}, _) -> ok; validate_will(#connect{will = #publish{topic = Topic, payload = Payload}, will_properties = Props}, JID) -> case mod_mqtt:check_publish_access(Topic, jid:tolower(JID)) of deny -> {error, will_topic_forbidden}; allow -> validate_payload(Props, Payload, will) end. -spec validate_publish(publish(), state()) -> ok | {error, error_reason()}. validate_publish(#publish{topic = Topic, payload = Payload, properties = Props}, State) -> case validate_topic(Topic, Props, State) of ok -> validate_payload(Props, Payload, publish); Err -> Err end. -spec validate_subscribe(subscribe()) -> ok | {error, error_reason()}. validate_subscribe(#subscribe{filters = Filters}) -> case lists:any( fun({<<"$share/", _/binary>>, _}) -> true; (_) -> false end, Filters) of true -> {error, {code, 'shared-subscriptions-not-supported'}}; false -> ok end. -spec validate_topic(binary(), properties(), state()) -> ok | {error, error_reason()}. validate_topic(<<>>, Props, State) -> case maps:get(topic_alias, Props, 0) of 0 -> {error, {code, 'topic-alias-invalid'}}; Alias -> case maps:is_key(Alias, State#state.topic_aliases) of true -> ok; false -> {error, unknown_topic_alias} end end; validate_topic(_, #{topic_alias := Alias}, State) -> JID = State#state.jid, Max = topic_alias_maximum(JID#jid.lserver), if Alias > Max -> {error, {code, 'topic-alias-invalid'}}; true -> ok end; validate_topic(_, _, _) -> ok. -spec validate_payload(properties(), binary(), will | publish) -> ok | {error, error_reason()}. validate_payload(#{payload_format_indicator := utf8}, Payload, Type) -> try mqtt_codec:utf8(Payload) of _ -> ok catch _:_ -> {error, {payload_format_invalid, Type}} end; validate_payload(_, _, _) -> ok. %%%=================================================================== %%% Misc %%%=================================================================== -spec resubscribe(jid:ljid(), subscriptions()) -> ok | {error, error_reason()}. resubscribe(USR, Subs) -> case maps:fold( fun(TopicFilter, {SubOpts, ID}, ok) -> mod_mqtt:subscribe(USR, TopicFilter, SubOpts, ID); (_, _, {error, _} = Err) -> Err end, ok, Subs) of ok -> ok; {error, _} = Err1 -> unsubscribe(maps:keys(Subs), USR, #{}), Err1 end. -spec publish_will(state()) -> state(). publish_will(#state{will = #publish{} = Will, jid = #jid{} = JID} = State) -> case publish(State, Will) of {ok, _} -> ?DEBUG("Will of ~ts has been published to ~ts", [jid:encode(JID), Will#publish.topic]); {error, Why} -> ?WARNING_MSG("Failed to publish will of ~ts to ~ts: ~ts", [jid:encode(JID), Will#publish.topic, format_error(Why)]) end, State#state{will = undefined}; publish_will(State) -> State. -spec next_id(non_neg_integer()) -> pos_integer(). next_id(ID) -> (ID rem 65535) + 1. -spec set_dup_flag(mqtt_packet()) -> mqtt_packet(). set_dup_flag(#publish{qos = QoS} = Pkt) when QoS>0 -> Pkt#publish{dup = true}; set_dup_flag(Pkt) -> Pkt. -spec get_publish_code_props({ok, non_neg_integer()} | {error, error_reason()}) -> {reason_code(), properties()}. get_publish_code_props({ok, 0}) -> {'no-matching-subscribers', #{}}; get_publish_code_props({ok, _}) -> {success, #{}}; get_publish_code_props({error, Err}) -> Code = publish_reason_code(Err), Reason = format_reason_string(Err), {Code, #{reason_string => Reason}}. -spec err_args(undefined | jid:jid(), peername(), error_reason()) -> iolist(). err_args(undefined, IP, Reason) -> [ejabberd_config:may_hide_data(misc:ip_to_list(IP)), format_error(Reason)]; err_args(JID, IP, Reason) -> [jid:encode(JID), ejabberd_config:may_hide_data(misc:ip_to_list(IP)), format_error(Reason)]. -spec log_disconnection(state(), error_reason()) -> ok. log_disconnection(#state{jid = JID, peername = IP}, Reason) -> Msg = case JID of undefined -> "Rejected MQTT connection from ~ts: ~ts"; _ -> "Closing MQTT connection for ~ts from ~ts: ~ts" end, case Reason of {Tag, _} when Tag == replaced; Tag == resumed; Tag == socket -> ?DEBUG(Msg, err_args(JID, IP, Reason)); idle_connection -> ?DEBUG(Msg, err_args(JID, IP, Reason)); Tag when Tag == session_expired; Tag == shutdown -> ?INFO_MSG(Msg, err_args(JID, IP, Reason)); {peer_disconnected, Code, _} -> case mqtt_codec:is_error_code(Code) of true -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)); false -> ?DEBUG(Msg, err_args(JID, IP, Reason)) end; _ -> ?WARNING_MSG(Msg, err_args(JID, IP, Reason)) end. ejabberd-21.12/src/mod_proxy65_sql.erl0000644000232200023220000001046314154362354020203 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 30 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_sql). -behaviour(mod_proxy65). %% API -export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init() -> NodeS = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'proxy65' table...", []), case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from proxy65 where " "node_i=%(NodeS)s or node_t=%(NodeS)s")) of {updated, _} -> ok; Err -> ?ERROR_MSG("Failed to clean 'proxy65' table: ~p", [Err]), Err end. register_stream(SID, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), F = fun() -> case ejabberd_sql:sql_query_t( ?SQL("update proxy65 set pid_i=%(PidS)s, " "node_i=%(NodeS)s where sid=%(SID)s")) of {updated, 1} -> ok; _ -> ejabberd_sql:sql_query_t( ?SQL("insert into proxy65" "(sid, pid_t, node_t, pid_i, node_i, jid_i) " "values (%(SID)s, %(PidS)s, %(NodeS)s, '', '', '')")) end end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of {atomic, _} -> ok; {aborted, Reason} -> {error, Reason} end. unregister_stream(SID) -> F = fun() -> ejabberd_sql:sql_query_t( ?SQL("delete from proxy65 where sid=%(SID)s")) end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of {atomic, _} -> ok; {aborted, Reason} -> {error, Reason} end. activate_stream(SID, IJID, MaxConnections, _Node) -> F = fun() -> case ejabberd_sql:sql_query_t( ?SQL("select @(pid_t)s, @(node_t)s, @(pid_i)s, " "@(node_i)s, @(jid_i)s from proxy65 where " "sid=%(SID)s")) of {selected, [{TPidS, TNodeS, IPidS, INodeS, <<"">>}]} when IPidS /= <<"">> -> try {misc:decode_pid(TPidS, TNodeS), misc:decode_pid(IPidS, INodeS)} of {TPid, IPid} -> case ejabberd_sql:sql_query_t( ?SQL("update proxy65 set jid_i=%(IJID)s " "where sid=%(SID)s")) of {updated, 1} when is_integer(MaxConnections) -> case ejabberd_sql:sql_query_t( ?SQL("select @(count(*))d from proxy65 " "where jid_i=%(IJID)s")) of {selected, [{Num}]} when Num > MaxConnections -> ejabberd_sql:abort({limit, IPid, TPid}); {selected, _} -> {ok, IPid, TPid}; Err -> ejabberd_sql:abort(Err) end; {updated, _} -> {ok, IPid, TPid}; Err -> ejabberd_sql:abort(Err) end catch _:{bad_node, _} -> {error, notfound} end; {selected, [{_, _, _, _, JID}]} when JID /= <<"">> -> {error, conflict}; {selected, _} -> {error, notfound}; Err -> ejabberd_sql:abort(Err) end end, case ejabberd_sql:sql_transaction(ejabberd_config:get_myname(), F) of {atomic, Result} -> Result; {aborted, {limit, _, _} = Limit} -> {error, Limit}; {aborted, Reason} -> {error, Reason} end. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/ejabberd_stun.erl0000644000232200023220000001564614154362354017750 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_stun.erl %%% Author : Evgeny Khramtsov %%% Purpose : STUN RFC-5766 %%% Created : 8 May 2014 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2013-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_stun). -behaviour(ejabberd_listener). -protocol({rfc, 5766}). -protocol({xep, 176, '1.0'}). -ifndef(STUN). -include("logger.hrl"). -export([accept/1, start/3, start_link/3, listen_options/0]). fail() -> ?CRITICAL_MSG("Listening module ~ts is not available: " "ejabberd is not compiled with STUN/TURN support", [?MODULE]), erlang:error(stun_not_compiled). accept(_) -> fail(). listen_options() -> fail(). start(_, _, _) -> fail(). start_link(_, _, _) -> fail(). -else. -export([tcp_init/2, udp_init/2, udp_recv/5, start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0, get_password/2]). -include("logger.hrl"). -ifndef(LAGER). -export([stun_filter/2]). -define(STUN_MAX_LOG_LEVEL, notice). % Drop STUN/TURN info/debug messages. -endif. %%%=================================================================== %%% API %%%=================================================================== tcp_init(Socket, Opts) -> init_logger(), ejabberd:start_app(stun), stun:tcp_init(Socket, prepare_turn_opts(Opts)). -dialyzer({nowarn_function, udp_init/2}). udp_init(Socket, Opts) -> init_logger(), ejabberd:start_app(stun), stun:udp_init(Socket, prepare_turn_opts(Opts)). udp_recv(Socket, Addr, Port, Packet, Opts) -> stun:udp_recv(Socket, Addr, Port, Packet, Opts). start(SockMod, Socket, Opts) -> stun:start({SockMod, Socket}, Opts). start_link(_SockMod, Socket, Opts) -> stun:start_link(Socket, Opts). accept(_Pid) -> ok. get_password(User, Realm) -> case ejabberd_hooks:run_fold(stun_get_password, <<>>, [User, Realm]) of Password when byte_size(Password) > 0 -> Password; <<>> -> case ejabberd_auth:get_password_s(User, Realm) of Password when is_binary(Password) -> Password; _ -> ?INFO_MSG("Cannot use hashed password of ~s@~s for " "STUN/TURN authentication", [User, Realm]), <<>> end end. %%%=================================================================== %%% Internal functions %%%=================================================================== prepare_turn_opts(Opts) -> UseTurn = proplists:get_bool(use_turn, Opts), prepare_turn_opts(Opts, UseTurn). prepare_turn_opts(Opts, _UseTurn = false) -> set_certfile(Opts); prepare_turn_opts(Opts, _UseTurn = true) -> NumberOfMyHosts = length(ejabberd_option:hosts()), TurnIP = case proplists:get_value(turn_ipv4_address, Opts) of undefined -> MyIP = misc:get_my_ipv4_address(), case MyIP of {127, _, _, _} -> ?WARNING_MSG("Option 'turn_ipv4_address' is " "undefined and the server's hostname " "doesn't resolve to a public IPv4 " "address, most likely the TURN relay " "won't be working properly", []); _ -> ok end, [{turn_ipv4_address, MyIP}]; _ -> [] end, AuthFun = fun ejabberd_stun:get_password/2, Shaper = proplists:get_value(shaper, Opts, none), AuthType = proplists:get_value(auth_type, Opts, user), Realm = case proplists:get_value(auth_realm, Opts) of undefined when AuthType == user -> if NumberOfMyHosts > 1 -> ?INFO_MSG("You have several virtual hosts " "configured, but option 'auth_realm' is " "undefined and 'auth_type' is set to " "'user', so the TURN relay might not be " "working properly. Using ~ts as a " "fallback", [ejabberd_config:get_myname()]); true -> ok end, [{auth_realm, ejabberd_config:get_myname()}]; _ -> [] end, MaxRate = ejabberd_shaper:get_max_rate(Shaper), Opts1 = TurnIP ++ Realm ++ [{auth_fun, AuthFun},{shaper, MaxRate} | lists:keydelete(shaper, 1, Opts)], set_certfile(Opts1). set_certfile(Opts) -> case lists:keymember(certfile, 1, Opts) of true -> Opts; false -> Realm = proplists:get_value(auth_realm, Opts, ejabberd_config:get_myname()), case ejabberd_pkix:get_certfile(Realm) of {ok, CertFile} -> [{certfile, CertFile}|Opts]; error -> Opts end end. listen_opt_type(use_turn) -> econf:bool(); listen_opt_type(ip) -> econf:ip(); listen_opt_type(turn_ipv4_address) -> econf:ipv4(); listen_opt_type(turn_ipv6_address) -> econf:ipv6(); listen_opt_type(auth_type) -> econf:enum([anonymous, user]); listen_opt_type(auth_realm) -> econf:binary(); listen_opt_type(turn_min_port) -> econf:int(1025, 65535); listen_opt_type(turn_max_port) -> econf:int(1025, 65535); listen_opt_type(turn_max_allocations) -> econf:pos_int(infinity); listen_opt_type(turn_max_permissions) -> econf:pos_int(infinity); listen_opt_type(turn_blacklist) -> econf:list_or_single(econf:ip_mask()); listen_opt_type(server_name) -> econf:binary(); listen_opt_type(certfile) -> econf:pem(). listen_options() -> [{shaper, none}, {use_turn, false}, {turn_ipv4_address, undefined}, {turn_ipv6_address, undefined}, {auth_type, user}, {auth_realm, undefined}, {tls, false}, {certfile, undefined}, {turn_min_port, 49152}, {turn_max_port, 65535}, {turn_max_allocations, 10}, {turn_max_permissions, 10}, {turn_blacklist, [<<"127.0.0.0/8">>, <<"::1/128">>]}, {server_name, <<"ejabberd">>}]. -spec init_logger() -> ok. -ifdef(LAGER). init_logger() -> ok. -else. init_logger() -> case logger:add_primary_filter(ejabberd_stun, {fun ?MODULE:stun_filter/2, ?STUN_MAX_LOG_LEVEL}) of ok -> ok; {error, {already_exist, _}} -> ok end. -spec stun_filter(logger:log_event(), logger:level() | term()) -> logger:filter_return(). stun_filter(#{meta := #{domain := [stun | _]}, level := Level}, MaxLevel) -> case logger:compare_levels(Level, MaxLevel) of lt -> stop; _ -> ignore end; stun_filter(Event, _Extra) -> Event. -endif. -endif. ejabberd-21.12/src/nodetree_virtual.erl0000644000232200023220000000760514154362354020510 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : nodetree_virtual.erl %%% Author : Christophe Romain %%% Purpose : Standard node tree plugin using no storage backend %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the PubSub node tree plugin that %%% allow virtual nodes handling. This prevent storage of nodes. %%%

PubSub node tree plugins are using the {@link gen_nodetree} behaviour.

%%%

This plugin development is still a work in progress. Due to optimizations in %%% mod_pubsub, this plugin can not work anymore without altering functioning. %%% Please, send us comments, feedback and improvements.

-module(nodetree_virtual). -behaviour(gen_pubsub_nodetree). -author('christophe.romain@process-one.net'). -include("pubsub.hrl"). -export([init/3, terminate/2, options/0, set_node/1, get_node/3, get_node/2, get_node/1, get_nodes/2, get_nodes/1, get_all_nodes/1, get_parentnodes/3, get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/6, delete_node/2]). init(_Host, _ServerHost, _Opts) -> ok. terminate(_Host, _ServerHost) -> ok. options() -> [{virtual_tree, true}]. set_node(_Node) -> ok. get_node(Host, Node, _From) -> get_node(Host, Node). get_node(Host, Node) -> Nidx = nodeidx(Host, Node), node_record(Host, Node, Nidx). get_node(Nidx) -> {Host, Node} = nodeid(Nidx), node_record(Host, Node, Nidx). get_nodes(Host) -> get_nodes(Host, infinity). get_nodes(_Host, _Limit) -> []. get_all_nodes(_Host) -> []. get_parentnodes(_Host, _Node, _From) -> []. get_parentnodes_tree(Host, Node, From) -> [{0, [get_node(Host, Node, From)]}]. get_subnodes(_Host, _Node, _From) -> []. get_subnodes_tree(Host, Node, _From) -> get_subnodes_tree(Host, Node). get_subnodes_tree(_Host, _Node) -> []. create_node(Host, Node, _Type, _Owner, _Options, _Parents) -> {error, {virtual, nodeidx(Host, Node)}}. delete_node(Host, Node) -> [get_node(Host, Node)]. %% internal helper node_record({U,S,R}, Node, Nidx) -> Host = mod_pubsub:host(S), Type = <<"pep">>, Module = mod_pubsub:plugin(Host, Type), #pubsub_node{nodeid = {{U,S,R},Node}, id = Nidx, type = Type, owners = [{U,S,R}], options = Module:options()}; node_record(Host, Node, Nidx) -> [Type|_] = mod_pubsub:plugins(Host), Module = mod_pubsub:plugin(Host, Type), #pubsub_node{nodeid = {Host, Node}, id = Nidx, type = Type, owners = [{<<"">>, Host, <<"">>}], options = Module:options()}. nodeidx({U,S,R}, Node) -> JID = jid:encode(jid:make(U,S,R)), <>; nodeidx(Host, Node) -> <>. nodeid(Nidx) -> [Head, Node] = binary:split(Nidx, <<":">>), case jid:decode(Head) of {jid,<<>>,Host,<<>>,_,_,_} -> {Host, Node}; {jid,U,S,R,_,_,_} -> {{U,S,R}, Node} end. ejabberd-21.12/src/ejabberd_auth_external.erl0000644000232200023220000000671514154362354021617 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_external.erl %%% Author : Alexey Shchepin %%% Purpose : Authentication via LDAP external script %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_external). -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -export([start/1, stop/1, reload/1, set_password/3, check_password/4, try_register/3, user_exists/2, remove_user/2, store_type/1, plain_password_required/1]). -include("logger.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> extauth:start(Host). stop(Host) -> extauth:stop(Host). reload(Host) -> extauth:reload(Host). plain_password_required(_) -> true. store_type(_) -> external. check_password(User, AuthzId, Server, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> {nocache, false}; true -> check_password_extauth(User, AuthzId, Server, Password) end. set_password(User, Server, Password) -> case extauth:set_password(User, Server, Password) of Res when is_boolean(Res) -> {cache, {ok, Password}}; {error, Reason} -> failure(User, Server, set_password, Reason) end. try_register(User, Server, Password) -> case extauth:try_register(User, Server, Password) of true -> {cache, {ok, Password}}; false -> {cache, {error, not_allowed}}; {error, Reason} -> failure(User, Server, try_register, Reason) end. user_exists(User, Server) -> case extauth:user_exists(User, Server) of Res when is_boolean(Res) -> {cache, Res}; {error, Reason} -> failure(User, Server, user_exists, Reason) end. remove_user(User, Server) -> case extauth:remove_user(User, Server) of false -> {error, not_allowed}; true -> ok; {error, Reason} -> {_, Err} = failure(User, Server, remove_user, Reason), Err end. check_password_extauth(User, _AuthzId, Server, Password) -> if Password /= <<"">> -> case extauth:check_password(User, Server, Password) of Res when is_boolean(Res) -> {cache, Res}; {error, Reason} -> {Tag, _} = failure(User, Server, check_password, Reason), {Tag, false} end; true -> {nocache, false} end. -spec failure(binary(), binary(), atom(), any()) -> {nocache, {error, db_failure}}. failure(User, Server, Fun, Reason) -> ?ERROR_MSG("External authentication program failed when calling " "'~ts' for ~ts@~ts: ~p", [Fun, User, Server, Reason]), {nocache, {error, db_failure}}. ejabberd-21.12/src/mod_shared_roster_opt.erl0000644000232200023220000000262514154362354021517 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_shared_roster_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster, use_cache). ejabberd-21.12/src/ejabberd_app.erl0000644000232200023220000001260114154362354017523 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_app.erl %%% Author : Alexey Shchepin %%% Purpose : ejabberd's application callback module %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_app). -author('alexey@process-one.net'). -behaviour(application). -export([start/2, prep_stop/1, stop/1]). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). %%% %%% Application API %%% start(normal, _Args) -> try {T1, _} = statistics(wall_clock), ejabberd_logger:start(), write_pid_file(), start_included_apps(), start_elixir_application(), setup_if_elixir_conf_used(), case ejabberd_config:load() of ok -> ejabberd_mnesia:start(), file_queue_init(), maybe_add_nameservers(), case ejabberd_sup:start_link() of {ok, SupPid} -> ejabberd_system_monitor:start(), register_elixir_config_hooks(), ejabberd_cluster:wait_for_sync(infinity), ejabberd_hooks:run(ejabberd_started, []), ejabberd:check_apps(), ejabberd_systemd:ready(), {T2, _} = statistics(wall_clock), ?INFO_MSG("ejabberd ~ts is started in the node ~p in ~.2fs", [ejabberd_option:version(), node(), (T2-T1)/1000]), {ok, SupPid}; Err -> ?CRITICAL_MSG("Failed to start ejabberd application: ~p", [Err]), ejabberd:halt() end; Err -> ?CRITICAL_MSG("Failed to start ejabberd application: ~ts", [ejabberd_config:format_error(Err)]), ejabberd:halt() end catch throw:{?MODULE, Error} -> ?DEBUG("Failed to start ejabberd application: ~p", [Error]), ejabberd:halt() end; start(_, _) -> {error, badarg}. start_included_apps() -> {ok, Apps} = application:get_key(ejabberd, included_applications), lists:foreach( fun(mnesia) -> ok; (lager) -> ok; (os_mon)-> ok; (App) -> application:ensure_all_started(App) end, Apps). %% Prepare the application for termination. %% This function is called when an application is about to be stopped, %% before shutting down the processes of the application. prep_stop(State) -> ejabberd_systemd:stopping(), ejabberd_hooks:run(ejabberd_stopping, []), ejabberd_listener:stop(), ejabberd_sm:stop(), ejabberd_service:stop(), ejabberd_s2s:stop(), gen_mod:stop(), State. %% All the processes were killed when this function is called stop(_State) -> ?INFO_MSG("ejabberd ~ts is stopped in the node ~p", [ejabberd_option:version(), node()]), delete_pid_file(). %%% %%% Internal functions %%% %% If ejabberd is running on some Windows machine, get nameservers and add to Erlang maybe_add_nameservers() -> case os:type() of {win32, _} -> add_windows_nameservers(); _ -> ok end. add_windows_nameservers() -> IPTs = win32_dns:get_nameservers(), ?INFO_MSG("Adding machine's DNS IPs to Erlang system:~n~p", [IPTs]), lists:foreach(fun(IPT) -> inet_db:add_ns(IPT) end, IPTs). %%% %%% PID file %%% write_pid_file() -> case ejabberd:get_pid_file() of false -> ok; PidFilename -> write_pid_file(os:getpid(), PidFilename) end. write_pid_file(Pid, PidFilename) -> case file:write_file(PidFilename, io_lib:format("~ts~n", [Pid])) of ok -> ok; {error, Reason} = Err -> ?CRITICAL_MSG("Cannot write PID file ~ts: ~ts", [PidFilename, file:format_error(Reason)]), throw({?MODULE, Err}) end. delete_pid_file() -> case ejabberd:get_pid_file() of false -> ok; PidFilename -> file:delete(PidFilename) end. file_queue_init() -> QueueDir = case ejabberd_option:queue_dir() of undefined -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "queue"); Path -> Path end, case p1_queue:start(QueueDir) of ok -> ok; Err -> throw({?MODULE, Err}) end. -ifdef(ELIXIR_ENABLED). is_using_elixir_config() -> Config = ejabberd_config:path(), 'Elixir.Ejabberd.ConfigUtil':is_elixir_config(Config). setup_if_elixir_conf_used() -> case is_using_elixir_config() of true -> 'Elixir.Ejabberd.Config.Store':start_link(); false -> ok end. register_elixir_config_hooks() -> case 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; {error, _Msg} -> ?ERROR_MSG("Elixir application not started.", []) end. -else. setup_if_elixir_conf_used() -> ok. register_elixir_config_hooks() -> ok. start_elixir_application() -> ok. -endif. ejabberd-21.12/src/ejabberd_cluster_mnesia.erl0000644000232200023220000001121114154362354021754 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_cluster_mnesia.erl %%% Author : Christophe Romain %%% Purpose : ejabberd clustering management via Mnesia %%% Created : 7 Oct 2015 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_cluster_mnesia). -behaviour(ejabberd_cluster). %% API -export([init/0, get_nodes/0, join/1, leave/1, get_known_nodes/0, node_id/0, get_node_by_id/1, send/2, wait_for_sync/1, subscribe/1]). -include("logger.hrl"). -spec init() -> ok. init() -> ok. -spec get_nodes() -> [node()]. get_nodes() -> mnesia:system_info(running_db_nodes). -spec get_known_nodes() -> [node()]. get_known_nodes() -> lists:usort(mnesia:system_info(db_nodes) ++ mnesia:system_info(extra_db_nodes)). -spec join(node()) -> ok | {error, any()}. join(Node) -> case {node(), net_adm:ping(Node)} of {Node, _} -> {error, {not_master, Node}}; {_, pong} -> application:stop(ejabberd), application:stop(mnesia), mnesia:delete_schema([node()]), application:start(mnesia), case mnesia:change_config(extra_db_nodes, [Node]) of {ok, _} -> replicate_database(Node), wait_for_sync(infinity), application:stop(mnesia), application:start(ejabberd); {error, Reason} -> {error, Reason} end; _ -> {error, {no_ping, Node}} end. -spec leave(node()) -> ok | {error, any()}. leave(Node) -> case {node(), net_adm:ping(Node)} of {Node, _} -> Cluster = get_nodes()--[Node], leave(Cluster, Node); {_, pong} -> rpc:call(Node, ?MODULE, leave, [Node], 10000); {_, pang} -> case mnesia:del_table_copy(schema, Node) of {atomic, ok} -> ok; {aborted, Reason} -> {error, Reason} end end. leave([], Node) -> {error, {no_cluster, Node}}; leave([Master|_], Node) -> application:stop(ejabberd), application:stop(mnesia), spawn(fun() -> rpc:call(Master, mnesia, del_table_copy, [schema, Node]), mnesia:delete_schema([node()]), erlang:halt(0) end), ok. -spec node_id() -> binary(). node_id() -> integer_to_binary(erlang:phash2(node())). -spec get_node_by_id(binary()) -> node(). get_node_by_id(Hash) -> try binary_to_integer(Hash) of I -> match_node_id(I) catch _:_ -> node() end. -spec send({atom(), node()}, term()) -> boolean(). send(Dst, Msg) -> case erlang:send(Dst, Msg, [nosuspend, noconnect]) of ok -> true; _ -> false end. -spec wait_for_sync(timeout()) -> ok. wait_for_sync(Timeout) -> ?INFO_MSG("Waiting for Mnesia synchronization to complete", []), mnesia:wait_for_tables(mnesia:system_info(local_tables), Timeout), ok. -spec subscribe(_) -> ok. subscribe(_) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== replicate_database(Node) -> mnesia:change_table_copy_type(schema, node(), disc_copies), lists:foreach( fun(Table) -> Type = rpc:call(Node, mnesia, table_info, [Table, storage_type]), mnesia:add_table_copy(Table, node(), Type) end, mnesia:system_info(tables)--[schema]). -spec match_node_id(integer()) -> node(). match_node_id(I) -> match_node_id(I, get_nodes()). -spec match_node_id(integer(), [node()]) -> node(). match_node_id(I, [Node|Nodes]) -> case erlang:phash2(Node) of I -> Node; _ -> match_node_id(I, Nodes) end; match_node_id(_I, []) -> node(). ejabberd-21.12/src/mod_mam_mnesia.erl0000644000232200023220000002115414154362354020075 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_mam_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_mam_mnesia). -behaviour(mod_mam). %% API -export([init/2, remove_user/2, remove_room/3, delete_old_messages/3, extended_fields/0, store/8, write_prefs/4, get_prefs/2, select/6, remove_from_archive/3, is_empty_for_user/2, is_empty_for_room/3]). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("mod_mam.hrl"). -define(BIN_GREATER_THAN(A, B), ((A > B andalso byte_size(A) == byte_size(B)) orelse byte_size(A) > byte_size(B))). -define(BIN_LESS_THAN(A, B), ((A < B andalso byte_size(A) == byte_size(B)) orelse byte_size(A) < byte_size(B))). -define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB. %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> try {atomic, _} = ejabberd_mnesia:create( ?MODULE, archive_msg, [{disc_only_copies, [node()]}, {type, bag}, {attributes, record_info(fields, archive_msg)}]), {atomic, _} = ejabberd_mnesia:create( ?MODULE, archive_prefs, [{disc_only_copies, [node()]}, {attributes, record_info(fields, archive_prefs)}]), ok catch _:{badmatch, _} -> {error, db_failure} end. remove_user(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> mnesia:delete({archive_msg, US}), mnesia:delete({archive_prefs, US}) end, mnesia:transaction(F). remove_room(_LServer, LName, LHost) -> remove_user(LName, LHost). remove_from_archive(LUser, LServer, none) -> US = {LUser, LServer}, case mnesia:transaction(fun () -> mnesia:delete({archive_msg, US}) end) of {atomic, _} -> ok; {aborted, Reason} -> {error, Reason} end; remove_from_archive(LUser, LServer, WithJid) -> US = {LUser, LServer}, Peer = jid:remove_resource(jid:split(WithJid)), F = fun () -> Msgs = mnesia:select( archive_msg, ets:fun2ms( fun(#archive_msg{us = US1, bare_peer = Peer1} = Msg) when US1 == US, Peer1 == Peer -> Msg end)), lists:foreach(fun mnesia:delete_object/1, Msgs) end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> {error, Reason} end. delete_old_messages(global, TimeStamp, Type) -> mnesia:change_table_copy_type(archive_msg, node(), disc_copies), Result = delete_old_user_messages(mnesia:dirty_first(archive_msg), TimeStamp, Type), mnesia:change_table_copy_type(archive_msg, node(), disc_only_copies), Result. delete_old_user_messages('$end_of_table', _TimeStamp, _Type) -> ok; delete_old_user_messages(User, TimeStamp, Type) -> F = fun() -> Msgs = mnesia:read(archive_msg, User), Keep = lists:filter( fun(#archive_msg{timestamp = MsgTS, type = MsgType}) -> MsgTS >= TimeStamp orelse (Type /= all andalso Type /= MsgType) end, Msgs), if length(Keep) < length(Msgs) -> mnesia:delete({archive_msg, User}), lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep); true -> ok end end, NextRecord = mnesia:dirty_next(archive_msg, User), case mnesia:transaction(F) of {atomic, ok} -> delete_old_user_messages(NextRecord, TimeStamp, Type); {aborted, Err} -> ?ERROR_MSG("Cannot delete old MAM messages: ~ts", [Err]), Err end. extended_fields() -> []. store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS) -> case {mnesia:table_info(archive_msg, disc_only_copies), mnesia:table_info(archive_msg, memory)} of {[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT -> ?ERROR_MSG("MAM archives too large, won't store message for ~ts@~ts", [LUser, LServer]), {error, overflow}; _ -> LPeer = {PUser, PServer, _} = jid:tolower(Peer), F = fun() -> mnesia:write( #archive_msg{us = {LUser, LServer}, id = integer_to_binary(TS), timestamp = misc:usec_to_now(TS), peer = LPeer, bare_peer = {PUser, PServer, <<>>}, type = Type, nick = Nick, packet = Pkt}) end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Err} -> ?ERROR_MSG("Cannot add message to MAM archive of ~ts@~ts: ~ts", [LUser, LServer, Err]), Err end end. write_prefs(_LUser, _LServer, Prefs, _ServerHost) -> mnesia:dirty_write(Prefs). get_prefs(LUser, LServer) -> case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of [Prefs] -> {ok, Prefs}; _ -> error end. select(_LServer, JidRequestor, #jid{luser = LUser, lserver = LServer} = JidArchive, Query, RSM, MsgType) -> Start = proplists:get_value(start, Query), End = proplists:get_value('end', Query), With = proplists:get_value(with, Query), LWith = if With /= undefined -> jid:tolower(With); true -> undefined end, MS = make_matchspec(LUser, LServer, Start, End, LWith), Msgs = mnesia:dirty_select(archive_msg, MS), SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs), {FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM), Count = length(Msgs), Result = {lists:flatmap( fun(Msg) -> case mod_mam:msg_to_el( Msg, MsgType, JidRequestor, JidArchive) of {ok, El} -> [{Msg#archive_msg.id, binary_to_integer(Msg#archive_msg.id), El}]; {error, _} -> [] end end, FilteredMsgs), IsComplete, Count}, erlang:garbage_collect(), Result. is_empty_for_user(LUser, LServer) -> mnesia:dirty_read(archive_msg, {LUser, LServer}) == []. is_empty_for_room(_LServer, LName, LHost) -> is_empty_for_user(LName, LHost). %%%=================================================================== %%% Internal functions %%%=================================================================== make_matchspec(LUser, LServer, Start, undefined, With) -> %% List is always greater than a tuple make_matchspec(LUser, LServer, Start, [], With); make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) -> ets:fun2ms( fun(#archive_msg{timestamp = TS, us = US, bare_peer = BPeer} = Msg) when Start =< TS, End >= TS, US == {LUser, LServer}, BPeer == With -> Msg end); make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) -> ets:fun2ms( fun(#archive_msg{timestamp = TS, us = US, peer = Peer} = Msg) when Start =< TS, End >= TS, US == {LUser, LServer}, Peer == With -> Msg end); make_matchspec(LUser, LServer, Start, End, undefined) -> ets:fun2ms( fun(#archive_msg{timestamp = TS, us = US, peer = Peer} = Msg) when Start =< TS, End >= TS, US == {LUser, LServer} -> Msg end). filter_by_rsm(Msgs, undefined) -> {Msgs, true}; filter_by_rsm(_Msgs, #rsm_set{max = Max}) when Max < 0 -> {[], true}; filter_by_rsm(Msgs, #rsm_set{max = Max, before = Before, 'after' = After}) -> NewMsgs = if is_binary(After), After /= <<"">> -> lists:filter( fun(#archive_msg{id = I}) -> ?BIN_GREATER_THAN(I, After) end, Msgs); is_binary(Before), Before /= <<"">> -> lists:foldl( fun(#archive_msg{id = I} = Msg, Acc) when ?BIN_LESS_THAN(I, Before) -> [Msg|Acc]; (_, Acc) -> Acc end, [], Msgs); is_binary(Before), Before == <<"">> -> lists:reverse(Msgs); true -> Msgs end, filter_by_max(NewMsgs, Max). filter_by_max(Msgs, undefined) -> {Msgs, true}; filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> {lists:sublist(Msgs, Len), length(Msgs) =< Len}; filter_by_max(_Msgs, _Junk) -> {[], true}. ejabberd-21.12/src/mod_http_fileserver_opt.erl0000644000232200023220000000414214154362354022054 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_http_fileserver_opt). -export([accesslog/1]). -export([content_types/1]). -export([custom_headers/1]). -export([default_content_type/1]). -export([directory_indices/1]). -export([docroot/1]). -export([must_authenticate_with/1]). -spec accesslog(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). accesslog(Opts) when is_map(Opts) -> gen_mod:get_opt(accesslog, Opts); accesslog(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, accesslog). -spec content_types(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. content_types(Opts) when is_map(Opts) -> gen_mod:get_opt(content_types, Opts); content_types(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, content_types). -spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. custom_headers(Opts) when is_map(Opts) -> gen_mod:get_opt(custom_headers, Opts); custom_headers(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, custom_headers). -spec default_content_type(gen_mod:opts() | global | binary()) -> binary(). default_content_type(Opts) when is_map(Opts) -> gen_mod:get_opt(default_content_type, Opts); default_content_type(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, default_content_type). -spec directory_indices(gen_mod:opts() | global | binary()) -> [binary()]. directory_indices(Opts) when is_map(Opts) -> gen_mod:get_opt(directory_indices, Opts); directory_indices(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, directory_indices). -spec docroot(gen_mod:opts() | global | binary()) -> binary(). docroot(Opts) when is_map(Opts) -> gen_mod:get_opt(docroot, Opts); docroot(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, docroot). -spec must_authenticate_with(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. must_authenticate_with(Opts) when is_map(Opts) -> gen_mod:get_opt(must_authenticate_with, Opts); must_authenticate_with(Host) -> gen_mod:get_module_opt(Host, mod_http_fileserver, must_authenticate_with). ejabberd-21.12/src/mod_mqtt.erl0000644000232200023220000005552114154362354016761 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mod_mqtt). -behaviour(p1_server). -behaviour(gen_mod). -behaviour(ejabberd_listener). -dialyzer({no_improper_lists, join_filter/1}). %% gen_mod API -export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_opt_type/1]). -export([mod_doc/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% ejabberd_listener API -export([start/3, start_link/3, listen_opt_type/1, listen_options/0, accept/1]). %% ejabberd_http API -export([socket_handoff/3]). %% Legacy ejabberd_listener API -export([become_controller/2, socket_type/0]). %% API -export([open_session/1, close_session/1, lookup_session/1, publish/3, subscribe/4, unsubscribe/2, select_retained/4, check_publish_access/2, check_subscribe_access/2]). %% ejabberd_hooks -export([remove_user/2]). -include("logger.hrl"). -include("mqtt.hrl"). -include("translate.hrl"). -define(MQTT_TOPIC_CACHE, mqtt_topic_cache). -define(MQTT_PAYLOAD_CACHE, mqtt_payload_cache). -type continuation() :: term(). -type seconds() :: non_neg_integer(). %% RAM backend callbacks -callback init() -> ok | {error, any()}. -callback open_session(jid:ljid()) -> ok | {error, db_failure}. -callback close_session(jid:ljid()) -> ok | {error, db_failure}. -callback lookup_session(jid:ljid()) -> {ok, pid()} | {error, notfound | db_failure}. -callback get_sessions(binary(), binary()) -> [jid:ljid()]. -callback subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> ok | {error, db_failure}. -callback unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. -callback find_subscriber(binary(), binary() | continuation()) -> {ok, {pid(), qos()}, continuation()} | {error, notfound | db_failure}. %% Disc backend callbacks -callback init(binary(), gen_mod:opts()) -> ok | {error, any()}. -callback publish(jid:ljid(), binary(), binary(), qos(), properties(), seconds()) -> ok | {error, db_failure}. -callback delete_published(jid:ljid(), binary()) -> ok | {error, db_failure}. -callback lookup_published(jid:ljid(), binary()) -> {ok, {binary(), qos(), properties(), seconds()}} | {error, notfound | db_failure}. -callback list_topics(binary()) -> {ok, [binary()]} | {error, db_failure}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). -record(state, {host :: binary()}). %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Sock, ListenOpts) -> mod_mqtt_session:start(SockMod, Sock, ListenOpts). start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). start_link(SockMod, Sock, ListenOpts) -> mod_mqtt_session:start_link(SockMod, Sock, ListenOpts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. socket_type() -> raw. become_controller(Pid, _) -> accept(Pid). accept(Pid) -> mod_mqtt_session:accept(Pid). socket_handoff(LocalPath, Request, Opts) -> mod_mqtt_ws:socket_handoff(LocalPath, Request, Opts). open_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:open_session({U, S, R}). close_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:close_session({U, S, R}). lookup_session({U, S, R}) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:lookup_session({U, S, R}). -spec publish(jid:ljid(), publish(), seconds()) -> {ok, non_neg_integer()} | {error, db_failure | publish_forbidden}. publish({_, S, _} = USR, Pkt, ExpiryTime) -> case check_publish_access(Pkt#publish.topic, USR) of allow -> case retain(USR, Pkt, ExpiryTime) of ok -> Mod = gen_mod:ram_db_mod(S, ?MODULE), route(Mod, S, Pkt, ExpiryTime); {error, _} = Err -> Err end; deny -> {error, publish_forbidden} end. -spec subscribe(jid:ljid(), binary(), sub_opts(), non_neg_integer()) -> ok | {error, db_failure | subscribe_forbidden}. subscribe({_, S, _} = USR, TopicFilter, SubOpts, ID) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Limit = mod_mqtt_opt:max_topic_depth(S), case check_topic_depth(TopicFilter, Limit) of allow -> case check_subscribe_access(TopicFilter, USR) of allow -> Mod:subscribe(USR, TopicFilter, SubOpts, ID); deny -> {error, subscribe_forbidden} end; deny -> {error, subscribe_forbidden} end. -spec unsubscribe(jid:ljid(), binary()) -> ok | {error, notfound | db_failure}. unsubscribe({U, S, R}, Topic) -> Mod = gen_mod:ram_db_mod(S, ?MODULE), Mod:unsubscribe({U, S, R}, Topic). -spec select_retained(jid:ljid(), binary(), qos(), non_neg_integer()) -> [{publish(), seconds()}]. select_retained({_, S, _} = USR, TopicFilter, QoS, SubID) -> Mod = gen_mod:db_mod(S, ?MODULE), Limit = mod_mqtt_opt:match_retained_limit(S), select_retained(Mod, USR, TopicFilter, QoS, SubID, Limit). remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:ram_db_mod(LServer, ?MODULE), Sessions = Mod:get_sessions(LUser, LServer), [close_session(Session) || Session <- Sessions]. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host|_]) -> Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Opts, ?MODULE), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), try ok = Mod:init(Host, Opts), ok = RMod:init(), ok = init_cache(Mod, Host, Opts), {ok, #state{host = Host}} catch _:{badmatch, {error, Why}} -> {stop, Why} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Options %%%=================================================================== -spec mod_options(binary()) -> [{access_publish, [{[binary()], acl:acl()}]} | {access_subscribe, [{[binary()], acl:acl()}]} | {atom(), any()}]. mod_options(Host) -> [{match_retained_limit, 1000}, {max_topic_depth, 8}, {max_topic_aliases, 100}, {session_expiry, timer:minutes(5)}, {max_queue, 5000}, {access_subscribe, []}, {access_publish, []}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, {queue_type, ejabberd_option:queue_type(Host)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_opt_type(max_queue) -> econf:pos_int(unlimited); mod_opt_type(session_expiry) -> econf:either( econf:int(0, 0), econf:timeout(second)); mod_opt_type(match_retained_limit) -> econf:pos_int(infinity); mod_opt_type(max_topic_depth) -> econf:pos_int(infinity); mod_opt_type(max_topic_aliases) -> econf:int(0, 65535); mod_opt_type(access_subscribe) -> topic_access_validator(); mod_opt_type(access_publish) -> topic_access_validator(); mod_opt_type(queue_type) -> econf:queue_type(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(ram_db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). listen_opt_type(tls_verify) -> econf:bool(); listen_opt_type(max_payload_size) -> econf:pos_int(infinity). listen_options() -> [{max_fsm_queue, 10000}, {max_payload_size, infinity}, {tls, false}, {tls_verify, false}]. %%%=================================================================== %%% Doc %%%=================================================================== mod_doc() -> #{desc => ?T("This module adds " "https://docs.ejabberd.im/admin/guide/mqtt/[support for the MQTT] " "protocol version '3.1.1' and '5.0'. Remember to configure " "'mod_mqtt' in 'modules' and 'listen' sections."), opts => [{access_subscribe, #{value => "{TopicFilter: AccessName}", desc => ?T("Access rules to restrict access to topics " "for subscribers. By default there are no restrictions.")}}, {access_publish, #{value => "{TopicFilter: AccessName}", desc => ?T("Access rules to restrict access to topics " "for publishers. By default there are no restrictions.")}}, {max_queue, #{value => ?T("Size"), desc => ?T("Maximum queue size for outgoing packets. " "The default value is '5000'.")}}, {session_expiry, #{value => "timeout()", desc => ?T("The option specifies how long to wait for " "an MQTT session resumption. When '0' is set, " "the session gets destroyed when the underlying " "client connection is closed. The default value is " "'5' minutes.")}}, {max_topic_depth, #{value => ?T("Depth"), desc => ?T("The maximum topic depth, i.e. the number of " "slashes ('/') in the topic. The default " "value is '8'.")}}, {max_topic_aliases, #{value => "0..65535", desc => ?T("The maximum number of aliases a client " "is able to associate with the topics. " "The default value is '100'.")}}, {match_retained_limit, #{value => "pos_integer() | infinity", desc => ?T("The option limits the number of retained messages " "returned to a client when it subscribes to some " "topic filter. The default value is '1000'.")}}, {queue_type, #{value => "ram | file", desc => ?T("Same as top-level _`queue_type`_ option, " "but applied to this module only.")}}, {ram_db_type, #{value => "mnesia", desc => ?T("Same as top-level _`default_ram_db`_ option, " "but applied to this module only.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, " "but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, " "but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, " "but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, " "but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, " "but applied to this module only.")}}]}. %%%=================================================================== %%% Internal functions %%%=================================================================== route(Mod, LServer, Pkt, ExpiryTime) -> route(Mod, LServer, Pkt, ExpiryTime, Pkt#publish.topic, 0). route(Mod, LServer, Pkt, ExpiryTime, Continuation, Num) -> case Mod:find_subscriber(LServer, Continuation) of {ok, {Pid, #sub_opts{no_local = true}, _}, Continuation1} when Pid == self() -> route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num); {ok, {Pid, SubOpts, ID}, Continuation1} -> ?DEBUG("Route to ~p: ~ts", [Pid, Pkt#publish.topic]), MinQoS = min(SubOpts#sub_opts.qos, Pkt#publish.qos), Retain = case SubOpts#sub_opts.retain_as_published of false -> false; true -> Pkt#publish.retain end, Props = set_sub_id(ID, Pkt#publish.properties), mod_mqtt_session:route( Pid, {Pkt#publish{qos = MinQoS, dup = false, retain = Retain, properties = Props}, ExpiryTime}), route(Mod, LServer, Pkt, ExpiryTime, Continuation1, Num+1); {error, _} -> {ok, Num} end. select_retained(Mod, {_, LServer, _} = USR, TopicFilter, QoS, SubID, Limit) -> Topics = match_topics(TopicFilter, LServer, Limit), lists:filtermap( fun({{Filter, _}, Topic}) -> case lookup_published(Mod, USR, Topic) of {ok, {Payload, QoS1, Props, ExpiryTime}} -> Props1 = set_sub_id(SubID, Props), {true, {#publish{topic = Topic, payload = Payload, retain = true, properties = Props1, qos = min(QoS, QoS1)}, ExpiryTime}}; error -> ets:delete(?MQTT_TOPIC_CACHE, {Filter, LServer}), false; _ -> false end end, Topics). match_topics(Topic, LServer, Limit) -> Filter = topic_filter(Topic), case Limit of infinity -> ets:match_object(?MQTT_TOPIC_CACHE, {{Filter, LServer}, '_'}); _ -> case ets:select(?MQTT_TOPIC_CACHE, [{{{Filter, LServer}, '_'}, [], ['$_']}], Limit) of {Topics, _} -> Topics; '$end_of_table' -> [] end end. retain({_, S, _} = USR, #publish{retain = true, topic = Topic, payload = Data, qos = QoS, properties = Props}, ExpiryTime) -> Mod = gen_mod:db_mod(S, ?MODULE), TopicKey = topic_key(Topic), case Data of <<>> -> ets:delete(?MQTT_TOPIC_CACHE, {TopicKey, S}), case use_cache(Mod, S) of true -> ets_cache:delete(?MQTT_PAYLOAD_CACHE, {S, Topic}, cache_nodes(Mod, S)); false -> ok end, Mod:delete_published(USR, Topic); _ -> ets:insert(?MQTT_TOPIC_CACHE, {{TopicKey, S}, Topic}), case use_cache(Mod, S) of true -> case ets_cache:update( ?MQTT_PAYLOAD_CACHE, {S, Topic}, {ok, {Data, QoS, Props, ExpiryTime}}, fun() -> Mod:publish(USR, Topic, Data, QoS, Props, ExpiryTime) end, cache_nodes(Mod, S)) of {ok, _} -> ok; {error, _} = Err -> Err end; false -> Mod:publish(USR, Topic, Data, QoS, Props, ExpiryTime) end end; retain(_, _, _) -> ok. lookup_published(Mod, {_, LServer, _} = USR, Topic) -> case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?MQTT_PAYLOAD_CACHE, {LServer, Topic}, fun() -> Mod:lookup_published(USR, Topic) end); false -> Mod:lookup_published(USR, Topic) end. set_sub_id(0, Props) -> Props; set_sub_id(ID, Props) -> Props#{subscription_identifier => [ID]}. %%%=================================================================== %%% Matching functions %%%=================================================================== topic_key(S) -> Parts = split_path(S), case join_key(Parts) of [<<>>|T] -> T; T -> T end. topic_filter(S) -> Parts = split_path(S), case join_filter(Parts) of [<<>>|T] -> T; T -> T end. join_key([X,Y|T]) -> [X, $/|join_key([Y|T])]; join_key([X]) -> [X]; join_key([]) -> []. join_filter([X, <<$#>>]) -> [wildcard(X)|'_']; join_filter([X,Y|T]) -> [wildcard(X), $/|join_filter([Y|T])]; join_filter([<<>>]) -> []; join_filter([<<$#>>]) -> '_'; join_filter([X]) -> [wildcard(X)]; join_filter([]) -> []. wildcard(<<$+>>) -> '_'; wildcard(Bin) -> Bin. check_topic_depth(_Topic, infinity) -> allow; check_topic_depth(_, N) when N=<0 -> deny; check_topic_depth(<<$/, T/binary>>, N) -> check_topic_depth(T, N-1); check_topic_depth(<<_, T/binary>>, N) -> check_topic_depth(T, N); check_topic_depth(<<>>, _) -> allow. split_path(Path) -> binary:split(Path, <<$/>>, [global]). %%%=================================================================== %%% Validators %%%=================================================================== -spec topic_access_validator() -> econf:validator(). topic_access_validator() -> econf:and_then( econf:map( fun(TF) -> try split_path(mqtt_codec:topic_filter(TF)) catch _:{mqtt_codec, _} = Reason -> econf:fail(Reason) end end, econf:acl(), [{return, orddict}]), fun lists:reverse/1). %%%=================================================================== %%% ACL checks %%%=================================================================== check_subscribe_access(Topic, {_, S, _} = USR) -> Rules = mod_mqtt_opt:access_subscribe(S), check_access(Topic, USR, Rules). check_publish_access(<<$$, _/binary>>, _) -> deny; check_publish_access(Topic, {_, S, _} = USR) -> Rules = mod_mqtt_opt:access_publish(S), check_access(Topic, USR, Rules). check_access(_, _, []) -> allow; check_access(Topic, {U, S, R} = USR, FilterRules) -> TopicParts = binary:split(Topic, <<$/>>, [global]), case lists:any( fun({FilterParts, Rule}) -> case match(TopicParts, FilterParts, U, S, R) of true -> allow == acl:match_rule(S, Rule, USR); false -> false end end, FilterRules) of true -> allow; false -> deny end. match(_, [<<"#">>|_], _, _, _) -> true; match([], [<<>>, <<"#">>|_], _, _, _) -> true; match([_|T1], [<<"+">>|T2], U, S, R) -> match(T1, T2, U, S, R); match([H|T1], [<<"%u">>|T2], U, S, R) -> case jid:nodeprep(H) of U -> match(T1, T2, U, S, R); _ -> false end; match([H|T1], [<<"%d">>|T2], U, S, R) -> case jid:nameprep(H) of S -> match(T1, T2, U, S, R); _ -> false end; match([H|T1], [<<"%c">>|T2], U, S, R) -> case jid:resourceprep(H) of R -> match(T1, T2, U, S, R); _ -> false end; match([H|T1], [<<"%g">>|T2], U, S, R) -> case jid:resourceprep(H) of H -> case acl:loaded_shared_roster_module(S) of undefined -> false; Mod -> case Mod:get_group_opts(S, H) of error -> false; _ -> case Mod:is_user_in_group({U, S}, H, S) of true -> match(T1, T2, U, S, R); _ -> false end end end; _ -> false end; match([H|T1], [H|T2], U, S, R) -> match(T1, T2, U, S, R); match([], [], _, _, _) -> true; match(_, _, _, _, _) -> false. %%%=================================================================== %%% Cache stuff %%%=================================================================== -spec init_cache(module(), binary(), gen_mod:opts()) -> ok | {error, db_failure}. init_cache(Mod, Host, Opts) -> init_payload_cache(Mod, Host, Opts), init_topic_cache(Mod, Host). -spec init_topic_cache(module(), binary()) -> ok | {error, db_failure}. init_topic_cache(Mod, Host) -> catch ets:new(?MQTT_TOPIC_CACHE, [named_table, ordered_set, public, {heir, erlang:group_leader(), none}]), ?INFO_MSG("Building MQTT cache for ~ts, this may take a while", [Host]), case Mod:list_topics(Host) of {ok, Topics} -> lists:foreach( fun(Topic) -> ets:insert(?MQTT_TOPIC_CACHE, {{topic_key(Topic), Host}, Topic}) end, Topics); {error, _} = Err -> Err end. -spec init_payload_cache(module(), binary(), gen_mod:opts()) -> ok. init_payload_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?MQTT_PAYLOAD_CACHE, CacheOpts); false -> ets_cache:delete(?MQTT_PAYLOAD_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_mqtt_opt:cache_size(Opts), CacheMissed = mod_mqtt_opt:cache_missed(Opts), LifeTime = mod_mqtt_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_mqtt_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. ejabberd-21.12/src/mod_push_keepalive_opt.erl0000644000232200023220000000167214154362354021660 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_push_keepalive_opt). -export([resume_timeout/1]). -export([wake_on_start/1]). -export([wake_on_timeout/1]). -spec resume_timeout(gen_mod:opts() | global | binary()) -> non_neg_integer(). resume_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(resume_timeout, Opts); resume_timeout(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, resume_timeout). -spec wake_on_start(gen_mod:opts() | global | binary()) -> boolean(). wake_on_start(Opts) when is_map(Opts) -> gen_mod:get_opt(wake_on_start, Opts); wake_on_start(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_start). -spec wake_on_timeout(gen_mod:opts() | global | binary()) -> boolean(). wake_on_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(wake_on_timeout, Opts); wake_on_timeout(Host) -> gen_mod:get_module_opt(Host, mod_push_keepalive, wake_on_timeout). ejabberd-21.12/src/mod_offline_sql.erl0000644000232200023220000002066014154362354020271 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_offline_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 15 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_offline_sql). -behaviour(mod_offline). -export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1, remove_old_messages/2, remove_user/2, read_message_headers/2, read_message/3, remove_message/3, read_all_messages/2, remove_all_messages/2, count_messages/2, import/1, export/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_offline.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. store_message(#offline_msg{us = {LUser, LServer}} = M) -> From = M#offline_msg.from, To = M#offline_msg.to, Packet = xmpp:set_from_to(M#offline_msg.packet, From, To), NewPacket = misc:add_delay_info( Packet, jid:make(LServer), M#offline_msg.timestamp, <<"Offline Storage">>), XML = fxml:element_to_binary( xmpp:encode(NewPacket)), case ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "spool", ["username=%(LUser)s", "server_host=%(LServer)s", "xml=%(XML)s"])) of {updated, _} -> ok; _ -> {error, db_failure} end. pop_messages(LUser, LServer) -> case get_and_del_spool_msg_t(LServer, LUser) of {atomic, {selected, Rs}} -> {ok, lists:flatmap( fun({_, XML}) -> case xml_to_offline_msg(XML) of {ok, Msg} -> [Msg]; _Err -> [] end end, Rs)}; Err -> {error, Err} end. remove_expired_messages(_LServer) -> %% TODO {atomic, ok}. remove_old_messages(Days, LServer) -> case ejabberd_sql:sql_query( LServer, fun(pgsql, _) -> ejabberd_sql:sql_query_t( ?SQL("DELETE FROM spool" " WHERE created_at <" " NOW() - %(Days)d * INTERVAL '1 DAY'")); (sqlite, _) -> ejabberd_sql:sql_query_t( ?SQL("DELETE FROM spool" " WHERE created_at <" " DATETIME('now', '-%(Days)d days')")); (_, _) -> ejabberd_sql:sql_query_t( ?SQL("DELETE FROM spool" " WHERE created_at < NOW() - INTERVAL %(Days)d DAY")) end) of {updated, N} -> ?INFO_MSG("~p message(s) deleted from offline spool", [N]); _Error -> ?ERROR_MSG("Cannot delete message in offline spool: ~p", [_Error]) end, {atomic, ok}. remove_user(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from spool where username=%(LUser)s and %(LServer)H")). read_message_headers(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s, @(seq)d from spool" " where username=%(LUser)s and %(LServer)H order by seq")) of {selected, Rows} -> lists:flatmap( fun({XML, Seq}) -> case xml_to_offline_msg(XML) of {ok, #offline_msg{from = From, to = To, timestamp = TS, packet = El}} -> [{Seq, From, To, TS, El}]; _ -> [] end end, Rows); _Err -> error end. read_message(LUser, LServer, Seq) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s from spool where username=%(LUser)s" " and %(LServer)H" " and seq=%(Seq)d")) of {selected, [{RawXML}|_]} -> case xml_to_offline_msg(RawXML) of {ok, Msg} -> {ok, Msg}; _ -> error end; _ -> error end. remove_message(LUser, LServer, Seq) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from spool where username=%(LUser)s and %(LServer)H" " and seq=%(Seq)d")), ok. read_all_messages(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s from spool where " "username=%(LUser)s and %(LServer)H order by seq")) of {selected, Rs} -> lists:flatmap( fun({XML}) -> case xml_to_offline_msg(XML) of {ok, Msg} -> [Msg]; _ -> [] end end, Rs); _ -> [] end. remove_all_messages(LUser, LServer) -> remove_user(LUser, LServer), {atomic, ok}. count_messages(LUser, LServer) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(count(*))d from spool " "where username=%(LUser)s and %(LServer)H")) of {selected, [{Res}]} -> {cache, Res}; {selected, []} -> {cache, 0}; _ -> {nocache, 0} end. export(_Server) -> [{offline_msg, fun(Host, #offline_msg{us = {LUser, LServer}}) when LServer == Host -> [?SQL("delete from spool where username=%(LUser)s" " and %(LServer)H;")]; (_Host, _R) -> [] end}, {offline_msg, fun(Host, #offline_msg{us = {LUser, LServer}, timestamp = TimeStamp, from = From, to = To, packet = El}) when LServer == Host -> try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of Packet -> Packet1 = xmpp:set_from_to(Packet, From, To), Packet2 = misc:add_delay_info( Packet1, jid:make(LServer), TimeStamp, <<"Offline Storage">>), XML = fxml:element_to_binary(xmpp:encode(Packet2)), [?SQL_INSERT( "spool", ["username=%(LUser)s", "server_host=%(LServer)s", "xml=%(XML)s"])] catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode packet ~p of user ~ts@~ts: ~ts", [El, LUser, LServer, xmpp:format_error(Why)]), [] end; (_Host, _R) -> [] end}]. import(_) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== xml_to_offline_msg(XML) -> case fxml_stream:parse_element(XML) of #xmlel{} = El -> el_to_offline_msg(El); Err -> ?ERROR_MSG("Got ~p when parsing XML packet ~ts", [Err, XML]), Err end. el_to_offline_msg(El) -> To_s = fxml:get_tag_attr_s(<<"to">>, El), From_s = fxml:get_tag_attr_s(<<"from">>, El), try To = jid:decode(To_s), From = jid:decode(From_s), {ok, #offline_msg{us = {To#jid.luser, To#jid.lserver}, from = From, to = To, packet = El}} catch _:{bad_jid, To_s} -> ?ERROR_MSG("Failed to get 'to' JID from offline XML ~p", [El]), {error, bad_jid_to}; _:{bad_jid, From_s} -> ?ERROR_MSG("Failed to get 'from' JID from offline XML ~p", [El]), {error, bad_jid_from} end. get_and_del_spool_msg_t(LServer, LUser) -> F = fun () -> Result = ejabberd_sql:sql_query_t( ?SQL("select @(username)s, @(xml)s from spool where " "username=%(LUser)s and %(LServer)H order by seq;")), DResult = ejabberd_sql:sql_query_t( ?SQL("delete from spool where" " username=%(LUser)s and %(LServer)H;")), case {Result, DResult} of {{selected, Rs}, {updated, DC}} when length(Rs) /= DC -> ejabberd_sql:restart(concurent_insert); _ -> Result end end, ejabberd_sql:sql_transaction(LServer, F). ejabberd-21.12/src/mod_legacy_auth.erl0000644000232200023220000001500114154362354020246 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_legacy_auth). -behaviour(gen_mod). -protocol({xep, 78, '2.5'}). %% gen_mod API -export([start/2, stop/1, reload/3, depends/2, mod_options/1, mod_doc/0]). %% hooks -export([c2s_unauthenticated_packet/2, c2s_stream_features/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -type c2s_state() :: ejabberd_c2s:state(). %%%=================================================================== %%% API %%%=================================================================== start(Host, _Opts) -> ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), ejabberd_hooks:add(c2s_pre_auth_features, Host, ?MODULE, c2s_stream_features, 50). stop(Host) -> ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, c2s_unauthenticated_packet, 50), ejabberd_hooks:delete(c2s_pre_auth_features, Host, ?MODULE, c2s_stream_features, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. mod_options(_) -> []. mod_doc() -> #{desc => [?T("The module implements " "https://xmpp.org/extensions/xep-0078.html" "[XEP-0078: Non-SASL Authentication]."), "", ?T("NOTE: This type of authentication was obsoleted in " "2008 and you unlikely need this module unless " "you have something like outdated Jabber bots.")]}. -spec c2s_unauthenticated_packet(c2s_state(), iq()) -> c2s_state() | {stop, c2s_state()}. c2s_unauthenticated_packet(State, #iq{type = T, sub_els = [_]} = IQ) when T == get; T == set -> try xmpp:try_subtag(IQ, #legacy_auth{}) of #legacy_auth{} = Auth -> {stop, authenticate(State, xmpp:set_els(IQ, [Auth]))}; false -> State catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Lang = maps:get(lang, State), Err = xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)), {stop, ejabberd_c2s:send(State, Err)} end; c2s_unauthenticated_packet(State, _) -> State. -spec c2s_stream_features([xmpp_element()], binary()) -> [xmpp_element()]. c2s_stream_features(Acc, LServer) -> case gen_mod:is_loaded(LServer, ?MODULE) of true -> [#legacy_auth_feature{}|Acc]; false -> Acc end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec authenticate(c2s_state(), iq()) -> c2s_state(). authenticate(#{server := Server} = State, #iq{type = get, sub_els = [#legacy_auth{}]} = IQ) -> LServer = jid:nameprep(Server), Auth = #legacy_auth{username = <<>>, password = <<>>, resource = <<>>}, Res = case ejabberd_auth:plain_password_required(LServer) of false -> xmpp:make_iq_result(IQ, Auth#legacy_auth{digest = <<>>}); true -> xmpp:make_iq_result(IQ, Auth) end, ejabberd_c2s:send(State, Res); authenticate(State, #iq{type = set, lang = Lang, sub_els = [#legacy_auth{username = U, resource = R}]} = IQ) when U == undefined; R == undefined; U == <<"">>; R == <<"">> -> Txt = ?T("Both the username and the resource are required"), Err = xmpp:make_error(IQ, xmpp:err_not_acceptable(Txt, Lang)), ejabberd_c2s:send(State, Err); authenticate(#{stream_id := StreamID, server := Server, access := Access, ip := IP} = State, #iq{type = set, lang = Lang, sub_els = [#legacy_auth{username = U, password = P0, digest = D0, resource = R}]} = IQ) -> P = if is_binary(P0) -> P0; true -> <<>> end, D = if is_binary(D0) -> D0; true -> <<>> end, DGen = fun (PW) -> str:sha(<>) end, JID = jid:make(U, Server, R), case JID /= error andalso acl:match_rule(JID#jid.lserver, Access, #{usr => jid:split(JID), ip => IP}) == allow of true -> case ejabberd_auth:check_password_with_authmodule( U, U, JID#jid.lserver, P, D, DGen) of {true, AuthModule} -> State1 = State#{sasl_mech => <<"legacy">>}, State2 = ejabberd_c2s:handle_auth_success( U, <<"legacy">>, AuthModule, State1), State3 = State2#{user := U}, open_session(State3, IQ, R); _ -> Err = xmpp:make_error(IQ, xmpp:err_not_authorized()), process_auth_failure(State, U, Err, 'not-authorized') end; false when JID == error -> Err = xmpp:make_error(IQ, xmpp:err_jid_malformed()), process_auth_failure(State, U, Err, 'jid-malformed'); false -> Txt = ?T("Access denied by service policy"), Err = xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)), process_auth_failure(State, U, Err, 'forbidden') end. -spec open_session(c2s_state(), iq(), binary()) -> c2s_state(). open_session(State, IQ, R) -> case ejabberd_c2s:bind(R, State) of {ok, State1} -> Res = xmpp:make_iq_result(IQ), ejabberd_c2s:send(State1, Res); {error, Err, State1} -> Res = xmpp:make_error(IQ, Err), ejabberd_c2s:send(State1, Res) end. -spec process_auth_failure(c2s_state(), binary(), iq(), atom()) -> c2s_state(). process_auth_failure(State, User, StanzaErr, Reason) -> State1 = ejabberd_c2s:send(State, StanzaErr), State2 = State1#{sasl_mech => <<"legacy">>}, Text = format_reason(Reason), ejabberd_c2s:handle_auth_failure(User, <<"legacy">>, Text, State2). -spec format_reason(atom()) -> binary(). format_reason('not-authorized') -> <<"Invalid username or password">>; format_reason('forbidden') -> <<"Access denied by service policy">>; format_reason('jid-malformed') -> <<"Malformed XMPP address">>. ejabberd-21.12/src/mod_client_state_opt.erl0000644000232200023220000000163614154362354021332 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_client_state_opt). -export([queue_chat_states/1]). -export([queue_pep/1]). -export([queue_presence/1]). -spec queue_chat_states(gen_mod:opts() | global | binary()) -> boolean(). queue_chat_states(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_chat_states, Opts); queue_chat_states(Host) -> gen_mod:get_module_opt(Host, mod_client_state, queue_chat_states). -spec queue_pep(gen_mod:opts() | global | binary()) -> boolean(). queue_pep(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_pep, Opts); queue_pep(Host) -> gen_mod:get_module_opt(Host, mod_client_state, queue_pep). -spec queue_presence(gen_mod:opts() | global | binary()) -> boolean(). queue_presence(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_presence, Opts); queue_presence(Host) -> gen_mod:get_module_opt(Host, mod_client_state, queue_presence). ejabberd-21.12/src/ejabberd_router_mnesia.erl0000644000232200023220000001477314154362354021633 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 11 Jan 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_router_mnesia). -behaviour(ejabberd_router). -behaviour(gen_server). %% API -export([init/0, register_route/5, unregister_route/3, find_routes/1, get_all_routes/0, use_cache/0]). %% gen_server callbacks -export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("ejabberd_router.hrl"). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). use_cache() -> false. register_route(Domain, ServerHost, LocalHint, undefined, Pid) -> F = fun () -> mnesia:write(#route{domain = Domain, pid = Pid, server_host = ServerHost, local_hint = LocalHint}) end, transaction(F); register_route(Domain, ServerHost, _LocalHint, N, Pid) -> F = fun () -> case mnesia:wread({route, Domain}) of [] -> mnesia:write(#route{domain = Domain, server_host = ServerHost, pid = Pid, local_hint = 1}), lists:foreach( fun (I) -> mnesia:write( #route{domain = Domain, pid = undefined, server_host = ServerHost, local_hint = I}) end, lists:seq(2, N)); Rs -> lists:any( fun (#route{pid = undefined, local_hint = I} = R) -> mnesia:write( #route{domain = Domain, pid = Pid, server_host = ServerHost, local_hint = I}), mnesia:delete_object(R), true; (_) -> false end, Rs) end end, transaction(F). unregister_route(Domain, undefined, Pid) -> F = fun () -> case mnesia:select( route, ets:fun2ms( fun(#route{domain = D, pid = P} = R) when D == Domain, P == Pid -> R end)) of [R] -> mnesia:delete_object(R); _ -> ok end end, transaction(F); unregister_route(Domain, _, Pid) -> F = fun () -> case mnesia:select( route, ets:fun2ms( fun(#route{domain = D, pid = P} = R) when D == Domain, P == Pid -> R end)) of [R] -> I = R#route.local_hint, ServerHost = R#route.server_host, mnesia:write(#route{domain = Domain, server_host = ServerHost, pid = undefined, local_hint = I}), mnesia:delete_object(R); _ -> ok end end, transaction(F). find_routes(Domain) -> {ok, mnesia:dirty_read(route, Domain)}. get_all_routes() -> {ok, mnesia:dirty_select( route, ets:fun2ms( fun(#route{domain = Domain, server_host = ServerHost}) when Domain /= ServerHost -> Domain end))}. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> update_tables(), ejabberd_mnesia:create(?MODULE, route, [{ram_copies, [node()]}, {type, bag}, {attributes, record_info(fields, route)}]), mnesia:subscribe({table, route, simple}), lists:foreach( fun (Pid) -> erlang:monitor(process, Pid) end, mnesia:dirty_select( route, ets:fun2ms( fun(#route{pid = Pid}) -> Pid end))), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({mnesia_table_event, {write, #route{pid = Pid}, _ActivityId}}, State) -> erlang:monitor(process, Pid), {noreply, State}; handle_info({mnesia_table_event, _}, State) -> {noreply, State}; handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> F = fun () -> Es = mnesia:select( route, ets:fun2ms( fun(#route{pid = P} = E) when P == Pid -> E end)), lists:foreach( fun(E) -> if is_integer(E#route.local_hint) -> LDomain = E#route.domain, I = E#route.local_hint, ServerHost = E#route.server_host, mnesia:write(#route{domain = LDomain, server_host = ServerHost, pid = undefined, local_hint = I}), mnesia:delete_object(E); true -> mnesia:delete_object(E) end end, Es) end, transaction(F), {noreply, State}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. -spec update_tables() -> ok. update_tables() -> try mnesia:transform_table(route, ignore, record_info(fields, route)) catch exit:{aborted, {no_exists, _}} -> ok end, case lists:member(local_route, mnesia:system_info(tables)) of true -> mnesia:delete_table(local_route); false -> ok end. ejabberd-21.12/src/mod_shared_roster_ldap_opt.erl0000644000232200023220000002077414154362354022524 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_shared_roster_ldap_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([ldap_auth_check/1]). -export([ldap_backups/1]). -export([ldap_base/1]). -export([ldap_deref_aliases/1]). -export([ldap_encrypt/1]). -export([ldap_filter/1]). -export([ldap_gfilter/1]). -export([ldap_groupattr/1]). -export([ldap_groupdesc/1]). -export([ldap_memberattr/1]). -export([ldap_memberattr_format/1]). -export([ldap_memberattr_format_re/1]). -export([ldap_password/1]). -export([ldap_port/1]). -export([ldap_rfilter/1]). -export([ldap_rootdn/1]). -export([ldap_servers/1]). -export([ldap_tls_cacertfile/1]). -export([ldap_tls_certfile/1]). -export([ldap_tls_depth/1]). -export([ldap_tls_verify/1]). -export([ldap_ufilter/1]). -export([ldap_uids/1]). -export([ldap_userdesc/1]). -export([ldap_userjidattr/1]). -export([ldap_useruid/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, cache_size). -spec ldap_auth_check(gen_mod:opts() | global | binary()) -> boolean(). ldap_auth_check(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_auth_check, Opts); ldap_auth_check(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_auth_check). -spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. ldap_backups(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_backups, Opts); ldap_backups(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_backups). -spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). ldap_base(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_base, Opts); ldap_base(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_base). -spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_deref_aliases, Opts); ldap_deref_aliases(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_deref_aliases). -spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. ldap_encrypt(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_encrypt, Opts); ldap_encrypt(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_encrypt). -spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). ldap_filter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_filter, Opts); ldap_filter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_filter). -spec ldap_gfilter(gen_mod:opts() | global | binary()) -> binary(). ldap_gfilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_gfilter, Opts); ldap_gfilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_gfilter). -spec ldap_groupattr(gen_mod:opts() | global | binary()) -> binary(). ldap_groupattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_groupattr, Opts); ldap_groupattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupattr). -spec ldap_groupdesc(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). ldap_groupdesc(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_groupdesc, Opts); ldap_groupdesc(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_groupdesc). -spec ldap_memberattr(gen_mod:opts() | global | binary()) -> binary(). ldap_memberattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr, Opts); ldap_memberattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr). -spec ldap_memberattr_format(gen_mod:opts() | global | binary()) -> binary(). ldap_memberattr_format(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr_format, Opts); ldap_memberattr_format(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format). -spec ldap_memberattr_format_re(gen_mod:opts() | global | binary()) -> 'undefined' | re:mp(). ldap_memberattr_format_re(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_memberattr_format_re, Opts); ldap_memberattr_format_re(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_memberattr_format_re). -spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). ldap_password(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_password, Opts); ldap_password(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_password). -spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. ldap_port(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_port, Opts); ldap_port(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_port). -spec ldap_rfilter(gen_mod:opts() | global | binary()) -> binary(). ldap_rfilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rfilter, Opts); ldap_rfilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rfilter). -spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). ldap_rootdn(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rootdn, Opts); ldap_rootdn(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_rootdn). -spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. ldap_servers(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_servers, Opts); ldap_servers(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_servers). -spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_cacertfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_cacertfile, Opts); ldap_tls_cacertfile(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_cacertfile). -spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_certfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_certfile, Opts); ldap_tls_certfile(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_certfile). -spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). ldap_tls_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_depth, Opts); ldap_tls_depth(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_depth). -spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. ldap_tls_verify(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_verify, Opts); ldap_tls_verify(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_tls_verify). -spec ldap_ufilter(gen_mod:opts() | global | binary()) -> binary(). ldap_ufilter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_ufilter, Opts); ldap_ufilter(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_ufilter). -spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. ldap_uids(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_uids, Opts); ldap_uids(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_uids). -spec ldap_userdesc(gen_mod:opts() | global | binary()) -> binary(). ldap_userdesc(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_userdesc, Opts); ldap_userdesc(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userdesc). -spec ldap_userjidattr(gen_mod:opts() | global | binary()) -> binary(). ldap_userjidattr(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_userjidattr, Opts); ldap_userjidattr(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_userjidattr). -spec ldap_useruid(gen_mod:opts() | global | binary()) -> binary(). ldap_useruid(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_useruid, Opts); ldap_useruid(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, ldap_useruid). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_shared_roster_ldap, use_cache). ejabberd-21.12/src/mod_mqtt_opt.erl0000644000232200023220000000746614154362354017650 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_mqtt_opt). -export([access_publish/1]). -export([access_subscribe/1]). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([match_retained_limit/1]). -export([max_queue/1]). -export([max_topic_aliases/1]). -export([max_topic_depth/1]). -export([queue_type/1]). -export([ram_db_type/1]). -export([session_expiry/1]). -export([use_cache/1]). -spec access_publish(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. access_publish(Opts) when is_map(Opts) -> gen_mod:get_opt(access_publish, Opts); access_publish(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, access_publish). -spec access_subscribe(gen_mod:opts() | global | binary()) -> [{[binary()],acl:acl()}]. access_subscribe(Opts) when is_map(Opts) -> gen_mod:get_opt(access_subscribe, Opts); access_subscribe(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, access_subscribe). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, db_type). -spec match_retained_limit(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). match_retained_limit(Opts) when is_map(Opts) -> gen_mod:get_opt(match_retained_limit, Opts); match_retained_limit(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, match_retained_limit). -spec max_queue(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). max_queue(Opts) when is_map(Opts) -> gen_mod:get_opt(max_queue, Opts); max_queue(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_queue). -spec max_topic_aliases(gen_mod:opts() | global | binary()) -> char(). max_topic_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(max_topic_aliases, Opts); max_topic_aliases(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_topic_aliases). -spec max_topic_depth(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_topic_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(max_topic_depth, Opts); max_topic_depth(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, max_topic_depth). -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, queue_type). -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, ram_db_type). -spec session_expiry(gen_mod:opts() | global | binary()) -> non_neg_integer(). session_expiry(Opts) when is_map(Opts) -> gen_mod:get_opt(session_expiry, Opts); session_expiry(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, session_expiry). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mqtt, use_cache). ejabberd-21.12/src/mod_last_opt.erl0000644000232200023220000000253714154362354017620 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_last_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_last, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_last, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_last, use_cache). ejabberd-21.12/src/ejabberd_auth_pam.erl0000644000232200023220000000477214154362354020553 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_auth_pam.erl %%% Author : Evgeniy Khramtsov %%% Purpose : PAM authentication %%% Created : 5 Jul 2007 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_auth_pam). -author('xram@jabber.ru'). -behaviour(ejabberd_auth). -export([start/1, stop/1, check_password/4, user_exists/2, store_type/1, plain_password_required/1]). start(_Host) -> ejabberd:start_app(epam). stop(_Host) -> ok. check_password(User, AuthzId, Host, Password) -> if AuthzId /= <<>> andalso AuthzId /= User -> false; true -> Service = get_pam_service(Host), UserInfo = case get_pam_userinfotype(Host) of username -> User; jid -> <> end, case catch epam:authenticate(Service, UserInfo, Password) of true -> {cache, true}; false -> {cache, false}; _ -> {nocache, false} end end. user_exists(User, Host) -> Service = get_pam_service(Host), UserInfo = case get_pam_userinfotype(Host) of username -> User; jid -> <> end, case catch epam:acct_mgmt(Service, UserInfo) of true -> {cache, true}; false -> {cache, false}; _Err -> {nocache, {error, db_failure}} end. plain_password_required(_) -> true. store_type(_) -> external. %%==================================================================== %% Internal functions %%==================================================================== get_pam_service(Host) -> ejabberd_option:pam_service(Host). get_pam_userinfotype(Host) -> ejabberd_option:pam_userinfotype(Host). ejabberd-21.12/src/ejabberd_options.erl0000644000232200023220000006012314154362354020440 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_options). -behaviour(ejabberd_config). -export([opt_type/1, options/0, globals/0, doc/0]). -ifdef(NEW_SQL_SCHEMA). -define(USE_NEW_SQL_SCHEMA_DEFAULT, true). -else. -define(USE_NEW_SQL_SCHEMA_DEFAULT, false). -endif. -include_lib("kernel/include/inet.hrl"). %%%=================================================================== %%% API %%%=================================================================== -spec opt_type(atom()) -> econf:validator(). opt_type(access_rules) -> acl:validator(access_rules); opt_type(acl) -> acl:validator(acl); opt_type(acme) -> econf:options( #{ca_url => econf:url(), contact => econf:list_or_single(econf:binary("^[a-zA-Z]+:[^:]+$")), auto => econf:bool(), cert_type => econf:enum([ec, rsa])}, [unique, {return, map}]); opt_type(allow_contrib_modules) -> econf:bool(); opt_type(allow_multiple_connections) -> econf:bool(); opt_type(anonymous_protocol) -> econf:enum([sasl_anon, login_anon, both]); opt_type(api_permissions) -> ejabberd_access_permissions:validator(); opt_type(append_host_config) -> opt_type(host_config); opt_type(auth_cache_life_time) -> econf:timeout(second, infinity); opt_type(auth_cache_missed) -> econf:bool(); opt_type(auth_cache_size) -> econf:pos_int(infinity); opt_type(auth_method) -> econf:list_or_single(econf:db_type(ejabberd_auth)); opt_type(auth_opts) -> fun(L) when is_list(L) -> lists:map( fun({host, V}) when is_binary(V) -> {host, V}; ({connection_pool_size, V}) when is_integer(V) -> {connection_pool_size, V}; ({connection_opts, V}) when is_list(V) -> {connection_opts, V}; ({basic_auth, V}) when is_binary(V) -> {basic_auth, V}; ({path_prefix, V}) when is_binary(V) -> {path_prefix, V} end, L) end; opt_type(auth_password_format) -> econf:enum([plain, scram]); opt_type(auth_scram_hash) -> econf:enum([sha, sha256, sha512]); opt_type(auth_use_cache) -> econf:bool(); opt_type(c2s_cafile) -> econf:file(); opt_type(c2s_ciphers) -> fun(L) when is_list(L) -> (econf:and_then( econf:list(econf:binary(), [unique]), concat_binary($:)))(L); (B) -> (econf:binary())(B) end; opt_type(c2s_dhfile) -> econf:file(); opt_type(c2s_protocol_options) -> econf:and_then( econf:list(econf:binary(), [unique]), concat_binary($|)); opt_type(c2s_tls_compression) -> econf:bool(); opt_type(ca_file) -> econf:pem(); opt_type(cache_life_time) -> econf:timeout(second, infinity); opt_type(cache_missed) -> econf:bool(); opt_type(cache_size) -> econf:pos_int(infinity); opt_type(captcha_cmd) -> econf:file(); opt_type(captcha_host) -> econf:binary(); opt_type(captcha_limit) -> econf:pos_int(infinity); opt_type(captcha_url) -> econf:url(); opt_type(certfiles) -> econf:list(econf:binary()); opt_type(cluster_backend) -> econf:db_type(ejabberd_cluster); opt_type(cluster_nodes) -> econf:list(econf:atom(), [unique]); opt_type(default_db) -> econf:enum([mnesia, sql]); opt_type(default_ram_db) -> econf:enum([mnesia, sql, redis]); opt_type(define_macro) -> econf:any(); opt_type(disable_sasl_mechanisms) -> econf:list_or_single( econf:and_then( econf:binary(), fun str:to_upper/1)); opt_type(domain_balancing) -> econf:map( econf:domain(), econf:options( #{component_number => econf:int(2, 1000), type => econf:enum([random, source, destination, bare_source, bare_destination])}, [{required, [component_number]}, {return, map}, unique]), [{return, map}]); opt_type(ext_api_path_oauth) -> econf:binary(); opt_type(ext_api_http_pool_size) -> econf:pos_int(); opt_type(ext_api_url) -> econf:url(); opt_type(ext_api_headers) -> econf:binary(); opt_type(extauth_pool_name) -> econf:binary(); opt_type(extauth_pool_size) -> econf:pos_int(); opt_type(extauth_program) -> econf:string(); opt_type(fqdn) -> econf:list_or_single(econf:domain()); opt_type(hide_sensitive_log_data) -> econf:bool(); opt_type(host_config) -> econf:and_then( econf:and_then( econf:map(econf:domain(), econf:list(econf:any())), fun econf:group_dups/1), econf:map( econf:enum(ejabberd_config:get_option(hosts)), validator(), [unique])); opt_type(hosts) -> econf:non_empty(econf:list(econf:domain(), [unique])); opt_type(include_config_file) -> econf:any(); opt_type(language) -> econf:lang(); opt_type(ldap_backups) -> econf:list(econf:domain(), [unique]); opt_type(ldap_base) -> econf:binary(); opt_type(ldap_deref_aliases) -> econf:enum([never, searching, finding, always]); opt_type(ldap_dn_filter) -> econf:and_then( econf:non_empty( econf:map( econf:ldap_filter(), econf:list(econf:binary()))), fun hd/1); opt_type(ldap_encrypt) -> econf:enum([tls, starttls, none]); opt_type(ldap_filter) -> econf:ldap_filter(); opt_type(ldap_password) -> econf:binary(); opt_type(ldap_port) -> econf:port(); opt_type(ldap_rootdn) -> econf:binary(); opt_type(ldap_servers) -> econf:list(econf:domain(), [unique]); opt_type(ldap_tls_cacertfile) -> econf:pem(); opt_type(ldap_tls_certfile) -> econf:pem(); opt_type(ldap_tls_depth) -> econf:non_neg_int(); opt_type(ldap_tls_verify) -> econf:enum([hard, soft, false]); opt_type(ldap_uids) -> econf:either( econf:list( econf:and_then( econf:binary(), fun(U) -> {U, <<"%u">>} end)), econf:map(econf:binary(), econf:binary(), [unique])); opt_type(listen) -> ejabberd_listener:validator(); opt_type(log_rotate_count) -> econf:non_neg_int(); opt_type(log_rotate_size) -> econf:pos_int(infinity); opt_type(loglevel) -> fun(N) when is_integer(N) -> (econf:and_then( econf:int(0, 5), fun ejabberd_logger:convert_loglevel/1))(N); (Level) -> (econf:enum([none, emergency, alert, critical, error, warning, notice, info, debug]))(Level) end; opt_type(max_fsm_queue) -> econf:pos_int(); opt_type(modules) -> econf:map(econf:atom(), econf:any()); opt_type(negotiation_timeout) -> econf:timeout(second); opt_type(net_ticktime) -> econf:timeout(second); opt_type(new_sql_schema) -> econf:bool(); opt_type(oauth_access) -> econf:acl(); opt_type(oauth_cache_life_time) -> econf:timeout(second, infinity); opt_type(oauth_cache_missed) -> econf:bool(); opt_type(oauth_cache_rest_failure_life_time) -> econf:timeout(second, infinity); opt_type(oauth_cache_size) -> econf:pos_int(infinity); opt_type(oauth_db_type) -> econf:db_type(ejabberd_oauth); opt_type(oauth_expire) -> econf:timeout(second); opt_type(oauth_use_cache) -> econf:bool(); opt_type(oauth_client_id_check) -> econf:enum([allow, deny, db]); opt_type(oom_killer) -> econf:bool(); opt_type(oom_queue) -> econf:pos_int(); opt_type(oom_watermark) -> econf:int(1, 99); opt_type(outgoing_s2s_families) -> econf:and_then( econf:non_empty( econf:list(econf:enum([ipv4, ipv6]), [unique])), fun(L) -> lists:map( fun(ipv4) -> inet; (ipv6) -> inet6 end, L) end); opt_type(outgoing_s2s_ipv4_address) -> econf:ipv4(); opt_type(outgoing_s2s_ipv6_address) -> econf:ipv6(); opt_type(outgoing_s2s_port) -> econf:port(); opt_type(outgoing_s2s_timeout) -> econf:timeout(second, infinity); opt_type(pam_service) -> econf:binary(); opt_type(pam_userinfotype) -> econf:enum([username, jid]); opt_type(pgsql_users_number_estimate) -> econf:bool(); opt_type(queue_dir) -> econf:directory(write); opt_type(queue_type) -> econf:enum([ram, file]); opt_type(redis_connect_timeout) -> econf:timeout(second); opt_type(redis_db) -> econf:non_neg_int(); opt_type(redis_password) -> econf:string(); opt_type(redis_pool_size) -> econf:pos_int(); opt_type(redis_port) -> econf:port(); opt_type(redis_queue_type) -> econf:enum([ram, file]); opt_type(redis_server) -> econf:string(); opt_type(registration_timeout) -> econf:timeout(second, infinity); opt_type(resource_conflict) -> econf:enum([setresource, closeold, closenew, acceptnew]); opt_type(router_cache_life_time) -> econf:timeout(second, infinity); opt_type(router_cache_missed) -> econf:bool(); opt_type(router_cache_size) -> econf:pos_int(infinity); opt_type(router_db_type) -> econf:db_type(ejabberd_router); opt_type(router_use_cache) -> econf:bool(); opt_type(rpc_timeout) -> econf:timeout(second); opt_type(s2s_access) -> econf:acl(); opt_type(s2s_cafile) -> econf:pem(); opt_type(s2s_ciphers) -> opt_type(c2s_ciphers); opt_type(s2s_dhfile) -> econf:file(); opt_type(s2s_dns_retries) -> econf:non_neg_int(); opt_type(s2s_dns_timeout) -> econf:timeout(second, infinity); opt_type(s2s_max_retry_delay) -> econf:timeout(second); opt_type(s2s_protocol_options) -> opt_type(c2s_protocol_options); opt_type(s2s_queue_type) -> econf:enum([ram, file]); opt_type(s2s_timeout) -> econf:timeout(second, infinity); opt_type(s2s_tls_compression) -> econf:bool(); opt_type(s2s_use_starttls) -> econf:either( econf:bool(), econf:enum([optional, required])); opt_type(s2s_zlib) -> econf:and_then( econf:bool(), fun(false) -> false; (true) -> ejabberd:start_app(ezlib), true end); opt_type(shaper) -> ejabberd_shaper:validator(shaper); opt_type(shaper_rules) -> ejabberd_shaper:validator(shaper_rules); opt_type(sm_cache_life_time) -> econf:timeout(second, infinity); opt_type(sm_cache_missed) -> econf:bool(); opt_type(sm_cache_size) -> econf:pos_int(infinity); opt_type(sm_db_type) -> econf:db_type(ejabberd_sm); opt_type(sm_use_cache) -> econf:bool(); opt_type(sql_connect_timeout) -> econf:timeout(second); opt_type(sql_database) -> econf:binary(); opt_type(sql_keepalive_interval) -> econf:timeout(second); opt_type(sql_password) -> econf:binary(); opt_type(sql_odbc_driver) -> econf:binary(); opt_type(sql_pool_size) -> econf:pos_int(); opt_type(sql_port) -> econf:port(); opt_type(sql_query_timeout) -> econf:timeout(second); opt_type(sql_queue_type) -> econf:enum([ram, file]); opt_type(sql_server) -> econf:binary(); opt_type(sql_ssl) -> econf:bool(); opt_type(sql_ssl_cafile) -> econf:pem(); opt_type(sql_ssl_certfile) -> econf:pem(); opt_type(sql_ssl_verify) -> econf:bool(); opt_type(sql_start_interval) -> econf:timeout(second); opt_type(sql_type) -> econf:enum([mysql, pgsql, sqlite, mssql, odbc]); opt_type(sql_username) -> econf:binary(); opt_type(sql_prepared_statements) -> econf:bool(); opt_type(trusted_proxies) -> econf:either(all, econf:list(econf:ip_mask())); opt_type(use_cache) -> econf:bool(); opt_type(validate_stream) -> econf:bool(); opt_type(version) -> econf:binary(); opt_type(websocket_origin) -> econf:list( econf:and_then( econf:and_then( econf:binary_sep("\\s+"), econf:list(econf:url(), [unique])), fun(L) -> str:join(L, <<" ">>) end), [unique]); opt_type(websocket_ping_interval) -> econf:timeout(second); opt_type(websocket_timeout) -> econf:timeout(second); opt_type(jwt_key) -> econf:and_then( econf:path(), fun(Path) -> case file:read_file(Path) of {ok, Data} -> try jose_jwk:from_binary(Data) of {error, _} -> econf:fail({bad_jwt_key, Path}); JWK -> case jose_jwk:to_map(JWK) of {_, #{<<"keys">> := [Key]}} -> jose_jwk:from_map(Key); {_, #{<<"keys">> := [_|_]}} -> econf:fail({bad_jwt_key_set, Path}); {_, #{<<"keys">> := _}} -> econf:fail({bad_jwt_key, Path}); _ -> JWK end catch _:_ -> econf:fail({bad_jwt_key, Path}) end; {error, Reason} -> econf:fail({read_file, Reason, Path}) end end); opt_type(jwt_jid_field) -> econf:binary(); opt_type(jwt_auth_only_rule) -> econf:atom(). %% We only define the types of options that cannot be derived %% automatically by tools/opt_type.sh script -spec options() -> [{s2s_protocol_options, undefined | binary()} | {c2s_protocol_options, undefined | binary()} | {s2s_ciphers, undefined | binary()} | {c2s_ciphers, undefined | binary()} | {websocket_origin, [binary()]} | {disable_sasl_mechanisms, [binary()]} | {s2s_zlib, boolean()} | {loglevel, ejabberd_logger:loglevel()} | {auth_opts, [{any(), any()}]} | {listen, [ejabberd_listener:listener()]} | {modules, [{module(), gen_mod:opts(), integer()}]} | {ldap_uids, [{binary(), binary()}]} | {ldap_dn_filter, {binary(), [binary()]}} | {outgoing_s2s_families, [inet | inet6, ...]} | {acl, [{atom(), [acl:acl_rule()]}]} | {access_rules, [{atom(), acl:access()}]} | {shaper, #{atom() => ejabberd_shaper:shaper_rate()}} | {shaper_rules, [{atom(), [ejabberd_shaper:shaper_rule()]}]} | {api_permissions, [ejabberd_access_permissions:permission()]} | {jwt_key, jose_jwk:key() | undefined} | {append_host_config, [{binary(), any()}]} | {host_config, [{binary(), any()}]} | {define_macro, any()} | {include_config_file, any()} | {atom(), any()}]. options() -> [%% Top-priority options hosts, {loglevel, info}, {cache_life_time, timer:seconds(3600)}, {cache_missed, true}, {cache_size, 1000}, {use_cache, true}, {default_db, mnesia}, {default_ram_db, mnesia}, {queue_type, ram}, {version, ejabberd_config:version()}, %% Other options {acl, []}, {access_rules, []}, {acme, #{}}, {allow_contrib_modules, true}, {allow_multiple_connections, false}, {anonymous_protocol, sasl_anon}, {api_permissions, [{<<"admin access">>, {[], [{acl, admin}, {oauth, {[<<"ejabberd:admin">>], [{acl, admin}]}}], {all, [start, stop]}}}]}, {append_host_config, []}, {auth_cache_life_time, fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, {auth_cache_missed, fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, {auth_cache_size, fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, {auth_method, fun(Host) -> [ejabberd_config:default_db(Host, ejabberd_auth)] end}, {auth_opts, []}, {auth_password_format, plain}, {auth_scram_hash, sha}, {auth_use_cache, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, {c2s_cafile, undefined}, {c2s_ciphers, undefined}, {c2s_dhfile, undefined}, {c2s_protocol_options, undefined}, {c2s_tls_compression, undefined}, {ca_file, iolist_to_binary(pkix:get_cafile())}, {captcha_cmd, undefined}, {captcha_host, <<"">>}, {captcha_limit, infinity}, {captcha_url, undefined}, {certfiles, undefined}, {cluster_backend, mnesia}, {cluster_nodes, []}, {define_macro, []}, {disable_sasl_mechanisms, []}, {domain_balancing, #{}}, {ext_api_headers, <<>>}, {ext_api_http_pool_size, 100}, {ext_api_path_oauth, <<"/oauth">>}, {ext_api_url, <<"http://localhost/api">>}, {extauth_pool_name, undefined}, {extauth_pool_size, undefined}, {extauth_program, undefined}, {fqdn, fun fqdn/1}, {hide_sensitive_log_data, false}, {host_config, []}, {include_config_file, []}, {language, <<"en">>}, {ldap_backups, []}, {ldap_base, <<"">>}, {ldap_deref_aliases, never}, {ldap_dn_filter, {undefined, []}}, {ldap_encrypt, none}, {ldap_filter, <<"">>}, {ldap_password, <<"">>}, {ldap_port, fun(Host) -> case ejabberd_config:get_option({ldap_encrypt, Host}) of tls -> 636; _ -> 389 end end}, {ldap_rootdn, <<"">>}, {ldap_servers, [<<"localhost">>]}, {ldap_tls_cacertfile, undefined}, {ldap_tls_certfile, undefined}, {ldap_tls_depth, undefined}, {ldap_tls_verify, false}, {ldap_uids, [{<<"uid">>, <<"%u">>}]}, {listen, []}, {log_rotate_count, 1}, {log_rotate_size, 10*1024*1024}, {max_fsm_queue, undefined}, {modules, []}, {negotiation_timeout, timer:seconds(30)}, {net_ticktime, timer:seconds(60)}, {new_sql_schema, ?USE_NEW_SQL_SCHEMA_DEFAULT}, {oauth_access, none}, {oauth_cache_life_time, fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, {oauth_cache_missed, fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, {oauth_cache_size, fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, {oauth_cache_rest_failure_life_time, infinity}, {oauth_db_type, fun(Host) -> ejabberd_config:default_db(Host, ejabberd_oauth) end}, {oauth_expire, 4294967}, {oauth_use_cache, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, {oauth_client_id_check, allow}, {oom_killer, true}, {oom_queue, 10000}, {oom_watermark, 80}, {outgoing_s2s_families, [inet, inet6]}, {outgoing_s2s_ipv4_address, undefined}, {outgoing_s2s_ipv6_address, undefined}, {outgoing_s2s_port, 5269}, {outgoing_s2s_timeout, timer:seconds(10)}, {pam_service, <<"ejabberd">>}, {pam_userinfotype, username}, {pgsql_users_number_estimate, false}, {queue_dir, undefined}, {redis_connect_timeout, timer:seconds(1)}, {redis_db, 0}, {redis_password, ""}, {redis_pool_size, 10}, {redis_port, 6379}, {redis_queue_type, fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, {redis_server, "localhost"}, {registration_timeout, timer:seconds(600)}, {resource_conflict, acceptnew}, {router_cache_life_time, fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, {router_cache_missed, fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, {router_cache_size, fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, {router_db_type, fun(Host) -> ejabberd_config:default_ram_db(Host, ejabberd_router) end}, {router_use_cache, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, {rpc_timeout, timer:seconds(5)}, {s2s_access, all}, {s2s_cafile, undefined}, {s2s_ciphers, undefined}, {s2s_dhfile, undefined}, {s2s_dns_retries, 2}, {s2s_dns_timeout, timer:seconds(10)}, {s2s_max_retry_delay, timer:seconds(300)}, {s2s_protocol_options, undefined}, {s2s_queue_type, fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, {s2s_timeout, timer:minutes(10)}, {s2s_tls_compression, undefined}, {s2s_use_starttls, false}, {s2s_zlib, false}, {shaper, #{}}, {shaper_rules, []}, {sm_cache_life_time, fun(Host) -> ejabberd_config:get_option({cache_life_time, Host}) end}, {sm_cache_missed, fun(Host) -> ejabberd_config:get_option({cache_missed, Host}) end}, {sm_cache_size, fun(Host) -> ejabberd_config:get_option({cache_size, Host}) end}, {sm_db_type, fun(Host) -> ejabberd_config:default_ram_db(Host, ejabberd_sm) end}, {sm_use_cache, fun(Host) -> ejabberd_config:get_option({use_cache, Host}) end}, {sql_type, odbc}, {sql_connect_timeout, timer:seconds(5)}, {sql_database, undefined}, {sql_keepalive_interval, undefined}, {sql_password, <<"">>}, {sql_odbc_driver, <<"libtdsodbc.so">>}, % default is FreeTDS driver {sql_pool_size, fun(Host) -> case ejabberd_config:get_option({sql_type, Host}) of sqlite -> 1; _ -> 10 end end}, {sql_port, fun(Host) -> case ejabberd_config:get_option({sql_type, Host}) of mssql -> 1433; mysql -> 3306; pgsql -> 5432; _ -> undefined end end}, {sql_query_timeout, timer:seconds(60)}, {sql_queue_type, fun(Host) -> ejabberd_config:get_option({queue_type, Host}) end}, {sql_server, <<"localhost">>}, {sql_ssl, false}, {sql_ssl_cafile, undefined}, {sql_ssl_certfile, undefined}, {sql_ssl_verify, false}, {sql_start_interval, timer:seconds(30)}, {sql_username, <<"ejabberd">>}, {sql_prepared_statements, true}, {trusted_proxies, []}, {validate_stream, false}, {websocket_origin, []}, {websocket_ping_interval, timer:seconds(60)}, {websocket_timeout, timer:minutes(5)}, {jwt_key, undefined}, {jwt_jid_field, <<"jid">>}, {jwt_auth_only_rule, none}]. -spec globals() -> [atom()]. globals() -> [acme, allow_contrib_modules, api_permissions, append_host_config, auth_cache_life_time, auth_cache_missed, auth_cache_size, ca_file, captcha_cmd, captcha_host, captcha_limit, captcha_url, certfiles, cluster_backend, cluster_nodes, domain_balancing, ext_api_path_oauth, fqdn, hosts, host_config, listen, loglevel, log_rotate_count, log_rotate_size, negotiation_timeout, net_ticktime, new_sql_schema, node_start, oauth_cache_life_time, oauth_cache_missed, oauth_cache_size, oauth_cache_rest_failure_life_time, oauth_db_type, oauth_expire, oauth_use_cache, oom_killer, oom_queue, oom_watermark, queue_dir, redis_connect_timeout, redis_db, redis_password, redis_pool_size, redis_port, redis_queue_type, redis_server, registration_timeout, router_cache_life_time, router_cache_missed, router_cache_size, router_db_type, router_use_cache, rpc_timeout, s2s_max_retry_delay, shaper, sm_cache_life_time, sm_cache_missed, sm_cache_size, trusted_proxies, validate_stream, version, websocket_origin, websocket_ping_interval, websocket_timeout]. doc() -> ejabberd_options_doc:doc(). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec validator() -> econf:validator(). validator() -> Disallowed = ejabberd_config:globals(), {Validators, Required} = ejabberd_config:validators(Disallowed), econf:and_then( fun econf:group_dups/1, econf:options( Validators, [{disallowed, Required ++ Disallowed}, unique])). -spec fqdn(global | binary()) -> [binary()]. fqdn(global) -> {ok, Hostname} = inet:gethostname(), case inet:gethostbyname(Hostname) of {ok, #hostent{h_name = FQDN}} -> case jid:nameprep(iolist_to_binary(FQDN)) of error -> []; Domain -> [Domain] end; {error, _} -> [] end; fqdn(_) -> ejabberd_config:get_option(fqdn). -spec concat_binary(char()) -> fun(([binary()]) -> binary()). concat_binary(C) -> fun(Opts) -> str:join(Opts, <>) end. ejabberd-21.12/src/ejabberd_sm_sql.erl0000644000232200023220000001205414154362354020243 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_sm_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 9 Mar 2015 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sm_sql). -behaviour(ejabberd_sm). %% API -export([init/0, set_session/1, delete_session/1, get_sessions/0, get_sessions/1, get_sessions/2]). -include("ejabberd_sm.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> Node = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL SM table...", []), lists:foldl( fun(Host, ok) -> case ejabberd_sql:sql_query( Host, ?SQL("delete from sm where node=%(Node)s")) of {updated, _} -> ok; Err -> ?ERROR_MSG("Failed to clean 'sm' table: ~p", [Err]), {error, db_failure} end; (_, Err) -> Err end, ok, ejabberd_sm:get_vh_by_backend(?MODULE)). set_session(#session{sid = {Now, Pid}, usr = {U, LServer, R}, priority = Priority, info = Info}) -> InfoS = misc:term_to_expr(Info), PrioS = enc_priority(Priority), TS = now_to_timestamp(Now), PidS = misc:encode_pid(Pid), Node = erlang:atom_to_binary(node(Pid), latin1), case ?SQL_UPSERT(LServer, "sm", ["!usec=%(TS)d", "!pid=%(PidS)s", "node=%(Node)s", "username=%(U)s", "server_host=%(LServer)s", "resource=%(R)s", "priority=%(PrioS)s", "info=%(InfoS)s"]) of ok -> ok; _Err -> {error, db_failure} end. delete_session(#session{usr = {_, LServer, _}, sid = {Now, Pid}}) -> TS = now_to_timestamp(Now), PidS = list_to_binary(erlang:pid_to_list(Pid)), case ejabberd_sql:sql_query( LServer, ?SQL("delete from sm where usec=%(TS)d and pid=%(PidS)s")) of {updated, _} -> ok; _Err -> {error, db_failure} end. get_sessions() -> lists:flatmap( fun(LServer) -> get_sessions(LServer) end, ejabberd_sm:get_vh_by_backend(?MODULE)). get_sessions(LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(usec)d, @(pid)s, @(node)s, @(username)s," " @(resource)s, @(priority)s, @(info)s from sm" " where %(LServer)H")) of {selected, Rows} -> lists:flatmap( fun(Row) -> try [row_to_session(LServer, Row)] catch _:{bad_node, _} -> [] end end, Rows); _Err -> [] end. get_sessions(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(usec)d, @(pid)s, @(node)s, @(username)s," " @(resource)s, @(priority)s, @(info)s from sm" " where username=%(LUser)s and %(LServer)H")) of {selected, Rows} -> {ok, lists:flatmap( fun(Row) -> try [row_to_session(LServer, Row)] catch _:{bad_node, _} -> [] end end, Rows)}; _Err -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== now_to_timestamp({MSec, Sec, USec}) -> (MSec * 1000000 + Sec) * 1000000 + USec. timestamp_to_now(I) -> Head = I div 1000000, USec = I rem 1000000, MSec = Head div 1000000, Sec = Head rem 1000000, {MSec, Sec, USec}. dec_priority(Prio) -> case catch binary_to_integer(Prio) of {'EXIT', _} -> undefined; Int -> Int end. enc_priority(undefined) -> <<"">>; enc_priority(Int) when is_integer(Int) -> integer_to_binary(Int). row_to_session(LServer, {USec, PidS, NodeS, User, Resource, PrioS, InfoS}) -> Now = timestamp_to_now(USec), Pid = misc:decode_pid(PidS, NodeS), Priority = dec_priority(PrioS), Info = ejabberd_sql:decode_term(InfoS), #session{sid = {Now, Pid}, us = {User, LServer}, usr = {User, LServer, Resource}, priority = Priority, info = Info}. ejabberd-21.12/src/ELDAPv3.erl0000644000232200023220000032034414154362354016231 0ustar debalancedebalance%% Generated by the Erlang ASN.1 BER_V2-compiler version, utilizing bit-syntax:2.0.1 %% Purpose: encoder and decoder to the types in mod ELDAPv3 -module('ELDAPv3'). -compile(nowarn_unused_vars). -dialyzer(no_match). -include("ELDAPv3.hrl"). -asn1_info([{vsn,'2.0.1'}, {module,'ELDAPv3'}, {options,[{i,"src"},{outdir,"src"},noobj,{i,"."},{i,"asn1"}]}]). -export([encoding_rule/0,bit_string_format/0]). -export([ 'enc_LDAPMessage'/2, 'enc_MessageID'/2, 'enc_LDAPString'/2, 'enc_LDAPOID'/2, 'enc_LDAPDN'/2, 'enc_RelativeLDAPDN'/2, 'enc_AttributeType'/2, 'enc_AttributeDescription'/2, 'enc_AttributeDescriptionList'/2, 'enc_AttributeValue'/2, 'enc_AttributeValueAssertion'/2, 'enc_AssertionValue'/2, 'enc_Attribute'/2, 'enc_MatchingRuleId'/2, 'enc_LDAPResult'/2, 'enc_Referral'/2, 'enc_LDAPURL'/2, 'enc_Controls'/2, 'enc_Control'/2, 'enc_BindRequest'/2, 'enc_AuthenticationChoice'/2, 'enc_SaslCredentials'/2, 'enc_BindResponse'/2, 'enc_UnbindRequest'/2, 'enc_SearchRequest'/2, 'enc_Filter'/2, 'enc_SubstringFilter'/2, 'enc_MatchingRuleAssertion'/2, 'enc_SearchResultEntry'/2, 'enc_PartialAttributeList'/2, 'enc_SearchResultReference'/2, 'enc_SearchResultDone'/2, 'enc_ModifyRequest'/2, 'enc_AttributeTypeAndValues'/2, 'enc_ModifyResponse'/2, 'enc_AddRequest'/2, 'enc_AttributeList'/2, 'enc_AddResponse'/2, 'enc_DelRequest'/2, 'enc_DelResponse'/2, 'enc_ModifyDNRequest'/2, 'enc_ModifyDNResponse'/2, 'enc_CompareRequest'/2, 'enc_CompareResponse'/2, 'enc_AbandonRequest'/2, 'enc_ExtendedRequest'/2, 'enc_ExtendedResponse'/2, 'enc_PasswdModifyRequestValue'/2, 'enc_PasswdModifyResponseValue'/2 ]). -export([ 'dec_LDAPMessage'/2, 'dec_MessageID'/2, 'dec_LDAPString'/2, 'dec_LDAPOID'/2, 'dec_LDAPDN'/2, 'dec_RelativeLDAPDN'/2, 'dec_AttributeType'/2, 'dec_AttributeDescription'/2, 'dec_AttributeDescriptionList'/2, 'dec_AttributeValue'/2, 'dec_AttributeValueAssertion'/2, 'dec_AssertionValue'/2, 'dec_Attribute'/2, 'dec_MatchingRuleId'/2, 'dec_LDAPResult'/2, 'dec_Referral'/2, 'dec_LDAPURL'/2, 'dec_Controls'/2, 'dec_Control'/2, 'dec_BindRequest'/2, 'dec_AuthenticationChoice'/2, 'dec_SaslCredentials'/2, 'dec_BindResponse'/2, 'dec_UnbindRequest'/2, 'dec_SearchRequest'/2, 'dec_Filter'/2, 'dec_SubstringFilter'/2, 'dec_MatchingRuleAssertion'/2, 'dec_SearchResultEntry'/2, 'dec_PartialAttributeList'/2, 'dec_SearchResultReference'/2, 'dec_SearchResultDone'/2, 'dec_ModifyRequest'/2, 'dec_AttributeTypeAndValues'/2, 'dec_ModifyResponse'/2, 'dec_AddRequest'/2, 'dec_AttributeList'/2, 'dec_AddResponse'/2, 'dec_DelRequest'/2, 'dec_DelResponse'/2, 'dec_ModifyDNRequest'/2, 'dec_ModifyDNResponse'/2, 'dec_CompareRequest'/2, 'dec_CompareResponse'/2, 'dec_AbandonRequest'/2, 'dec_ExtendedRequest'/2, 'dec_ExtendedResponse'/2, 'dec_PasswdModifyRequestValue'/2, 'dec_PasswdModifyResponseValue'/2 ]). -export([ 'maxInt'/0, 'passwdModifyOID'/0 ]). -export([info/0]). -export([encode/2,decode/2]). encoding_rule() -> ber. bit_string_format() -> bitstring. encode(Type,Data) -> case catch encode_disp(Type,Data) of {'EXIT',{error,Reason}} -> {error,Reason}; {'EXIT',Reason} -> {error,{asn1,Reason}}; {Bytes,_Len} -> {ok,iolist_to_binary(Bytes)}; Bytes -> {ok,iolist_to_binary(Bytes)} end. decode(Type,Data) -> case catch decode_disp(Type,element(1, ber_decode_nif(Data))) of {'EXIT',{error,Reason}} -> {error,Reason}; {'EXIT',Reason} -> {error,{asn1,Reason}}; Result -> {ok,Result} end. encode_disp('LDAPMessage',Data) -> 'enc_LDAPMessage'(Data); encode_disp('MessageID',Data) -> 'enc_MessageID'(Data); encode_disp('LDAPString',Data) -> 'enc_LDAPString'(Data); encode_disp('LDAPOID',Data) -> 'enc_LDAPOID'(Data); encode_disp('LDAPDN',Data) -> 'enc_LDAPDN'(Data); encode_disp('RelativeLDAPDN',Data) -> 'enc_RelativeLDAPDN'(Data); encode_disp('AttributeType',Data) -> 'enc_AttributeType'(Data); encode_disp('AttributeDescription',Data) -> 'enc_AttributeDescription'(Data); encode_disp('AttributeDescriptionList',Data) -> 'enc_AttributeDescriptionList'(Data); encode_disp('AttributeValue',Data) -> 'enc_AttributeValue'(Data); encode_disp('AttributeValueAssertion',Data) -> 'enc_AttributeValueAssertion'(Data); encode_disp('AssertionValue',Data) -> 'enc_AssertionValue'(Data); encode_disp('Attribute',Data) -> 'enc_Attribute'(Data); encode_disp('MatchingRuleId',Data) -> 'enc_MatchingRuleId'(Data); encode_disp('LDAPResult',Data) -> 'enc_LDAPResult'(Data); encode_disp('Referral',Data) -> 'enc_Referral'(Data); encode_disp('LDAPURL',Data) -> 'enc_LDAPURL'(Data); encode_disp('Controls',Data) -> 'enc_Controls'(Data); encode_disp('Control',Data) -> 'enc_Control'(Data); encode_disp('BindRequest',Data) -> 'enc_BindRequest'(Data); encode_disp('AuthenticationChoice',Data) -> 'enc_AuthenticationChoice'(Data); encode_disp('SaslCredentials',Data) -> 'enc_SaslCredentials'(Data); encode_disp('BindResponse',Data) -> 'enc_BindResponse'(Data); encode_disp('UnbindRequest',Data) -> 'enc_UnbindRequest'(Data); encode_disp('SearchRequest',Data) -> 'enc_SearchRequest'(Data); encode_disp('Filter',Data) -> 'enc_Filter'(Data); encode_disp('SubstringFilter',Data) -> 'enc_SubstringFilter'(Data); encode_disp('MatchingRuleAssertion',Data) -> 'enc_MatchingRuleAssertion'(Data); encode_disp('SearchResultEntry',Data) -> 'enc_SearchResultEntry'(Data); encode_disp('PartialAttributeList',Data) -> 'enc_PartialAttributeList'(Data); encode_disp('SearchResultReference',Data) -> 'enc_SearchResultReference'(Data); encode_disp('SearchResultDone',Data) -> 'enc_SearchResultDone'(Data); encode_disp('ModifyRequest',Data) -> 'enc_ModifyRequest'(Data); encode_disp('AttributeTypeAndValues',Data) -> 'enc_AttributeTypeAndValues'(Data); encode_disp('ModifyResponse',Data) -> 'enc_ModifyResponse'(Data); encode_disp('AddRequest',Data) -> 'enc_AddRequest'(Data); encode_disp('AttributeList',Data) -> 'enc_AttributeList'(Data); encode_disp('AddResponse',Data) -> 'enc_AddResponse'(Data); encode_disp('DelRequest',Data) -> 'enc_DelRequest'(Data); encode_disp('DelResponse',Data) -> 'enc_DelResponse'(Data); encode_disp('ModifyDNRequest',Data) -> 'enc_ModifyDNRequest'(Data); encode_disp('ModifyDNResponse',Data) -> 'enc_ModifyDNResponse'(Data); encode_disp('CompareRequest',Data) -> 'enc_CompareRequest'(Data); encode_disp('CompareResponse',Data) -> 'enc_CompareResponse'(Data); encode_disp('AbandonRequest',Data) -> 'enc_AbandonRequest'(Data); encode_disp('ExtendedRequest',Data) -> 'enc_ExtendedRequest'(Data); encode_disp('ExtendedResponse',Data) -> 'enc_ExtendedResponse'(Data); encode_disp('PasswdModifyRequestValue',Data) -> 'enc_PasswdModifyRequestValue'(Data); encode_disp('PasswdModifyResponseValue',Data) -> 'enc_PasswdModifyResponseValue'(Data); encode_disp(Type,_Data) -> exit({error,{asn1,{undefined_type,Type}}}). decode_disp('LDAPMessage',Data) -> 'dec_LDAPMessage'(Data); decode_disp('MessageID',Data) -> 'dec_MessageID'(Data); decode_disp('LDAPString',Data) -> 'dec_LDAPString'(Data); decode_disp('LDAPOID',Data) -> 'dec_LDAPOID'(Data); decode_disp('LDAPDN',Data) -> 'dec_LDAPDN'(Data); decode_disp('RelativeLDAPDN',Data) -> 'dec_RelativeLDAPDN'(Data); decode_disp('AttributeType',Data) -> 'dec_AttributeType'(Data); decode_disp('AttributeDescription',Data) -> 'dec_AttributeDescription'(Data); decode_disp('AttributeDescriptionList',Data) -> 'dec_AttributeDescriptionList'(Data); decode_disp('AttributeValue',Data) -> 'dec_AttributeValue'(Data); decode_disp('AttributeValueAssertion',Data) -> 'dec_AttributeValueAssertion'(Data); decode_disp('AssertionValue',Data) -> 'dec_AssertionValue'(Data); decode_disp('Attribute',Data) -> 'dec_Attribute'(Data); decode_disp('MatchingRuleId',Data) -> 'dec_MatchingRuleId'(Data); decode_disp('LDAPResult',Data) -> 'dec_LDAPResult'(Data); decode_disp('Referral',Data) -> 'dec_Referral'(Data); decode_disp('LDAPURL',Data) -> 'dec_LDAPURL'(Data); decode_disp('Controls',Data) -> 'dec_Controls'(Data); decode_disp('Control',Data) -> 'dec_Control'(Data); decode_disp('BindRequest',Data) -> 'dec_BindRequest'(Data); decode_disp('AuthenticationChoice',Data) -> 'dec_AuthenticationChoice'(Data); decode_disp('SaslCredentials',Data) -> 'dec_SaslCredentials'(Data); decode_disp('BindResponse',Data) -> 'dec_BindResponse'(Data); decode_disp('UnbindRequest',Data) -> 'dec_UnbindRequest'(Data); decode_disp('SearchRequest',Data) -> 'dec_SearchRequest'(Data); decode_disp('Filter',Data) -> 'dec_Filter'(Data); decode_disp('SubstringFilter',Data) -> 'dec_SubstringFilter'(Data); decode_disp('MatchingRuleAssertion',Data) -> 'dec_MatchingRuleAssertion'(Data); decode_disp('SearchResultEntry',Data) -> 'dec_SearchResultEntry'(Data); decode_disp('PartialAttributeList',Data) -> 'dec_PartialAttributeList'(Data); decode_disp('SearchResultReference',Data) -> 'dec_SearchResultReference'(Data); decode_disp('SearchResultDone',Data) -> 'dec_SearchResultDone'(Data); decode_disp('ModifyRequest',Data) -> 'dec_ModifyRequest'(Data); decode_disp('AttributeTypeAndValues',Data) -> 'dec_AttributeTypeAndValues'(Data); decode_disp('ModifyResponse',Data) -> 'dec_ModifyResponse'(Data); decode_disp('AddRequest',Data) -> 'dec_AddRequest'(Data); decode_disp('AttributeList',Data) -> 'dec_AttributeList'(Data); decode_disp('AddResponse',Data) -> 'dec_AddResponse'(Data); decode_disp('DelRequest',Data) -> 'dec_DelRequest'(Data); decode_disp('DelResponse',Data) -> 'dec_DelResponse'(Data); decode_disp('ModifyDNRequest',Data) -> 'dec_ModifyDNRequest'(Data); decode_disp('ModifyDNResponse',Data) -> 'dec_ModifyDNResponse'(Data); decode_disp('CompareRequest',Data) -> 'dec_CompareRequest'(Data); decode_disp('CompareResponse',Data) -> 'dec_CompareResponse'(Data); decode_disp('AbandonRequest',Data) -> 'dec_AbandonRequest'(Data); decode_disp('ExtendedRequest',Data) -> 'dec_ExtendedRequest'(Data); decode_disp('ExtendedResponse',Data) -> 'dec_ExtendedResponse'(Data); decode_disp('PasswdModifyRequestValue',Data) -> 'dec_PasswdModifyRequestValue'(Data); decode_disp('PasswdModifyResponseValue',Data) -> 'dec_PasswdModifyResponseValue'(Data); decode_disp(Type,_Data) -> exit({error,{asn1,{undefined_type,Type}}}). info() -> case ?MODULE:module_info(attributes) of Attributes when is_list(Attributes) -> case lists:keyfind(asn1_info, 1, Attributes) of {_,Info} when is_list(Info) -> Info; _ -> [] end; _ -> [] end. %%================================ %% LDAPMessage %%================================ 'enc_LDAPMessage'(Val) -> 'enc_LDAPMessage'(Val, [<<48>>]). 'enc_LDAPMessage'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3} = Val, %%------------------------------------------------- %% attribute messageID(1) with type INTEGER %%------------------------------------------------- {EncBytes1,EncLen1} = encode_integer(Cindex1, [<<2>>]), %%------------------------------------------------- %% attribute protocolOp(2) with type CHOICE %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_LDAPMessage_protocolOp'(Cindex2, []), %%------------------------------------------------- %% attribute controls(3) External ELDAPv3:Controls OPTIONAL %%------------------------------------------------- {EncBytes3,EncLen3} = case Cindex3 of asn1_NOVALUE -> {<<>>,0}; _ -> 'enc_Controls'(Cindex3, [<<160>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], LenSoFar = EncLen1 + EncLen2 + EncLen3, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% LDAPMessage_protocolOp %%================================ 'enc_LDAPMessage_protocolOp'(Val, TagIn) -> {EncBytes,EncLen} = case element(1,Val) of bindRequest -> 'enc_BindRequest'(element(2,Val), [<<96>>]); bindResponse -> 'enc_BindResponse'(element(2,Val), [<<97>>]); unbindRequest -> encode_null(element(2,Val), [<<66>>]); searchRequest -> 'enc_SearchRequest'(element(2,Val), [<<99>>]); searchResEntry -> 'enc_SearchResultEntry'(element(2,Val), [<<100>>]); searchResDone -> 'enc_SearchResultDone'(element(2,Val), [<<101>>]); searchResRef -> 'enc_SearchResultReference'(element(2,Val), [<<115>>]); modifyRequest -> 'enc_ModifyRequest'(element(2,Val), [<<102>>]); modifyResponse -> 'enc_ModifyResponse'(element(2,Val), [<<103>>]); addRequest -> 'enc_AddRequest'(element(2,Val), [<<104>>]); addResponse -> 'enc_AddResponse'(element(2,Val), [<<105>>]); delRequest -> encode_restricted_string(element(2,Val), [<<74>>]); delResponse -> 'enc_DelResponse'(element(2,Val), [<<107>>]); modDNRequest -> 'enc_ModifyDNRequest'(element(2,Val), [<<108>>]); modDNResponse -> 'enc_ModifyDNResponse'(element(2,Val), [<<109>>]); compareRequest -> 'enc_CompareRequest'(element(2,Val), [<<110>>]); compareResponse -> 'enc_CompareResponse'(element(2,Val), [<<111>>]); abandonRequest -> encode_integer(element(2,Val), [<<80>>]); extendedReq -> 'enc_ExtendedRequest'(element(2,Val), [<<119>>]); extendedResp -> 'enc_ExtendedResponse'(element(2,Val), [<<120>>]); Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, encode_tags(TagIn, EncBytes, EncLen). 'dec_LDAPMessage_protocolOp'(Tlv, TagIn) -> Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'bindRequest' {65536, V1} -> {bindRequest, 'dec_BindRequest'(V1, [])}; %% 'bindResponse' {65537, V1} -> {bindResponse, 'dec_BindResponse'(V1, [])}; %% 'unbindRequest' {65538, V1} -> {unbindRequest, decode_null(V1,[])}; %% 'searchRequest' {65539, V1} -> {searchRequest, 'dec_SearchRequest'(V1, [])}; %% 'searchResEntry' {65540, V1} -> {searchResEntry, 'dec_SearchResultEntry'(V1, [])}; %% 'searchResDone' {65541, V1} -> {searchResDone, 'dec_SearchResultDone'(V1, [])}; %% 'searchResRef' {65555, V1} -> {searchResRef, 'dec_SearchResultReference'(V1, [])}; %% 'modifyRequest' {65542, V1} -> {modifyRequest, 'dec_ModifyRequest'(V1, [])}; %% 'modifyResponse' {65543, V1} -> {modifyResponse, 'dec_ModifyResponse'(V1, [])}; %% 'addRequest' {65544, V1} -> {addRequest, 'dec_AddRequest'(V1, [])}; %% 'addResponse' {65545, V1} -> {addResponse, 'dec_AddResponse'(V1, [])}; %% 'delRequest' {65546, V1} -> {delRequest, decode_restricted_string(V1,[])}; %% 'delResponse' {65547, V1} -> {delResponse, 'dec_DelResponse'(V1, [])}; %% 'modDNRequest' {65548, V1} -> {modDNRequest, 'dec_ModifyDNRequest'(V1, [])}; %% 'modDNResponse' {65549, V1} -> {modDNResponse, 'dec_ModifyDNResponse'(V1, [])}; %% 'compareRequest' {65550, V1} -> {compareRequest, 'dec_CompareRequest'(V1, [])}; %% 'compareResponse' {65551, V1} -> {compareResponse, 'dec_CompareResponse'(V1, [])}; %% 'abandonRequest' {65552, V1} -> {abandonRequest, decode_integer(V1,{0,2147483647},[])}; %% 'extendedReq' {65559, V1} -> {extendedReq, 'dec_ExtendedRequest'(V1, [])}; %% 'extendedResp' {65560, V1} -> {extendedResp, 'dec_ExtendedResponse'(V1, [])}; Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . 'dec_LDAPMessage'(Tlv) -> 'dec_LDAPMessage'(Tlv, [16]). 'dec_LDAPMessage'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute messageID(1) with type INTEGER %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_integer(V1,{0,2147483647},[2]), %%------------------------------------------------- %% attribute protocolOp(2) with type CHOICE %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_LDAPMessage_protocolOp'(V2, []), %%------------------------------------------------- %% attribute controls(3) External ELDAPv3:Controls OPTIONAL %%------------------------------------------------- {Term3,Tlv4} = case Tlv3 of [{131072,V3}|TempTlv4] -> {'dec_Controls'(V3, []), TempTlv4}; _ -> { asn1_NOVALUE, Tlv3} end, case Tlv4 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed end, {'LDAPMessage', Term1, Term2, Term3}. %%================================ %% MessageID %%================================ 'enc_MessageID'(Val) -> 'enc_MessageID'(Val, [<<2>>]). 'enc_MessageID'(Val, TagIn) -> encode_integer(Val, TagIn). 'dec_MessageID'(Tlv) -> 'dec_MessageID'(Tlv, [2]). 'dec_MessageID'(Tlv, TagIn) -> decode_integer(Tlv,{0,2147483647},TagIn). %%================================ %% LDAPString %%================================ 'enc_LDAPString'(Val) -> 'enc_LDAPString'(Val, [<<4>>]). 'enc_LDAPString'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_LDAPString'(Tlv) -> 'dec_LDAPString'(Tlv, [4]). 'dec_LDAPString'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% LDAPOID %%================================ 'enc_LDAPOID'(Val) -> 'enc_LDAPOID'(Val, [<<4>>]). 'enc_LDAPOID'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_LDAPOID'(Tlv) -> 'dec_LDAPOID'(Tlv, [4]). 'dec_LDAPOID'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% LDAPDN %%================================ 'enc_LDAPDN'(Val) -> 'enc_LDAPDN'(Val, [<<4>>]). 'enc_LDAPDN'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_LDAPDN'(Tlv) -> 'dec_LDAPDN'(Tlv, [4]). 'dec_LDAPDN'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% RelativeLDAPDN %%================================ 'enc_RelativeLDAPDN'(Val) -> 'enc_RelativeLDAPDN'(Val, [<<4>>]). 'enc_RelativeLDAPDN'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_RelativeLDAPDN'(Tlv) -> 'dec_RelativeLDAPDN'(Tlv, [4]). 'dec_RelativeLDAPDN'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% AttributeType %%================================ 'enc_AttributeType'(Val) -> 'enc_AttributeType'(Val, [<<4>>]). 'enc_AttributeType'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_AttributeType'(Tlv) -> 'dec_AttributeType'(Tlv, [4]). 'dec_AttributeType'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% AttributeDescription %%================================ 'enc_AttributeDescription'(Val) -> 'enc_AttributeDescription'(Val, [<<4>>]). 'enc_AttributeDescription'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_AttributeDescription'(Tlv) -> 'dec_AttributeDescription'(Tlv, [4]). 'dec_AttributeDescription'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% AttributeDescriptionList %%================================ 'enc_AttributeDescriptionList'(Val) -> 'enc_AttributeDescriptionList'(Val, [<<48>>]). 'enc_AttributeDescriptionList'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_AttributeDescriptionList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_AttributeDescriptionList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeDescriptionList_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_AttributeDescriptionList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_AttributeDescriptionList'(Tlv) -> 'dec_AttributeDescriptionList'(Tlv, [16]). 'dec_AttributeDescriptionList'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. %%================================ %% AttributeValue %%================================ 'enc_AttributeValue'(Val) -> 'enc_AttributeValue'(Val, [<<4>>]). 'enc_AttributeValue'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_AttributeValue'(Tlv) -> 'dec_AttributeValue'(Tlv, [4]). 'dec_AttributeValue'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% AttributeValueAssertion %%================================ 'enc_AttributeValueAssertion'(Val) -> 'enc_AttributeValueAssertion'(Val, [<<48>>]). 'enc_AttributeValueAssertion'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute attributeDesc(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute assertionValue(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AttributeValueAssertion'(Tlv) -> 'dec_AttributeValueAssertion'(Tlv, [16]). 'dec_AttributeValueAssertion'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute attributeDesc(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute assertionValue(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'AttributeValueAssertion', Term1, Term2}. %%================================ %% AssertionValue %%================================ 'enc_AssertionValue'(Val) -> 'enc_AssertionValue'(Val, [<<4>>]). 'enc_AssertionValue'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_AssertionValue'(Tlv) -> 'dec_AssertionValue'(Tlv, [4]). 'dec_AssertionValue'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% Attribute %%================================ 'enc_Attribute'(Val) -> 'enc_Attribute'(Val, [<<48>>]). 'enc_Attribute'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_Attribute_vals'(Cindex2, [<<49>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% Attribute_vals %%================================ 'enc_Attribute_vals'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_Attribute_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_Attribute_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Attribute_vals_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_Attribute_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Attribute_vals'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. 'dec_Attribute'(Tlv) -> 'dec_Attribute'(Tlv, [16]). 'dec_Attribute'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_Attribute_vals'(V2, [17]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'Attribute', Term1, Term2}. %%================================ %% MatchingRuleId %%================================ 'enc_MatchingRuleId'(Val) -> 'enc_MatchingRuleId'(Val, [<<4>>]). 'enc_MatchingRuleId'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_MatchingRuleId'(Tlv) -> 'dec_MatchingRuleId'(Tlv, [4]). 'dec_MatchingRuleId'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% LDAPResult %%================================ 'enc_LDAPResult'(Val) -> 'enc_LDAPResult'(Val, [<<48>>]). 'enc_LDAPResult'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of success -> encode_enumerated(0, [<<10>>]); operationsError -> encode_enumerated(1, [<<10>>]); protocolError -> encode_enumerated(2, [<<10>>]); timeLimitExceeded -> encode_enumerated(3, [<<10>>]); sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); compareFalse -> encode_enumerated(5, [<<10>>]); compareTrue -> encode_enumerated(6, [<<10>>]); authMethodNotSupported -> encode_enumerated(7, [<<10>>]); strongAuthRequired -> encode_enumerated(8, [<<10>>]); referral -> encode_enumerated(10, [<<10>>]); adminLimitExceeded -> encode_enumerated(11, [<<10>>]); unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); confidentialityRequired -> encode_enumerated(13, [<<10>>]); saslBindInProgress -> encode_enumerated(14, [<<10>>]); noSuchAttribute -> encode_enumerated(16, [<<10>>]); undefinedAttributeType -> encode_enumerated(17, [<<10>>]); inappropriateMatching -> encode_enumerated(18, [<<10>>]); constraintViolation -> encode_enumerated(19, [<<10>>]); attributeOrValueExists -> encode_enumerated(20, [<<10>>]); invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); noSuchObject -> encode_enumerated(32, [<<10>>]); aliasProblem -> encode_enumerated(33, [<<10>>]); invalidDNSyntax -> encode_enumerated(34, [<<10>>]); aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); invalidCredentials -> encode_enumerated(49, [<<10>>]); insufficientAccessRights -> encode_enumerated(50, [<<10>>]); busy -> encode_enumerated(51, [<<10>>]); unavailable -> encode_enumerated(52, [<<10>>]); unwillingToPerform -> encode_enumerated(53, [<<10>>]); loopDetect -> encode_enumerated(54, [<<10>>]); namingViolation -> encode_enumerated(64, [<<10>>]); objectClassViolation -> encode_enumerated(65, [<<10>>]); notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); entryAlreadyExists -> encode_enumerated(68, [<<10>>]); objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); other -> encode_enumerated(80, [<<10>>]); Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) end, %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {EncBytes4,EncLen4} = case Cindex4 of asn1_NOVALUE -> {<<>>,0}; _ -> 'enc_Referral'(Cindex4, [<<163>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_LDAPResult'(Tlv) -> 'dec_LDAPResult'(Tlv, [16]). 'dec_LDAPResult'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {Term4,Tlv5} = case Tlv4 of [{131075,V4}|TempTlv5] -> {'dec_Referral'(V4, []), TempTlv5}; _ -> { asn1_NOVALUE, Tlv4} end, case Tlv5 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed end, {'LDAPResult', Term1, Term2, Term3, Term4}. %%================================ %% Referral %%================================ 'enc_Referral'(Val) -> 'enc_Referral'(Val, [<<48>>]). 'enc_Referral'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_Referral_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_Referral_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Referral_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_Referral_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Referral'(Tlv) -> 'dec_Referral'(Tlv, [16]). 'dec_Referral'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. %%================================ %% LDAPURL %%================================ 'enc_LDAPURL'(Val) -> 'enc_LDAPURL'(Val, [<<4>>]). 'enc_LDAPURL'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_LDAPURL'(Tlv) -> 'dec_LDAPURL'(Tlv, [4]). 'dec_LDAPURL'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% Controls %%================================ 'enc_Controls'(Val) -> 'enc_Controls'(Val, [<<48>>]). 'enc_Controls'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_Controls_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_Controls_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Controls_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_Control'(H, [<<48>>]), 'enc_Controls_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Controls'(Tlv) -> 'dec_Controls'(Tlv, [16]). 'dec_Controls'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Control'(V1, [16]) || V1 <- Tlv1]. %%================================ %% Control %%================================ 'enc_Control'(Val) -> 'enc_Control'(Val, [<<48>>]). 'enc_Control'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3} = Val, %%------------------------------------------------- %% attribute controlType(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute criticality(2) with type BOOLEAN DEFAULT = false %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of asn1_DEFAULT -> {<<>>,0}; false -> {<<>>,0}; _ -> encode_boolean(Cindex2, [<<1>>]) end, %%------------------------------------------------- %% attribute controlValue(3) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes3,EncLen3} = case Cindex3 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex3, [<<4>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], LenSoFar = EncLen1 + EncLen2 + EncLen3, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_Control'(Tlv) -> 'dec_Control'(Tlv, [16]). 'dec_Control'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute controlType(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute criticality(2) with type BOOLEAN DEFAULT = false %%------------------------------------------------- {Term2,Tlv3} = case Tlv2 of [{1,V2}|TempTlv3] -> {decode_boolean(V2,[]), TempTlv3}; _ -> {false,Tlv2} end, %%------------------------------------------------- %% attribute controlValue(3) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term3,Tlv4} = case Tlv3 of [{4,V3}|TempTlv4] -> {decode_restricted_string(V3,[]), TempTlv4}; _ -> { asn1_NOVALUE, Tlv3} end, case Tlv4 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed end, {'Control', Term1, Term2, Term3}. %%================================ %% BindRequest %%================================ 'enc_BindRequest'(Val) -> 'enc_BindRequest'(Val, [<<96>>]). 'enc_BindRequest'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3} = Val, %%------------------------------------------------- %% attribute version(1) with type INTEGER %%------------------------------------------------- {EncBytes1,EncLen1} = encode_integer(Cindex1, [<<2>>]), %%------------------------------------------------- %% attribute name(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), %%------------------------------------------------- %% attribute authentication(3) External ELDAPv3:AuthenticationChoice %%------------------------------------------------- {EncBytes3,EncLen3} = 'enc_AuthenticationChoice'(Cindex3, []), BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], LenSoFar = EncLen1 + EncLen2 + EncLen3, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindRequest'(Tlv) -> 'dec_BindRequest'(Tlv, [65536]). 'dec_BindRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute version(1) with type INTEGER %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_integer(V1,{1,127},[2]), %%------------------------------------------------- %% attribute name(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute authentication(3) External ELDAPv3:AuthenticationChoice %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = 'dec_AuthenticationChoice'(V3, []), case Tlv4 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed end, {'BindRequest', Term1, Term2, Term3}. %%================================ %% AuthenticationChoice %%================================ 'enc_AuthenticationChoice'(Val) -> 'enc_AuthenticationChoice'(Val, []). 'enc_AuthenticationChoice'(Val, TagIn) -> {EncBytes,EncLen} = case element(1,Val) of simple -> encode_restricted_string(element(2,Val), [<<128>>]); sasl -> 'enc_SaslCredentials'(element(2,Val), [<<163>>]); Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, encode_tags(TagIn, EncBytes, EncLen). 'dec_AuthenticationChoice'(Tlv) -> 'dec_AuthenticationChoice'(Tlv, []). 'dec_AuthenticationChoice'(Tlv, TagIn) -> Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'simple' {131072, V1} -> {simple, decode_restricted_string(V1,[])}; %% 'sasl' {131075, V1} -> {sasl, 'dec_SaslCredentials'(V1, [])}; Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . %%================================ %% SaslCredentials %%================================ 'enc_SaslCredentials'(Val) -> 'enc_SaslCredentials'(Val, [<<48>>]). 'enc_SaslCredentials'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute mechanism(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute credentials(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex2, [<<4>>]) end, BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SaslCredentials'(Tlv) -> 'dec_SaslCredentials'(Tlv, [16]). 'dec_SaslCredentials'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute mechanism(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute credentials(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term2,Tlv3} = case Tlv2 of [{4,V2}|TempTlv3] -> {decode_restricted_string(V2,[]), TempTlv3}; _ -> { asn1_NOVALUE, Tlv2} end, case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'SaslCredentials', Term1, Term2}. %%================================ %% BindResponse %%================================ 'enc_BindResponse'(Val) -> 'enc_BindResponse'(Val, [<<97>>]). 'enc_BindResponse'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5} = Val, %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of success -> encode_enumerated(0, [<<10>>]); operationsError -> encode_enumerated(1, [<<10>>]); protocolError -> encode_enumerated(2, [<<10>>]); timeLimitExceeded -> encode_enumerated(3, [<<10>>]); sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); compareFalse -> encode_enumerated(5, [<<10>>]); compareTrue -> encode_enumerated(6, [<<10>>]); authMethodNotSupported -> encode_enumerated(7, [<<10>>]); strongAuthRequired -> encode_enumerated(8, [<<10>>]); referral -> encode_enumerated(10, [<<10>>]); adminLimitExceeded -> encode_enumerated(11, [<<10>>]); unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); confidentialityRequired -> encode_enumerated(13, [<<10>>]); saslBindInProgress -> encode_enumerated(14, [<<10>>]); noSuchAttribute -> encode_enumerated(16, [<<10>>]); undefinedAttributeType -> encode_enumerated(17, [<<10>>]); inappropriateMatching -> encode_enumerated(18, [<<10>>]); constraintViolation -> encode_enumerated(19, [<<10>>]); attributeOrValueExists -> encode_enumerated(20, [<<10>>]); invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); noSuchObject -> encode_enumerated(32, [<<10>>]); aliasProblem -> encode_enumerated(33, [<<10>>]); invalidDNSyntax -> encode_enumerated(34, [<<10>>]); aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); invalidCredentials -> encode_enumerated(49, [<<10>>]); insufficientAccessRights -> encode_enumerated(50, [<<10>>]); busy -> encode_enumerated(51, [<<10>>]); unavailable -> encode_enumerated(52, [<<10>>]); unwillingToPerform -> encode_enumerated(53, [<<10>>]); loopDetect -> encode_enumerated(54, [<<10>>]); namingViolation -> encode_enumerated(64, [<<10>>]); objectClassViolation -> encode_enumerated(65, [<<10>>]); notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); entryAlreadyExists -> encode_enumerated(68, [<<10>>]); objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); other -> encode_enumerated(80, [<<10>>]); Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) end, %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {EncBytes4,EncLen4} = case Cindex4 of asn1_NOVALUE -> {<<>>,0}; _ -> 'enc_Referral'(Cindex4, [<<163>>]) end, %%------------------------------------------------- %% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes5,EncLen5} = case Cindex5 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex5, [<<135>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_BindResponse'(Tlv) -> 'dec_BindResponse'(Tlv, [65537]). 'dec_BindResponse'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {Term4,Tlv5} = case Tlv4 of [{131075,V4}|TempTlv5] -> {'dec_Referral'(V4, []), TempTlv5}; _ -> { asn1_NOVALUE, Tlv4} end, %%------------------------------------------------- %% attribute serverSaslCreds(5) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term5,Tlv6} = case Tlv5 of [{131079,V5}|TempTlv6] -> {decode_restricted_string(V5,[]), TempTlv6}; _ -> { asn1_NOVALUE, Tlv5} end, case Tlv6 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv6}}}) % extra fields not allowed end, {'BindResponse', Term1, Term2, Term3, Term4, Term5}. %%================================ %% UnbindRequest %%================================ 'enc_UnbindRequest'(Val) -> 'enc_UnbindRequest'(Val, [<<66>>]). 'enc_UnbindRequest'(Val, TagIn) -> encode_null(Val, TagIn). 'dec_UnbindRequest'(Tlv) -> 'dec_UnbindRequest'(Tlv, [65538]). 'dec_UnbindRequest'(Tlv, TagIn) -> decode_null(Tlv,TagIn). %%================================ %% SearchRequest %%================================ 'enc_SearchRequest'(Val) -> 'enc_SearchRequest'(Val, [<<99>>]). 'enc_SearchRequest'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6, Cindex7, Cindex8} = Val, %%------------------------------------------------- %% attribute baseObject(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute scope(2) with type ENUMERATED %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of baseObject -> encode_enumerated(0, [<<10>>]); singleLevel -> encode_enumerated(1, [<<10>>]); wholeSubtree -> encode_enumerated(2, [<<10>>]); Enumval2 -> exit({error,{asn1, {enumerated_not_in_range,Enumval2}}}) end, %%------------------------------------------------- %% attribute derefAliases(3) with type ENUMERATED %%------------------------------------------------- {EncBytes3,EncLen3} = case Cindex3 of neverDerefAliases -> encode_enumerated(0, [<<10>>]); derefInSearching -> encode_enumerated(1, [<<10>>]); derefFindingBaseObj -> encode_enumerated(2, [<<10>>]); derefAlways -> encode_enumerated(3, [<<10>>]); Enumval3 -> exit({error,{asn1, {enumerated_not_in_range,Enumval3}}}) end, %%------------------------------------------------- %% attribute sizeLimit(4) with type INTEGER %%------------------------------------------------- {EncBytes4,EncLen4} = encode_integer(Cindex4, [<<2>>]), %%------------------------------------------------- %% attribute timeLimit(5) with type INTEGER %%------------------------------------------------- {EncBytes5,EncLen5} = encode_integer(Cindex5, [<<2>>]), %%------------------------------------------------- %% attribute typesOnly(6) with type BOOLEAN %%------------------------------------------------- {EncBytes6,EncLen6} = encode_boolean(Cindex6, [<<1>>]), %%------------------------------------------------- %% attribute filter(7) External ELDAPv3:Filter %%------------------------------------------------- {EncBytes7,EncLen7} = 'enc_Filter'(Cindex7, []), %%------------------------------------------------- %% attribute attributes(8) External ELDAPv3:AttributeDescriptionList %%------------------------------------------------- {EncBytes8,EncLen8} = 'enc_AttributeDescriptionList'(Cindex8, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6, EncBytes7, EncBytes8], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6 + EncLen7 + EncLen8, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchRequest'(Tlv) -> 'dec_SearchRequest'(Tlv, [65539]). 'dec_SearchRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute baseObject(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute scope(2) with type ENUMERATED %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_enumerated(V2,[{baseObject,0},{singleLevel,1},{wholeSubtree,2}],[10]), %%------------------------------------------------- %% attribute derefAliases(3) with type ENUMERATED %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_enumerated(V3,[{neverDerefAliases,0},{derefInSearching,1},{derefFindingBaseObj,2},{derefAlways,3}],[10]), %%------------------------------------------------- %% attribute sizeLimit(4) with type INTEGER %%------------------------------------------------- [V4|Tlv5] = Tlv4, Term4 = decode_integer(V4,{0,2147483647},[2]), %%------------------------------------------------- %% attribute timeLimit(5) with type INTEGER %%------------------------------------------------- [V5|Tlv6] = Tlv5, Term5 = decode_integer(V5,{0,2147483647},[2]), %%------------------------------------------------- %% attribute typesOnly(6) with type BOOLEAN %%------------------------------------------------- [V6|Tlv7] = Tlv6, Term6 = decode_boolean(V6,[1]), %%------------------------------------------------- %% attribute filter(7) External ELDAPv3:Filter %%------------------------------------------------- [V7|Tlv8] = Tlv7, Term7 = 'dec_Filter'(V7, []), %%------------------------------------------------- %% attribute attributes(8) External ELDAPv3:AttributeDescriptionList %%------------------------------------------------- [V8|Tlv9] = Tlv8, Term8 = 'dec_AttributeDescriptionList'(V8, [16]), case Tlv9 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv9}}}) % extra fields not allowed end, {'SearchRequest', Term1, Term2, Term3, Term4, Term5, Term6, Term7, Term8}. %%================================ %% Filter %%================================ 'enc_Filter'(Val) -> 'enc_Filter'(Val, []). 'enc_Filter'(Val, TagIn) -> {EncBytes,EncLen} = case element(1,Val) of 'and' -> 'enc_Filter_and'(element(2,Val), [<<160>>]); 'or' -> 'enc_Filter_or'(element(2,Val), [<<161>>]); 'not' -> 'enc_Filter'(element(2,Val), [<<162>>]); equalityMatch -> 'enc_AttributeValueAssertion'(element(2,Val), [<<163>>]); substrings -> 'enc_SubstringFilter'(element(2,Val), [<<164>>]); greaterOrEqual -> 'enc_AttributeValueAssertion'(element(2,Val), [<<165>>]); lessOrEqual -> 'enc_AttributeValueAssertion'(element(2,Val), [<<166>>]); present -> encode_restricted_string(element(2,Val), [<<135>>]); approxMatch -> 'enc_AttributeValueAssertion'(element(2,Val), [<<168>>]); extensibleMatch -> 'enc_MatchingRuleAssertion'(element(2,Val), [<<169>>]); Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, encode_tags(TagIn, EncBytes, EncLen). %%================================ %% Filter_and %%================================ 'enc_Filter_and'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_Filter_and_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_Filter_and_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Filter_and_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_Filter'(H, []), 'enc_Filter_and_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Filter_and'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Filter'(V1, []) || V1 <- Tlv1]. %%================================ %% Filter_or %%================================ 'enc_Filter_or'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_Filter_or_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_Filter_or_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_Filter_or_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_Filter'(H, []), 'enc_Filter_or_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_Filter_or'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_Filter'(V1, []) || V1 <- Tlv1]. 'dec_Filter'(Tlv) -> 'dec_Filter'(Tlv, []). 'dec_Filter'(Tlv, TagIn) -> Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'and' {131072, V1} -> {'and', 'dec_Filter_and'(V1, [])}; %% 'or' {131073, V1} -> {'or', 'dec_Filter_or'(V1, [])}; %% 'not' {131074, V1} -> {'not', 'dec_Filter'(V1, [])}; %% 'equalityMatch' {131075, V1} -> {equalityMatch, 'dec_AttributeValueAssertion'(V1, [])}; %% 'substrings' {131076, V1} -> {substrings, 'dec_SubstringFilter'(V1, [])}; %% 'greaterOrEqual' {131077, V1} -> {greaterOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; %% 'lessOrEqual' {131078, V1} -> {lessOrEqual, 'dec_AttributeValueAssertion'(V1, [])}; %% 'present' {131079, V1} -> {present, decode_restricted_string(V1,[])}; %% 'approxMatch' {131080, V1} -> {approxMatch, 'dec_AttributeValueAssertion'(V1, [])}; %% 'extensibleMatch' {131081, V1} -> {extensibleMatch, 'dec_MatchingRuleAssertion'(V1, [])}; Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . %%================================ %% SubstringFilter %%================================ 'enc_SubstringFilter'(Val) -> 'enc_SubstringFilter'(Val, [<<48>>]). 'enc_SubstringFilter'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute substrings(2) with type SEQUENCE OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_SubstringFilter_substrings'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% SubstringFilter_substrings %%================================ 'enc_SubstringFilter_substrings'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_SubstringFilter_substrings_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_SubstringFilter_substrings_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_SubstringFilter_substrings_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_SubstringFilter_substrings_SEQOF'(H, []), 'enc_SubstringFilter_substrings_components'(T,[EncBytes|AccBytes], AccLen + EncLen). %%================================ %% SubstringFilter_substrings_SEQOF %%================================ 'enc_SubstringFilter_substrings_SEQOF'(Val, TagIn) -> {EncBytes,EncLen} = case element(1,Val) of initial -> encode_restricted_string(element(2,Val), [<<128>>]); any -> encode_restricted_string(element(2,Val), [<<129>>]); final -> encode_restricted_string(element(2,Val), [<<130>>]); Else -> exit({error,{asn1,{invalid_choice_type,Else}}}) end, encode_tags(TagIn, EncBytes, EncLen). 'dec_SubstringFilter_substrings_SEQOF'(Tlv, TagIn) -> Tlv1 = match_tags(Tlv, TagIn), case (case Tlv1 of [CtempTlv1] -> CtempTlv1; _ -> Tlv1 end) of %% 'initial' {131072, V1} -> {initial, decode_restricted_string(V1,[])}; %% 'any' {131073, V1} -> {any, decode_restricted_string(V1,[])}; %% 'final' {131074, V1} -> {final, decode_restricted_string(V1,[])}; Else -> exit({error,{asn1,{invalid_choice_tag,Else}}}) end . 'dec_SubstringFilter_substrings'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_SubstringFilter_substrings_SEQOF'(V1, []) || V1 <- Tlv1]. 'dec_SubstringFilter'(Tlv) -> 'dec_SubstringFilter'(Tlv, [16]). 'dec_SubstringFilter'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute substrings(2) with type SEQUENCE OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_SubstringFilter_substrings'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'SubstringFilter', Term1, Term2}. %%================================ %% MatchingRuleAssertion %%================================ 'enc_MatchingRuleAssertion'(Val) -> 'enc_MatchingRuleAssertion'(Val, [<<48>>]). 'enc_MatchingRuleAssertion'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, %%------------------------------------------------- %% attribute matchingRule(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex1, [<<129>>]) end, %%------------------------------------------------- %% attribute type(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex2, [<<130>>]) end, %%------------------------------------------------- %% attribute matchValue(3) with type OCTET STRING %%------------------------------------------------- {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<131>>]), %%------------------------------------------------- %% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false %%------------------------------------------------- {EncBytes4,EncLen4} = case Cindex4 of asn1_DEFAULT -> {<<>>,0}; false -> {<<>>,0}; _ -> encode_boolean(Cindex4, [<<132>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_MatchingRuleAssertion'(Tlv) -> 'dec_MatchingRuleAssertion'(Tlv, [16]). 'dec_MatchingRuleAssertion'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute matchingRule(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term1,Tlv2} = case Tlv1 of [{131073,V1}|TempTlv2] -> {decode_restricted_string(V1,[]), TempTlv2}; _ -> { asn1_NOVALUE, Tlv1} end, %%------------------------------------------------- %% attribute type(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term2,Tlv3} = case Tlv2 of [{131074,V2}|TempTlv3] -> {decode_restricted_string(V2,[]), TempTlv3}; _ -> { asn1_NOVALUE, Tlv2} end, %%------------------------------------------------- %% attribute matchValue(3) with type OCTET STRING %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[131075]), %%------------------------------------------------- %% attribute dnAttributes(4) with type BOOLEAN DEFAULT = false %%------------------------------------------------- {Term4,Tlv5} = case Tlv4 of [{131076,V4}|TempTlv5] -> {decode_boolean(V4,[]), TempTlv5}; _ -> {false,Tlv4} end, case Tlv5 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed end, {'MatchingRuleAssertion', Term1, Term2, Term3, Term4}. %%================================ %% SearchResultEntry %%================================ 'enc_SearchResultEntry'(Val) -> 'enc_SearchResultEntry'(Val, [<<100>>]). 'enc_SearchResultEntry'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute objectName(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:PartialAttributeList %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_PartialAttributeList'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_SearchResultEntry'(Tlv) -> 'dec_SearchResultEntry'(Tlv, [65540]). 'dec_SearchResultEntry'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute objectName(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:PartialAttributeList %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_PartialAttributeList'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'SearchResultEntry', Term1, Term2}. %%================================ %% PartialAttributeList %%================================ 'enc_PartialAttributeList'(Val) -> 'enc_PartialAttributeList'(Val, [<<48>>]). 'enc_PartialAttributeList'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_PartialAttributeList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_PartialAttributeList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_PartialAttributeList_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_PartialAttributeList_SEQOF'(H, [<<48>>]), 'enc_PartialAttributeList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). %%================================ %% PartialAttributeList_SEQOF %%================================ 'enc_PartialAttributeList_SEQOF'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_PartialAttributeList_SEQOF_vals'(Cindex2, [<<49>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% PartialAttributeList_SEQOF_vals %%================================ 'enc_PartialAttributeList_SEQOF_vals'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_PartialAttributeList_SEQOF_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_PartialAttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_PartialAttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_PartialAttributeList_SEQOF_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_PartialAttributeList_SEQOF_vals'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. 'dec_PartialAttributeList_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_PartialAttributeList_SEQOF_vals'(V2, [17]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'PartialAttributeList_SEQOF', Term1, Term2}. 'dec_PartialAttributeList'(Tlv) -> 'dec_PartialAttributeList'(Tlv, [16]). 'dec_PartialAttributeList'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_PartialAttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. %%================================ %% SearchResultReference %%================================ 'enc_SearchResultReference'(Val) -> 'enc_SearchResultReference'(Val, [<<115>>]). 'enc_SearchResultReference'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_SearchResultReference_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_SearchResultReference_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_SearchResultReference_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_SearchResultReference_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_SearchResultReference'(Tlv) -> 'dec_SearchResultReference'(Tlv, [65555]). 'dec_SearchResultReference'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. %%================================ %% SearchResultDone %%================================ 'enc_SearchResultDone'(Val) -> 'enc_SearchResultDone'(Val, [<<101>>]). 'enc_SearchResultDone'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_SearchResultDone'(Tlv) -> 'dec_SearchResultDone'(Tlv, [65541]). 'dec_SearchResultDone'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% ModifyRequest %%================================ 'enc_ModifyRequest'(Val) -> 'enc_ModifyRequest'(Val, [<<102>>]). 'enc_ModifyRequest'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute object(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute modification(2) with type SEQUENCE OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_ModifyRequest_modification'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% ModifyRequest_modification %%================================ 'enc_ModifyRequest_modification'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_ModifyRequest_modification_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_ModifyRequest_modification_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_ModifyRequest_modification_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_ModifyRequest_modification_SEQOF'(H, [<<48>>]), 'enc_ModifyRequest_modification_components'(T,[EncBytes|AccBytes], AccLen + EncLen). %%================================ %% ModifyRequest_modification_SEQOF %%================================ 'enc_ModifyRequest_modification_SEQOF'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute operation(1) with type ENUMERATED %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of add -> encode_enumerated(0, [<<10>>]); delete -> encode_enumerated(1, [<<10>>]); replace -> encode_enumerated(2, [<<10>>]); Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) end, %%------------------------------------------------- %% attribute modification(2) External ELDAPv3:AttributeTypeAndValues %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_AttributeTypeAndValues'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyRequest_modification_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute operation(1) with type ENUMERATED %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{add,0},{delete,1},{replace,2}],[10]), %%------------------------------------------------- %% attribute modification(2) External ELDAPv3:AttributeTypeAndValues %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeTypeAndValues'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'ModifyRequest_modification_SEQOF', Term1, Term2}. 'dec_ModifyRequest_modification'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_ModifyRequest_modification_SEQOF'(V1, [16]) || V1 <- Tlv1]. 'dec_ModifyRequest'(Tlv) -> 'dec_ModifyRequest'(Tlv, [65542]). 'dec_ModifyRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute object(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute modification(2) with type SEQUENCE OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_ModifyRequest_modification'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'ModifyRequest', Term1, Term2}. %%================================ %% AttributeTypeAndValues %%================================ 'enc_AttributeTypeAndValues'(Val) -> 'enc_AttributeTypeAndValues'(Val, [<<48>>]). 'enc_AttributeTypeAndValues'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_AttributeTypeAndValues_vals'(Cindex2, [<<49>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% AttributeTypeAndValues_vals %%================================ 'enc_AttributeTypeAndValues_vals'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_AttributeTypeAndValues_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_AttributeTypeAndValues_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeTypeAndValues_vals_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_AttributeTypeAndValues_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_AttributeTypeAndValues_vals'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. 'dec_AttributeTypeAndValues'(Tlv) -> 'dec_AttributeTypeAndValues'(Tlv, [16]). 'dec_AttributeTypeAndValues'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeTypeAndValues_vals'(V2, [17]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'AttributeTypeAndValues', Term1, Term2}. %%================================ %% ModifyResponse %%================================ 'enc_ModifyResponse'(Val) -> 'enc_ModifyResponse'(Val, [<<103>>]). 'enc_ModifyResponse'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_ModifyResponse'(Tlv) -> 'dec_ModifyResponse'(Tlv, [65543]). 'dec_ModifyResponse'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% AddRequest %%================================ 'enc_AddRequest'(Val) -> 'enc_AddRequest'(Val, [<<104>>]). 'enc_AddRequest'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:AttributeList %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_AttributeList'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_AddRequest'(Tlv) -> 'dec_AddRequest'(Tlv, [65544]). 'dec_AddRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute attributes(2) External ELDAPv3:AttributeList %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeList'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'AddRequest', Term1, Term2}. %%================================ %% AttributeList %%================================ 'enc_AttributeList'(Val) -> 'enc_AttributeList'(Val, [<<48>>]). 'enc_AttributeList'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_AttributeList_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_AttributeList_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeList_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = 'enc_AttributeList_SEQOF'(H, [<<48>>]), 'enc_AttributeList_components'(T,[EncBytes|AccBytes], AccLen + EncLen). %%================================ %% AttributeList_SEQOF %%================================ 'enc_AttributeList_SEQOF'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_AttributeList_SEQOF_vals'(Cindex2, [<<49>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). %%================================ %% AttributeList_SEQOF_vals %%================================ 'enc_AttributeList_SEQOF_vals'(Val, TagIn) -> {EncBytes,EncLen} = 'enc_AttributeList_SEQOF_vals_components'(Val,[],0), encode_tags(TagIn, EncBytes, EncLen). 'enc_AttributeList_SEQOF_vals_components'([], AccBytes, AccLen) -> {lists:reverse(AccBytes),AccLen}; 'enc_AttributeList_SEQOF_vals_components'([H|T],AccBytes, AccLen) -> {EncBytes,EncLen} = encode_restricted_string(H, [<<4>>]), 'enc_AttributeList_SEQOF_vals_components'(T,[EncBytes|AccBytes], AccLen + EncLen). 'dec_AttributeList_SEQOF_vals'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), [decode_restricted_string(V1,[4]) || V1 <- Tlv1]. 'dec_AttributeList_SEQOF'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute type(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute vals(2) with type SET OF %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeList_SEQOF_vals'(V2, [17]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'AttributeList_SEQOF', Term1, Term2}. 'dec_AttributeList'(Tlv) -> 'dec_AttributeList'(Tlv, [16]). 'dec_AttributeList'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), ['dec_AttributeList_SEQOF'(V1, [16]) || V1 <- Tlv1]. %%================================ %% AddResponse %%================================ 'enc_AddResponse'(Val) -> 'enc_AddResponse'(Val, [<<105>>]). 'enc_AddResponse'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_AddResponse'(Tlv) -> 'dec_AddResponse'(Tlv, [65545]). 'dec_AddResponse'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% DelRequest %%================================ 'enc_DelRequest'(Val) -> 'enc_DelRequest'(Val, [<<74>>]). 'enc_DelRequest'(Val, TagIn) -> encode_restricted_string(Val, TagIn). 'dec_DelRequest'(Tlv) -> 'dec_DelRequest'(Tlv, [65546]). 'dec_DelRequest'(Tlv, TagIn) -> decode_restricted_string(Tlv,TagIn). %%================================ %% DelResponse %%================================ 'enc_DelResponse'(Val) -> 'enc_DelResponse'(Val, [<<107>>]). 'enc_DelResponse'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_DelResponse'(Tlv) -> 'dec_DelResponse'(Tlv, [65547]). 'dec_DelResponse'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% ModifyDNRequest %%================================ 'enc_ModifyDNRequest'(Val) -> 'enc_ModifyDNRequest'(Val, [<<108>>]). 'enc_ModifyDNRequest'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4} = Val, %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute newrdn(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), %%------------------------------------------------- %% attribute deleteoldrdn(3) with type BOOLEAN %%------------------------------------------------- {EncBytes3,EncLen3} = encode_boolean(Cindex3, [<<1>>]), %%------------------------------------------------- %% attribute newSuperior(4) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes4,EncLen4} = case Cindex4 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex4, [<<128>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ModifyDNRequest'(Tlv) -> 'dec_ModifyDNRequest'(Tlv, [65548]). 'dec_ModifyDNRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute newrdn(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute deleteoldrdn(3) with type BOOLEAN %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_boolean(V3,[1]), %%------------------------------------------------- %% attribute newSuperior(4) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term4,Tlv5} = case Tlv4 of [{131072,V4}|TempTlv5] -> {decode_restricted_string(V4,[]), TempTlv5}; _ -> { asn1_NOVALUE, Tlv4} end, case Tlv5 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv5}}}) % extra fields not allowed end, {'ModifyDNRequest', Term1, Term2, Term3, Term4}. %%================================ %% ModifyDNResponse %%================================ 'enc_ModifyDNResponse'(Val) -> 'enc_ModifyDNResponse'(Val, [<<109>>]). 'enc_ModifyDNResponse'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_ModifyDNResponse'(Tlv) -> 'dec_ModifyDNResponse'(Tlv, [65549]). 'dec_ModifyDNResponse'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% CompareRequest %%================================ 'enc_CompareRequest'(Val) -> 'enc_CompareRequest'(Val, [<<110>>]). 'enc_CompareRequest'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<4>>]), %%------------------------------------------------- %% attribute ava(2) External ELDAPv3:AttributeValueAssertion %%------------------------------------------------- {EncBytes2,EncLen2} = 'enc_AttributeValueAssertion'(Cindex2, [<<48>>]), BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_CompareRequest'(Tlv) -> 'dec_CompareRequest'(Tlv, [65550]). 'dec_CompareRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute entry(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[4]), %%------------------------------------------------- %% attribute ava(2) External ELDAPv3:AttributeValueAssertion %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = 'dec_AttributeValueAssertion'(V2, [16]), case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'CompareRequest', Term1, Term2}. %%================================ %% CompareResponse %%================================ 'enc_CompareResponse'(Val) -> 'enc_CompareResponse'(Val, [<<111>>]). 'enc_CompareResponse'(Val, TagIn) -> 'enc_LDAPResult'(Val, TagIn). 'dec_CompareResponse'(Tlv) -> 'dec_CompareResponse'(Tlv, [65551]). 'dec_CompareResponse'(Tlv, TagIn) -> 'dec_LDAPResult'(Tlv, TagIn). %%================================ %% AbandonRequest %%================================ 'enc_AbandonRequest'(Val) -> 'enc_AbandonRequest'(Val, [<<80>>]). 'enc_AbandonRequest'(Val, TagIn) -> encode_integer(Val, TagIn). 'dec_AbandonRequest'(Tlv) -> 'dec_AbandonRequest'(Tlv, [65552]). 'dec_AbandonRequest'(Tlv, TagIn) -> decode_integer(Tlv,{0,2147483647},TagIn). %%================================ %% ExtendedRequest %%================================ 'enc_ExtendedRequest'(Val) -> 'enc_ExtendedRequest'(Val, [<<119>>]). 'enc_ExtendedRequest'(Val, TagIn) -> {_,Cindex1, Cindex2} = Val, %%------------------------------------------------- %% attribute requestName(1) with type OCTET STRING %%------------------------------------------------- {EncBytes1,EncLen1} = encode_restricted_string(Cindex1, [<<128>>]), %%------------------------------------------------- %% attribute requestValue(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex2, [<<129>>]) end, BytesSoFar = [EncBytes1, EncBytes2], LenSoFar = EncLen1 + EncLen2, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedRequest'(Tlv) -> 'dec_ExtendedRequest'(Tlv, [65559]). 'dec_ExtendedRequest'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute requestName(1) with type OCTET STRING %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_restricted_string(V1,[131072]), %%------------------------------------------------- %% attribute requestValue(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term2,Tlv3} = case Tlv2 of [{131073,V2}|TempTlv3] -> {decode_restricted_string(V2,[]), TempTlv3}; _ -> { asn1_NOVALUE, Tlv2} end, case Tlv3 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv3}}}) % extra fields not allowed end, {'ExtendedRequest', Term1, Term2}. %%================================ %% ExtendedResponse %%================================ 'enc_ExtendedResponse'(Val) -> 'enc_ExtendedResponse'(Val, [<<120>>]). 'enc_ExtendedResponse'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3, Cindex4, Cindex5, Cindex6} = Val, %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of success -> encode_enumerated(0, [<<10>>]); operationsError -> encode_enumerated(1, [<<10>>]); protocolError -> encode_enumerated(2, [<<10>>]); timeLimitExceeded -> encode_enumerated(3, [<<10>>]); sizeLimitExceeded -> encode_enumerated(4, [<<10>>]); compareFalse -> encode_enumerated(5, [<<10>>]); compareTrue -> encode_enumerated(6, [<<10>>]); authMethodNotSupported -> encode_enumerated(7, [<<10>>]); strongAuthRequired -> encode_enumerated(8, [<<10>>]); referral -> encode_enumerated(10, [<<10>>]); adminLimitExceeded -> encode_enumerated(11, [<<10>>]); unavailableCriticalExtension -> encode_enumerated(12, [<<10>>]); confidentialityRequired -> encode_enumerated(13, [<<10>>]); saslBindInProgress -> encode_enumerated(14, [<<10>>]); noSuchAttribute -> encode_enumerated(16, [<<10>>]); undefinedAttributeType -> encode_enumerated(17, [<<10>>]); inappropriateMatching -> encode_enumerated(18, [<<10>>]); constraintViolation -> encode_enumerated(19, [<<10>>]); attributeOrValueExists -> encode_enumerated(20, [<<10>>]); invalidAttributeSyntax -> encode_enumerated(21, [<<10>>]); noSuchObject -> encode_enumerated(32, [<<10>>]); aliasProblem -> encode_enumerated(33, [<<10>>]); invalidDNSyntax -> encode_enumerated(34, [<<10>>]); aliasDereferencingProblem -> encode_enumerated(36, [<<10>>]); inappropriateAuthentication -> encode_enumerated(48, [<<10>>]); invalidCredentials -> encode_enumerated(49, [<<10>>]); insufficientAccessRights -> encode_enumerated(50, [<<10>>]); busy -> encode_enumerated(51, [<<10>>]); unavailable -> encode_enumerated(52, [<<10>>]); unwillingToPerform -> encode_enumerated(53, [<<10>>]); loopDetect -> encode_enumerated(54, [<<10>>]); namingViolation -> encode_enumerated(64, [<<10>>]); objectClassViolation -> encode_enumerated(65, [<<10>>]); notAllowedOnNonLeaf -> encode_enumerated(66, [<<10>>]); notAllowedOnRDN -> encode_enumerated(67, [<<10>>]); entryAlreadyExists -> encode_enumerated(68, [<<10>>]); objectClassModsProhibited -> encode_enumerated(69, [<<10>>]); affectsMultipleDSAs -> encode_enumerated(71, [<<10>>]); other -> encode_enumerated(80, [<<10>>]); Enumval1 -> exit({error,{asn1, {enumerated_not_in_range,Enumval1}}}) end, %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- {EncBytes2,EncLen2} = encode_restricted_string(Cindex2, [<<4>>]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- {EncBytes3,EncLen3} = encode_restricted_string(Cindex3, [<<4>>]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {EncBytes4,EncLen4} = case Cindex4 of asn1_NOVALUE -> {<<>>,0}; _ -> 'enc_Referral'(Cindex4, [<<163>>]) end, %%------------------------------------------------- %% attribute responseName(5) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes5,EncLen5} = case Cindex5 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex5, [<<138>>]) end, %%------------------------------------------------- %% attribute response(6) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes6,EncLen6} = case Cindex6 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex6, [<<139>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3, EncBytes4, EncBytes5, EncBytes6], LenSoFar = EncLen1 + EncLen2 + EncLen3 + EncLen4 + EncLen5 + EncLen6, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_ExtendedResponse'(Tlv) -> 'dec_ExtendedResponse'(Tlv, [65560]). 'dec_ExtendedResponse'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute resultCode(1) with type ENUMERATED %%------------------------------------------------- [V1|Tlv2] = Tlv1, Term1 = decode_enumerated(V1,[{success,0},{operationsError,1},{protocolError,2},{timeLimitExceeded,3},{sizeLimitExceeded,4},{compareFalse,5},{compareTrue,6},{authMethodNotSupported,7},{strongAuthRequired,8},{referral,10},{adminLimitExceeded,11},{unavailableCriticalExtension,12},{confidentialityRequired,13},{saslBindInProgress,14},{noSuchAttribute,16},{undefinedAttributeType,17},{inappropriateMatching,18},{constraintViolation,19},{attributeOrValueExists,20},{invalidAttributeSyntax,21},{noSuchObject,32},{aliasProblem,33},{invalidDNSyntax,34},{aliasDereferencingProblem,36},{inappropriateAuthentication,48},{invalidCredentials,49},{insufficientAccessRights,50},{busy,51},{unavailable,52},{unwillingToPerform,53},{loopDetect,54},{namingViolation,64},{objectClassViolation,65},{notAllowedOnNonLeaf,66},{notAllowedOnRDN,67},{entryAlreadyExists,68},{objectClassModsProhibited,69},{affectsMultipleDSAs,71},{other,80}],[10]), %%------------------------------------------------- %% attribute matchedDN(2) with type OCTET STRING %%------------------------------------------------- [V2|Tlv3] = Tlv2, Term2 = decode_restricted_string(V2,[4]), %%------------------------------------------------- %% attribute errorMessage(3) with type OCTET STRING %%------------------------------------------------- [V3|Tlv4] = Tlv3, Term3 = decode_restricted_string(V3,[4]), %%------------------------------------------------- %% attribute referral(4) External ELDAPv3:Referral OPTIONAL %%------------------------------------------------- {Term4,Tlv5} = case Tlv4 of [{131075,V4}|TempTlv5] -> {'dec_Referral'(V4, []), TempTlv5}; _ -> { asn1_NOVALUE, Tlv4} end, %%------------------------------------------------- %% attribute responseName(5) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term5,Tlv6} = case Tlv5 of [{131082,V5}|TempTlv6] -> {decode_restricted_string(V5,[]), TempTlv6}; _ -> { asn1_NOVALUE, Tlv5} end, %%------------------------------------------------- %% attribute response(6) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term6,Tlv7} = case Tlv6 of [{131083,V6}|TempTlv7] -> {decode_restricted_string(V6,[]), TempTlv7}; _ -> { asn1_NOVALUE, Tlv6} end, case Tlv7 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv7}}}) % extra fields not allowed end, {'ExtendedResponse', Term1, Term2, Term3, Term4, Term5, Term6}. %%================================ %% PasswdModifyRequestValue %%================================ 'enc_PasswdModifyRequestValue'(Val) -> 'enc_PasswdModifyRequestValue'(Val, [<<48>>]). 'enc_PasswdModifyRequestValue'(Val, TagIn) -> {_,Cindex1, Cindex2, Cindex3} = Val, %%------------------------------------------------- %% attribute userIdentity(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex1, [<<128>>]) end, %%------------------------------------------------- %% attribute oldPasswd(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes2,EncLen2} = case Cindex2 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex2, [<<129>>]) end, %%------------------------------------------------- %% attribute newPasswd(3) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes3,EncLen3} = case Cindex3 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex3, [<<130>>]) end, BytesSoFar = [EncBytes1, EncBytes2, EncBytes3], LenSoFar = EncLen1 + EncLen2 + EncLen3, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyRequestValue'(Tlv) -> 'dec_PasswdModifyRequestValue'(Tlv, [16]). 'dec_PasswdModifyRequestValue'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute userIdentity(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term1,Tlv2} = case Tlv1 of [{131072,V1}|TempTlv2] -> {decode_restricted_string(V1,[]), TempTlv2}; _ -> { asn1_NOVALUE, Tlv1} end, %%------------------------------------------------- %% attribute oldPasswd(2) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term2,Tlv3} = case Tlv2 of [{131073,V2}|TempTlv3] -> {decode_restricted_string(V2,[]), TempTlv3}; _ -> { asn1_NOVALUE, Tlv2} end, %%------------------------------------------------- %% attribute newPasswd(3) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term3,Tlv4} = case Tlv3 of [{131074,V3}|TempTlv4] -> {decode_restricted_string(V3,[]), TempTlv4}; _ -> { asn1_NOVALUE, Tlv3} end, case Tlv4 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv4}}}) % extra fields not allowed end, {'PasswdModifyRequestValue', Term1, Term2, Term3}. %%================================ %% PasswdModifyResponseValue %%================================ 'enc_PasswdModifyResponseValue'(Val) -> 'enc_PasswdModifyResponseValue'(Val, [<<48>>]). 'enc_PasswdModifyResponseValue'(Val, TagIn) -> {_,Cindex1} = Val, %%------------------------------------------------- %% attribute genPasswd(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {EncBytes1,EncLen1} = case Cindex1 of asn1_NOVALUE -> {<<>>,0}; _ -> encode_restricted_string(Cindex1, [<<128>>]) end, BytesSoFar = [EncBytes1], LenSoFar = EncLen1, encode_tags(TagIn, BytesSoFar, LenSoFar). 'dec_PasswdModifyResponseValue'(Tlv) -> 'dec_PasswdModifyResponseValue'(Tlv, [16]). 'dec_PasswdModifyResponseValue'(Tlv, TagIn) -> %%------------------------------------------------- %% decode tag and length %%------------------------------------------------- Tlv1 = match_tags(Tlv, TagIn), %%------------------------------------------------- %% attribute genPasswd(1) with type OCTET STRING OPTIONAL %%------------------------------------------------- {Term1,Tlv2} = case Tlv1 of [{131072,V1}|TempTlv2] -> {decode_restricted_string(V1,[]), TempTlv2}; _ -> { asn1_NOVALUE, Tlv1} end, case Tlv2 of [] -> true;_ -> exit({error,{asn1, {unexpected,Tlv2}}}) % extra fields not allowed end, {'PasswdModifyResponseValue', Term1}. 'maxInt'() -> 2147483647. 'passwdModifyOID'() -> [49,46,51,46,54,46,49,46,52,46,49,46,52,50,48,51,46,49,46,49,49,46,49]. %%% %%% Run-time functions. %%% ber_decode_nif(B) -> asn1rt_nif:decode_ber_tlv(B). collect_parts(TlvList) -> collect_parts(TlvList, []). collect_parts([{_,L}|Rest], Acc) when is_list(L) -> collect_parts(Rest, [collect_parts(L)|Acc]); collect_parts([{3,<>}|Rest], _Acc) -> collect_parts_bit(Rest, [Bits], Unused); collect_parts([{_T,V}|Rest], Acc) -> collect_parts(Rest, [V|Acc]); collect_parts([], Acc) -> list_to_binary(lists:reverse(Acc)). collect_parts_bit([{3,<>}|Rest], Acc, Uacc) -> collect_parts_bit(Rest, [Bits|Acc], Unused + Uacc); collect_parts_bit([], Acc, Uacc) -> list_to_binary([Uacc|lists:reverse(Acc)]). decode_boolean(Tlv, TagIn) -> Val = match_tags(Tlv, TagIn), case Val of <<0:8>> -> false; <<_:8>> -> true; _ -> exit({error,{asn1,{decode_boolean,Val}}}) end. decode_enumerated(Tlv, NamedNumberList, Tags) -> Buffer = match_tags(Tlv, Tags), decode_enumerated_notag(Buffer, NamedNumberList, Tags). decode_enumerated1(Val, NamedNumberList) -> case lists:keyfind(Val, 2, NamedNumberList) of {NamedVal,_} -> NamedVal; _ -> {asn1_enum,Val} end. decode_enumerated_notag(Buffer, {NamedNumberList,ExtList}, _Tags) -> IVal = decode_integer(Buffer), case decode_enumerated1(IVal, NamedNumberList) of {asn1_enum,IVal} -> decode_enumerated1(IVal, ExtList); EVal -> EVal end; decode_enumerated_notag(Buffer, NNList, _Tags) -> IVal = decode_integer(Buffer), case decode_enumerated1(IVal, NNList) of {asn1_enum,_} -> exit({error,{asn1,{illegal_enumerated,IVal}}}); EVal -> EVal end. decode_integer(Bin) -> Len = byte_size(Bin), <> = Bin, Int. decode_integer(Tlv, Range, TagIn) -> V = match_tags(Tlv, TagIn), Int = decode_integer(V), range_check_integer(Int, Range). decode_null(Tlv, Tags) -> Val = match_tags(Tlv, Tags), case Val of <<>> -> 'NULL'; _ -> exit({error,{asn1,{decode_null,Val}}}) end. decode_restricted_string(Tlv, TagsIn) -> Bin = match_and_collect(Tlv, TagsIn), Bin. encode_boolean(true, TagIn) -> encode_tags(TagIn, [255], 1); encode_boolean(false, TagIn) -> encode_tags(TagIn, [0], 1); encode_boolean(X, _) -> exit({error,{asn1,{encode_boolean,X}}}). encode_enumerated(Val, TagIn) when is_integer(Val) -> encode_tags(TagIn, encode_integer(Val)). encode_integer(Val) -> Bytes = if Val >= 0 -> encode_integer_pos(Val, []); true -> encode_integer_neg(Val, []) end, {Bytes,length(Bytes)}. encode_integer(Val, Tag) when is_integer(Val) -> encode_tags(Tag, encode_integer(Val)); encode_integer(Val, _Tag) -> exit({error,{asn1,{encode_integer,Val}}}). encode_integer_neg(- 1, [B1|_T] = L) when B1 > 127 -> L; encode_integer_neg(N, Acc) -> encode_integer_neg(N bsr 8, [N band 255|Acc]). encode_integer_pos(0, [B|_Acc] = L) when B < 128 -> L; encode_integer_pos(N, Acc) -> encode_integer_pos(N bsr 8, [N band 255|Acc]). encode_length(L) when L =< 127 -> {[L],1}; encode_length(L) -> Oct = minimum_octets(L), Len = length(Oct), if Len =< 126 -> {[128 bor Len|Oct],Len + 1}; true -> exit({error,{asn1,too_long_length_oct,Len}}) end. encode_null(_Val, TagIn) -> encode_tags(TagIn, [], 0). encode_restricted_string(OctetList, TagIn) when is_binary(OctetList) -> encode_tags(TagIn, OctetList, byte_size(OctetList)); encode_restricted_string(OctetList, TagIn) when is_list(OctetList) -> encode_tags(TagIn, OctetList, length(OctetList)). encode_tags(TagIn, {BytesSoFar,LenSoFar}) -> encode_tags(TagIn, BytesSoFar, LenSoFar). encode_tags([Tag|Trest], BytesSoFar, LenSoFar) -> {Bytes2,L2} = encode_length(LenSoFar), encode_tags(Trest, [Tag,Bytes2|BytesSoFar], LenSoFar + byte_size(Tag) + L2); encode_tags([], BytesSoFar, LenSoFar) -> {BytesSoFar,LenSoFar}. match_and_collect(Tlv, TagsIn) -> Val = match_tags(Tlv, TagsIn), case Val of [_|_] = PartList -> collect_parts(PartList); Bin when is_binary(Bin) -> Bin end. match_tags({T,V}, [T]) -> V; match_tags({T,V}, [T|Tt]) -> match_tags(V, Tt); match_tags([{T,V}], [T|Tt]) -> match_tags(V, Tt); match_tags([{T,_V}|_] = Vlist, [T]) -> Vlist; match_tags(Tlv, []) -> Tlv; match_tags({Tag,_V} = Tlv, [T|_Tt]) -> exit({error,{asn1,{wrong_tag,{{expected,T},{got,Tag,Tlv}}}}}). minimum_octets(0, Acc) -> Acc; minimum_octets(Val, Acc) -> minimum_octets(Val bsr 8, [Val band 255|Acc]). minimum_octets(Val) -> minimum_octets(Val, []). range_check_integer(Int, Range) -> case Range of [] -> Int; {Lb,Ub} when Int >= Lb, Ub >= Int -> Int; {_,_} -> exit({error,{asn1,{integer_range,Range,Int}}}); Int -> Int; SingleValue when is_integer(SingleValue) -> exit({error,{asn1,{integer_range,Range,Int}}}); _ -> Int end. ejabberd-21.12/src/mod_http_upload_quota.erl0000644000232200023220000003454714154362354021535 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_http_upload_quota.erl %%% Author : Holger Weiss %%% Purpose : Quota management for HTTP File Upload (XEP-0363) %%% Created : 15 Oct 2015 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2015-2021 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(mod_http_upload_quota). -author('holger@zedat.fu-berlin.de'). -define(TIMEOUT, timer:hours(24)). -define(FORMAT(Error), file:format_error(Error)). -behaviour(gen_server). -behaviour(gen_mod). %% gen_mod/supervisor callbacks. -export([start/2, stop/1, depends/2, mod_doc/0, mod_opt_type/1, mod_options/1]). %% gen_server callbacks. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% ejabberd_hooks callback. -export([handle_slot_request/6]). -include_lib("xmpp/include/jid.hrl"). -include("logger.hrl"). -include("translate.hrl"). -include_lib("kernel/include/file.hrl"). -record(state, {server_host :: binary(), access_soft_quota :: atom(), access_hard_quota :: atom(), max_days :: pos_integer() | infinity, docroot :: binary(), disk_usage = #{} :: disk_usage(), timer :: reference() | undefined}). -type disk_usage() :: #{{binary(), binary()} => non_neg_integer()}. -type state() :: #state{}. %%-------------------------------------------------------------------- %% gen_mod/supervisor callbacks. %%-------------------------------------------------------------------- start(ServerHost, Opts) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_mod:start_child(?MODULE, ServerHost, Opts, Proc). stop(ServerHost) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_mod:stop_child(Proc). -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access_soft_quota) -> econf:shaper(); mod_opt_type(access_hard_quota) -> econf:shaper(); mod_opt_type(max_days) -> econf:pos_int(infinity). -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(_) -> [{access_soft_quota, soft_upload_quota}, {access_hard_quota, hard_upload_quota}, {max_days, infinity}]. mod_doc() -> #{desc => [?T("This module adds quota support for mod_http_upload."), "", ?T("This module depends on 'mod_http_upload'.")], opts => [{max_days, #{value => ?T("Days"), desc => ?T("If a number larger than zero is specified, " "any files (and directories) older than this " "number of days are removed from the subdirectories " "of the 'docroot' directory, once per day. " "The default value is 'infinity'.")}}, {access_soft_quota, #{value => ?T("AccessName"), desc => ?T("This option defines which access rule is used " "to specify the \"soft quota\" for the matching JIDs. " "That rule must yield a positive number of megabytes " "for any JID that is supposed to have a quota limit. " "See the description of the 'access_hard_quota' option " "for details. The default value is 'soft_upload_quota'.")}}, {access_hard_quota, #{value => ?T("AccessName"), desc => ?T("This option defines which access rule is used to " "specify the \"hard quota\" for the matching JIDs. " "That rule must yield a positive number for any " "JID that is supposed to have a quota limit. " "This is the number of megabytes a corresponding " "user may upload. When this threshold is exceeded, " "ejabberd deletes the oldest files uploaded by that " "user until their disk usage equals or falls below " "the specified soft quota (see 'access_soft_quota'). " "The default value is 'hard_upload_quota'.")}}], example => [{?T("Please note that it's not necessary to specify the " "'access_hard_quota' and 'access_soft_quota' options in order " "to use the quota feature. You can stick to the default names " "and just specify access rules such as those in this example:"), ["shaper_rules:", " ...", " soft_upload_quota:", " 1000: all # MiB", " hard_upload_quota:", " 1100: all # MiB", " ...", "", "modules:", " ...", " mod_http_upload: {}", " mod_http_upload_quota:", " max_days: 100", " ..."]}]}. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> [{mod_http_upload, hard}]. %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(list()) -> {ok, state()}. init([ServerHost|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(ServerHost, ?MODULE), AccessSoftQuota = mod_http_upload_quota_opt:access_soft_quota(Opts), AccessHardQuota = mod_http_upload_quota_opt:access_hard_quota(Opts), MaxDays = mod_http_upload_quota_opt:max_days(Opts), DocRoot1 = mod_http_upload_opt:docroot(ServerHost), DocRoot2 = mod_http_upload:expand_home(str:strip(DocRoot1, right, $/)), DocRoot3 = mod_http_upload:expand_host(DocRoot2, ServerHost), Timer = if MaxDays == infinity -> undefined; true -> Timeout = p1_rand:uniform(?TIMEOUT div 2), erlang:send_after(Timeout, self(), sweep) end, ejabberd_hooks:add(http_upload_slot_request, ServerHost, ?MODULE, handle_slot_request, 50), {ok, #state{server_host = ServerHost, access_soft_quota = AccessSoftQuota, access_hard_quota = AccessHardQuota, max_days = MaxDays, docroot = DocRoot3, timer = Timer}}. -spec handle_call(_, {pid(), _}, state()) -> {noreply, state()}. handle_call(Request, From, State) -> ?ERROR_MSG("Unexpected request from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(_, state()) -> {noreply, state()}. handle_cast({handle_slot_request, #jid{user = U, server = S} = JID, Path, Size}, #state{server_host = ServerHost, access_soft_quota = AccessSoftQuota, access_hard_quota = AccessHardQuota, disk_usage = DiskUsage} = State) -> HardQuota = case ejabberd_shaper:match(ServerHost, AccessHardQuota, JID) of Hard when is_integer(Hard), Hard > 0 -> Hard * 1024 * 1024; _ -> 0 end, SoftQuota = case ejabberd_shaper:match(ServerHost, AccessSoftQuota, JID) of Soft when is_integer(Soft), Soft > 0 -> Soft * 1024 * 1024; _ -> 0 end, OldSize = case maps:find({U, S}, DiskUsage) of {ok, Value} -> Value; error -> undefined end, NewSize = case {HardQuota, SoftQuota} of {0, 0} -> ?DEBUG("No quota specified for ~ts", [jid:encode(JID)]), undefined; {0, _} -> ?WARNING_MSG("No hard quota specified for ~ts", [jid:encode(JID)]), enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); {_, 0} -> ?WARNING_MSG("No soft quota specified for ~ts", [jid:encode(JID)]), enforce_quota(Path, Size, OldSize, HardQuota, HardQuota); _ when SoftQuota > HardQuota -> ?WARNING_MSG("Bad quota for ~ts (soft: ~p, hard: ~p)", [jid:encode(JID), SoftQuota, HardQuota]), enforce_quota(Path, Size, OldSize, SoftQuota, SoftQuota); _ -> ?DEBUG("Enforcing quota for ~ts", [jid:encode(JID)]), enforce_quota(Path, Size, OldSize, SoftQuota, HardQuota) end, NewDiskUsage = if is_integer(NewSize) -> maps:put({U, S}, NewSize, DiskUsage); true -> DiskUsage end, {noreply, State#state{disk_usage = NewDiskUsage}}; handle_cast(Request, State) -> ?ERROR_MSG("Unexpected request: ~p", [Request]), {noreply, State}. -spec handle_info(_, state()) -> {noreply, state()}. handle_info(sweep, #state{server_host = ServerHost, docroot = DocRoot, max_days = MaxDays} = State) when is_integer(MaxDays), MaxDays > 0 -> ?DEBUG("Got 'sweep' message for ~ts", [ServerHost]), Timer = erlang:send_after(?TIMEOUT, self(), sweep), case file:list_dir(DocRoot) of {ok, Entries} -> BackThen = secs_since_epoch() - (MaxDays * 86400), DocRootS = binary_to_list(DocRoot), PathNames = lists:map(fun(Entry) -> DocRootS ++ "/" ++ Entry end, Entries), UserDirs = lists:filter(fun filelib:is_dir/1, PathNames), lists:foreach(fun(UserDir) -> delete_old_files(UserDir, BackThen) end, UserDirs); {error, Error} -> ?ERROR_MSG("Cannot open document root ~ts: ~ts", [DocRoot, ?FORMAT(Error)]) end, {noreply, State#state{timer = Timer}}; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(normal | shutdown | {shutdown, _} | _, state()) -> ok. terminate(Reason, #state{server_host = ServerHost, timer = Timer}) -> ?DEBUG("Stopping upload quota process for ~ts: ~p", [ServerHost, Reason]), ejabberd_hooks:delete(http_upload_slot_request, ServerHost, ?MODULE, handle_slot_request, 50), misc:cancel_timer(Timer). -spec code_change({down, _} | _, state(), _) -> {ok, state()}. code_change(_OldVsn, #state{server_host = ServerHost} = State, _Extra) -> ?DEBUG("Updating upload quota process for ~ts", [ServerHost]), {ok, State}. %%-------------------------------------------------------------------- %% ejabberd_hooks callback. %%-------------------------------------------------------------------- -spec handle_slot_request(allow | deny, binary(), jid(), binary(), non_neg_integer(), binary()) -> allow | deny. handle_slot_request(allow, ServerHost, JID, Path, Size, _Lang) -> Proc = mod_http_upload:get_proc_name(ServerHost, ?MODULE), gen_server:cast(Proc, {handle_slot_request, JID, Path, Size}), allow; handle_slot_request(Acc, _ServerHost, _JID, _Path, _Size, _Lang) -> Acc. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec enforce_quota(file:filename_all(), non_neg_integer(), non_neg_integer() | undefined, non_neg_integer(), non_neg_integer()) -> non_neg_integer(). enforce_quota(_UserDir, SlotSize, OldSize, _MinSize, MaxSize) when is_integer(OldSize), OldSize + SlotSize =< MaxSize -> OldSize + SlotSize; enforce_quota(UserDir, SlotSize, _OldSize, MinSize, MaxSize) -> Files = lists:sort(fun({_PathA, _SizeA, TimeA}, {_PathB, _SizeB, TimeB}) -> TimeA > TimeB end, gather_file_info(UserDir)), {DelFiles, OldSize, NewSize} = lists:foldl(fun({_Path, Size, _Time}, {[], AccSize, AccSize}) when AccSize + Size + SlotSize =< MinSize -> {[], AccSize + Size, AccSize + Size}; ({Path, Size, _Time}, {[], AccSize, AccSize}) -> {[Path], AccSize + Size, AccSize}; ({Path, Size, _Time}, {AccFiles, AccSize, NewSize}) -> {[Path | AccFiles], AccSize + Size, NewSize} end, {[], 0, 0}, Files), if OldSize + SlotSize > MaxSize -> lists:foreach(fun del_file_and_dir/1, DelFiles), file:del_dir(UserDir), % In case it's empty, now. NewSize + SlotSize; true -> OldSize + SlotSize end. -spec delete_old_files(file:filename_all(), integer()) -> ok. delete_old_files(UserDir, CutOff) -> FileInfo = gather_file_info(UserDir), case [Path || {Path, _Size, Time} <- FileInfo, Time < CutOff] of [] -> ok; OldFiles -> lists:foreach(fun del_file_and_dir/1, OldFiles), file:del_dir(UserDir) % In case it's empty, now. end. -spec gather_file_info(file:filename_all()) -> [{binary(), non_neg_integer(), non_neg_integer()}]. gather_file_info(Dir) when is_binary(Dir) -> gather_file_info(binary_to_list(Dir)); gather_file_info(Dir) -> case file:list_dir(Dir) of {ok, Entries} -> lists:foldl(fun(Entry, Acc) -> Path = Dir ++ "/" ++ Entry, case file:read_file_info(Path, [{time, posix}]) of {ok, #file_info{type = directory}} -> gather_file_info(Path) ++ Acc; {ok, #file_info{type = regular, mtime = Time, size = Size}} -> [{Path, Size, Time} | Acc]; {ok, _Info} -> ?DEBUG("Won't stat(2) non-regular file ~ts", [Path]), Acc; {error, Error} -> ?ERROR_MSG("Cannot stat(2) ~ts: ~ts", [Path, ?FORMAT(Error)]), Acc end end, [], Entries); {error, enoent} -> ?DEBUG("Directory ~ts doesn't exist", [Dir]), []; {error, Error} -> ?ERROR_MSG("Cannot open directory ~ts: ~ts", [Dir, ?FORMAT(Error)]), [] end. -spec del_file_and_dir(file:name_all()) -> ok. del_file_and_dir(File) -> case file:delete(File) of ok -> ?INFO_MSG("Removed ~ts", [File]), Dir = filename:dirname(File), case file:del_dir(Dir) of ok -> ?DEBUG("Removed ~ts", [Dir]); {error, Error} -> ?DEBUG("Cannot remove ~ts: ~ts", [Dir, ?FORMAT(Error)]) end; {error, Error} -> ?WARNING_MSG("Cannot remove ~ts: ~ts", [File, ?FORMAT(Error)]) end. -spec secs_since_epoch() -> non_neg_integer(). secs_since_epoch() -> {MegaSecs, Secs, _MicroSecs} = os:timestamp(), MegaSecs * 1000000 + Secs. ejabberd-21.12/src/mod_privacy_sql.erl0000644000232200023220000003541414154362354020327 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_privacy_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_privacy_sql). -behaviour(mod_privacy). %% API -export([init/2, set_default/3, unset_default/2, set_lists/1, set_list/4, get_lists/2, get_list/3, remove_lists/2, remove_list/3, import/1, export/1]). -export([item_to_raw/1, raw_to_item/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. unset_default(LUser, LServer) -> case unset_default_privacy_list(LUser, LServer) of ok -> ok; _Err -> {error, db_failure} end. set_default(LUser, LServer, Name) -> F = fun () -> case get_privacy_list_names_t(LUser, LServer) of {selected, []} -> {error, notfound}; {selected, Names} -> case lists:member({Name}, Names) of true -> set_default_privacy_list(LUser, LServer, Name); false -> {error, notfound} end end end, transaction(LServer, F). remove_list(LUser, LServer, Name) -> F = fun () -> case get_default_privacy_list_t(LUser, LServer) of {selected, []} -> remove_privacy_list_t(LUser, LServer, Name); {selected, [{Default}]} -> if Name == Default -> {error, conflict}; true -> remove_privacy_list_t(LUser, LServer, Name) end end end, transaction(LServer, F). set_lists(#privacy{us = {LUser, LServer}, default = Default, lists = Lists}) -> F = fun() -> lists:foreach( fun({Name, List}) -> add_privacy_list(LUser, LServer, Name), {selected, [{I}]} = get_privacy_list_id_t(LUser, LServer, Name), RItems = lists:map(fun item_to_raw/1, List), set_privacy_list(I, RItems), if is_binary(Default) -> set_default_privacy_list( LUser, LServer, Default); true -> ok end end, Lists) end, transaction(LServer, F). set_list(LUser, LServer, Name, List) -> RItems = lists:map(fun item_to_raw/1, List), F = fun () -> ID = case get_privacy_list_id_t(LUser, LServer, Name) of {selected, []} -> add_privacy_list(LUser, LServer, Name), {selected, [{I}]} = get_privacy_list_id_t(LUser, LServer, Name), I; {selected, [{I}]} -> I end, set_privacy_list(ID, RItems) end, transaction(LServer, F). get_list(LUser, LServer, default) -> case get_default_privacy_list(LUser, LServer) of {selected, []} -> error; {selected, [{Default}]} -> get_list(LUser, LServer, Default); _Err -> {error, db_failure} end; get_list(LUser, LServer, Name) -> case get_privacy_list_data(LUser, LServer, Name) of {selected, []} -> error; {selected, RItems} -> {ok, {Name, lists:flatmap(fun raw_to_item/1, RItems)}}; _Err -> {error, db_failure} end. get_lists(LUser, LServer) -> case get_default_privacy_list(LUser, LServer) of {selected, Selected} -> Default = case Selected of [] -> none; [{DefName}] -> DefName end, case get_privacy_list_names(LUser, LServer) of {selected, Names} -> case lists:foldl( fun(_, {error, _} = Err) -> Err; ({Name}, Acc) -> case get_privacy_list_data(LUser, LServer, Name) of {selected, RItems} -> Items = lists:flatmap( fun raw_to_item/1, RItems), [{Name, Items}|Acc]; _Err -> {error, db_failure} end end, [], Names) of {error, Reason} -> {error, Reason}; Lists -> {ok, #privacy{default = Default, us = {LUser, LServer}, lists = Lists}} end; _Err -> {error, db_failure} end; _Err -> {error, db_failure} end. remove_lists(LUser, LServer) -> case del_privacy_lists(LUser, LServer) of ok -> ok; _Err -> {error, db_failure} end. export(Server) -> SqlType = ejabberd_option:sql_type(Server), case catch ejabberd_sql:sql_query(jid:nameprep(Server), [<<"select id from privacy_list order by " "id desc limit 1;">>]) of {selected, [<<"id">>], [[I]]} -> put(id, binary_to_integer(I)); _ -> put(id, 0) end, [{privacy, fun(Host, #privacy{us = {LUser, LServer}, lists = Lists, default = Default}) when LServer == Host -> if Default /= none -> [?SQL("delete from privacy_default_list where" " username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "privacy_default_list", ["username=%(LUser)s", "server_host=%(LServer)s", "name=%(Default)s"])]; true -> [] end ++ lists:flatmap( fun({Name, List}) -> RItems = lists:map(fun item_to_raw/1, List), ID = get_id(), [?SQL("delete from privacy_list where" " username=%(LUser)s and %(LServer)H and" " name=%(Name)s;"), ?SQL_INSERT( "privacy_list", ["username=%(LUser)s", "server_host=%(LServer)s", "name=%(Name)s", "id=%(ID)d"]), ?SQL("delete from privacy_list_data where" " id=%(ID)d;")] ++ case SqlType of pgsql -> [?SQL("insert into privacy_list_data(id, t, " "value, action, ord, match_all, match_iq, " "match_message, match_presence_in, " "match_presence_out) " "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," " %(Order)d, CAST(%(MatchAll)b as boolean), CAST(%(MatchIQ)b as boolean)," " CAST(%(MatchMessage)b as boolean), CAST(%(MatchPresenceIn)b as boolean)," " CAST(%(MatchPresenceOut)b as boolean));") || {SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut} <- RItems]; _ -> [?SQL("insert into privacy_list_data(id, t, " "value, action, ord, match_all, match_iq, " "match_message, match_presence_in, " "match_presence_out) " "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," " %(Order)d, %(MatchAll)b, %(MatchIQ)b," " %(MatchMessage)b, %(MatchPresenceIn)b," " %(MatchPresenceOut)b);") || {SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut} <- RItems] end end, Lists); (_Host, _R) -> [] end}]. get_id() -> ID = get(id), put(id, ID + 1), ID + 1. import(_) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(LServer, F) -> case ejabberd_sql:sql_transaction(LServer, F) of {atomic, Res} -> Res; {aborted, _Reason} -> {error, db_failure} end. raw_to_item({SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut} = Row) -> try {Type, Value} = case SType of <<"n">> -> {none, none}; <<"j">> -> JID = jid:decode(SValue), {jid, jid:tolower(JID)}; <<"g">> -> {group, SValue}; <<"s">> -> case SValue of <<"none">> -> {subscription, none}; <<"both">> -> {subscription, both}; <<"from">> -> {subscription, from}; <<"to">> -> {subscription, to} end end, Action = case SAction of <<"a">> -> allow; <<"d">> -> deny end, [#listitem{type = Type, value = Value, action = Action, order = Order, match_all = MatchAll, match_iq = MatchIQ, match_message = MatchMessage, match_presence_in = MatchPresenceIn, match_presence_out = MatchPresenceOut}] catch _:_ -> ?WARNING_MSG("Failed to parse row: ~p", [Row]), [] end. item_to_raw(#listitem{type = Type, value = Value, action = Action, order = Order, match_all = MatchAll, match_iq = MatchIQ, match_message = MatchMessage, match_presence_in = MatchPresenceIn, match_presence_out = MatchPresenceOut}) -> {SType, SValue} = case Type of none -> {<<"n">>, <<"">>}; jid -> {<<"j">>, jid:encode(Value)}; group -> {<<"g">>, Value}; subscription -> case Value of none -> {<<"s">>, <<"none">>}; both -> {<<"s">>, <<"both">>}; from -> {<<"s">>, <<"from">>}; to -> {<<"s">>, <<"to">>} end end, SAction = case Action of allow -> <<"a">>; deny -> <<"d">> end, {SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut}. get_default_privacy_list(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_default_list " "where username=%(LUser)s and %(LServer)H")). get_default_privacy_list_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_default_list " "where username=%(LUser)s and %(LServer)H")). get_privacy_list_names(LUser, LServer) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s from privacy_list" " where username=%(LUser)s and %(LServer)H")). get_privacy_list_names_t(LUser, LServer) -> ejabberd_sql:sql_query_t( ?SQL("select @(name)s from privacy_list" " where username=%(LUser)s and %(LServer)H")). get_privacy_list_id_t(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( ?SQL("select @(id)d from privacy_list" " where username=%(LUser)s and %(LServer)H and name=%(Name)s")). get_privacy_list_data(LUser, LServer, Name) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(t)s, @(value)s, @(action)s, @(ord)d, @(match_all)b, " "@(match_iq)b, @(match_message)b, @(match_presence_in)b, " "@(match_presence_out)b from privacy_list_data " "where id =" " (select id from privacy_list" " where username=%(LUser)s and %(LServer)H and name=%(Name)s) " "order by ord")). set_default_privacy_list(LUser, LServer, Name) -> ?SQL_UPSERT_T( "privacy_default_list", ["!username=%(LUser)s", "!server_host=%(LServer)s", "name=%(Name)s"]). unset_default_privacy_list(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from privacy_default_list" " where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; Err -> Err end. remove_privacy_list_t(LUser, LServer, Name) -> case ejabberd_sql:sql_query_t( ?SQL("delete from privacy_list where" " username=%(LUser)s and %(LServer)H and name=%(Name)s")) of {updated, 0} -> {error, notfound}; {updated, _} -> ok; Err -> Err end. add_privacy_list(LUser, LServer, Name) -> ejabberd_sql:sql_query_t( ?SQL_INSERT( "privacy_list", ["username=%(LUser)s", "server_host=%(LServer)s", "name=%(Name)s"])). set_privacy_list(ID, RItems) -> ejabberd_sql:sql_query_t( ?SQL("delete from privacy_list_data where id=%(ID)d")), lists:foreach( fun({SType, SValue, SAction, Order, MatchAll, MatchIQ, MatchMessage, MatchPresenceIn, MatchPresenceOut}) -> ejabberd_sql:sql_query_t( ?SQL("insert into privacy_list_data(id, t, " "value, action, ord, match_all, match_iq, " "match_message, match_presence_in, match_presence_out) " "values (%(ID)d, %(SType)s, %(SValue)s, %(SAction)s," " %(Order)d, %(MatchAll)b, %(MatchIQ)b," " %(MatchMessage)b, %(MatchPresenceIn)b," " %(MatchPresenceOut)b)")) end, RItems). del_privacy_lists(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from privacy_list where username=%(LUser)s and %(LServer)H")) of {updated, _} -> case ejabberd_sql:sql_query( LServer, ?SQL("delete from privacy_default_list " "where username=%(LUser)s and %(LServer)H")) of {updated, _} -> ok; Err -> Err end; Err -> Err end. ejabberd-21.12/src/mod_caps_opt.erl0000644000232200023220000000253714154362354017603 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_caps_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_caps, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_caps, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_caps, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_caps, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_caps, use_cache). ejabberd-21.12/src/mod_private_mnesia.erl0000644000232200023220000000774014154362354021002 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_private_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_private_mnesia). -behaviour(mod_private). %% API -export([init/2, set_data/3, get_data/3, get_all_data/2, del_data/2, use_cache/1, import/3]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_private.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, private_storage, [{disc_only_copies, [node()]}, {attributes, record_info(fields, private_storage)}]). use_cache(Host) -> case mnesia:table_info(private_storage, storage_type) of disc_only_copies -> mod_private_opt:use_cache(Host); _ -> false end. set_data(LUser, LServer, Data) -> F = fun () -> lists:foreach( fun({XmlNS, Xmlel}) -> mnesia:write( #private_storage{ usns = {LUser, LServer, XmlNS}, xml = Xmlel}) end, Data) end, transaction(F). get_data(LUser, LServer, XmlNS) -> case mnesia:dirty_read(private_storage, {LUser, LServer, XmlNS}) of [#private_storage{xml = Storage_Xmlel}] -> {ok, Storage_Xmlel}; _ -> error end. get_all_data(LUser, LServer) -> case lists:flatten( mnesia:dirty_select(private_storage, [{#private_storage{usns = {LUser, LServer, '_'}, xml = '$1'}, [], ['$1']}])) of [] -> error; Res -> {ok, Res} end. del_data(LUser, LServer) -> F = fun () -> Namespaces = mnesia:select(private_storage, [{#private_storage{usns = {LUser, LServer, '$1'}, _ = '_'}, [], ['$$']}]), lists:foreach(fun ([Namespace]) -> mnesia:delete({private_storage, {LUser, LServer, Namespace}}) end, Namespaces) end, transaction(F). import(LServer, <<"private_storage">>, [LUser, XMLNS, XML, _TimeStamp]) -> El = #xmlel{} = fxml_stream:parse_element(XML), PS = #private_storage{usns = {LUser, LServer, XMLNS}, xml = El}, mnesia:dirty_write(PS). need_transform({private_storage, {U, S, NS}, _}) when is_list(U) orelse is_list(S) orelse is_list(NS) -> ?INFO_MSG("Mnesia table 'private_storage' will be converted to binary", []), true; need_transform(_) -> false. transform(#private_storage{usns = {U, S, NS}, xml = El} = R) -> R#private_storage{usns = {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(NS)}, xml = fxml:to_xmlel(El)}. %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of {atomic, Res} -> Res; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. ejabberd-21.12/src/ejabberd_s2s_in.erl0000644000232200023220000003102314154362354020137 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 12 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_s2s_in). -behaviour(xmpp_stream_in). -behaviour(ejabberd_listener). %% ejabberd_listener callbacks -export([start/3, start_link/3, accept/1, listen_options/0]). %% xmpp_stream_in callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([tls_options/1, tls_required/1, tls_enabled/1, compress_methods/1, unauthenticated_stream_features/1, authenticated_stream_features/1, handle_stream_start/2, handle_stream_end/2, handle_stream_established/1, handle_auth_success/4, handle_auth_failure/4, handle_send/3, handle_recv/3, handle_cdata/2, handle_unauthenticated_packet/2, handle_authenticated_packet/2]). %% Hooks -export([handle_unexpected_info/2, handle_unexpected_cast/2, reject_unauthenticated_packet/2, process_closed/2]). %% API -export([stop_async/1, close/1, close/2, send/2, update_state/2, establish/1, host_up/1, host_down/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -type state() :: xmpp_stream_in:state(). -export_type([state/0]). %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Socket, Opts) -> xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). start_link(SockMod, Socket, Opts) -> xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). close(Ref) -> xmpp_stream_in:close(Ref). close(Ref, Reason) -> xmpp_stream_in:close(Ref, Reason). -spec stop_async(pid()) -> ok. stop_async(Pid) -> xmpp_stream_in:stop_async(Pid). accept(Ref) -> xmpp_stream_in:accept(Ref). -spec send(pid(), xmpp_element()) -> ok; (state(), xmpp_element()) -> state(). send(Stream, Pkt) -> xmpp_stream_in:send(Stream, Pkt). -spec establish(state()) -> state(). establish(State) -> xmpp_stream_in:establish(State). -spec update_state(pid(), fun((state()) -> state()) | {module(), atom(), list()}) -> ok. update_state(Ref, Callback) -> xmpp_stream_in:cast(Ref, {update_state, Callback}). -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_hooks:add(s2s_in_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:add(s2s_in_unauthenticated_packet, Host, ?MODULE, reject_unauthenticated_packet, 100), ejabberd_hooks:add(s2s_in_handle_info, Host, ?MODULE, handle_unexpected_info, 100), ejabberd_hooks:add(s2s_in_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100). -spec host_down(binary()) -> ok. host_down(Host) -> ejabberd_hooks:delete(s2s_in_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:delete(s2s_in_unauthenticated_packet, Host, ?MODULE, reject_unauthenticated_packet, 100), ejabberd_hooks:delete(s2s_in_handle_info, Host, ?MODULE, handle_unexpected_info, 100), ejabberd_hooks:delete(s2s_in_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100). %%%=================================================================== %%% Hooks %%%=================================================================== handle_unexpected_info(State, Info) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), State. handle_unexpected_cast(State, Msg) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), State. reject_unauthenticated_packet(State, _Pkt) -> Err = xmpp:serr_not_authorized(), send(State, Err). process_closed(#{server := LServer} = State, Reason) -> RServer = case State of #{remote_server := Name} -> Name; #{ip := IP} -> ejabberd_config:may_hide_data(misc:ip_to_list(IP)) end, ?INFO_MSG("Closing inbound s2s connection ~ts -> ~ts: ~ts", [RServer, LServer, xmpp_stream_out:format_error(Reason)]), stop_async(self()), State. %%%=================================================================== %%% xmpp_stream_in callbacks %%%=================================================================== tls_options(#{tls_options := TLSOpts, lserver := LServer, server_host := ServerHost}) -> ejabberd_s2s:tls_options(LServer, ServerHost, TLSOpts). tls_required(#{server_host := ServerHost}) -> ejabberd_s2s:tls_required(ServerHost). tls_enabled(#{server_host := ServerHost}) -> ejabberd_s2s:tls_enabled(ServerHost). compress_methods(#{server_host := ServerHost}) -> case ejabberd_s2s:zlib_enabled(ServerHost) of true -> [<<"zlib">>]; false -> [] end. unauthenticated_stream_features(#{server_host := LServer}) -> ejabberd_hooks:run_fold(s2s_in_pre_auth_features, LServer, [], [LServer]). authenticated_stream_features(#{server_host := LServer}) -> ejabberd_hooks:run_fold(s2s_in_post_auth_features, LServer, [], [LServer]). handle_stream_start(_StreamStart, #{lserver := LServer} = State) -> case check_to(jid:make(LServer), State) of false -> send(State, xmpp:serr_host_unknown()); true -> ServerHost = ejabberd_router:host_of_route(LServer), Opts = ejabberd_config:codec_options(), State#{server_host => ServerHost, codec_options => Opts} end. handle_stream_end(Reason, #{server_host := ServerHost} = State) -> State1 = State#{stop_reason => Reason}, ejabberd_hooks:run_fold(s2s_in_closed, ServerHost, State1, [Reason]). handle_stream_established(State) -> set_idle_timeout(State#{established => true}). handle_auth_success(RServer, Mech, _AuthModule, #{socket := Socket, ip := IP, auth_domains := AuthDomains, server_host := ServerHost, lserver := LServer} = State) -> ?INFO_MSG("(~ts) Accepted inbound s2s ~ts authentication ~ts -> ~ts (~ts)", [xmpp_socket:pp(Socket), Mech, RServer, LServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), State1 = case ejabberd_s2s:allow_host(ServerHost, RServer) of true -> AuthDomains1 = sets:add_element(RServer, AuthDomains), State0 = change_shaper(State, RServer), State0#{auth_domains => AuthDomains1}; false -> State end, ejabberd_hooks:run_fold(s2s_in_auth_result, ServerHost, State1, [true, RServer]). handle_auth_failure(RServer, Mech, Reason, #{socket := Socket, ip := IP, server_host := ServerHost, lserver := LServer} = State) -> ?WARNING_MSG("(~ts) Failed inbound s2s ~ts authentication ~ts -> ~ts (~ts): ~ts", [xmpp_socket:pp(Socket), Mech, RServer, LServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]), ejabberd_hooks:run_fold(s2s_in_auth_result, ServerHost, State, [false, RServer]). handle_unauthenticated_packet(Pkt, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_unauthenticated_packet, ServerHost, State, [Pkt]). handle_authenticated_packet(Pkt, #{server_host := ServerHost} = State) when not ?is_stanza(Pkt) -> ejabberd_hooks:run_fold(s2s_in_authenticated_packet, ServerHost, State, [Pkt]); handle_authenticated_packet(Pkt0, #{ip := {IP, _}} = State) -> Pkt = xmpp:put_meta(Pkt0, ip, IP), From = xmpp:get_from(Pkt), To = xmpp:get_to(Pkt), case check_from_to(From, To, State) of ok -> LServer = ejabberd_router:host_of_route(To#jid.lserver), State1 = ejabberd_hooks:run_fold(s2s_in_authenticated_packet, LServer, State, [Pkt]), {Pkt1, State2} = ejabberd_hooks:run_fold(s2s_receive_packet, LServer, {Pkt, State1}, []), case Pkt1 of drop -> ok; _ -> ejabberd_router:route(Pkt1) end, State2; {error, Err} -> send(State, Err) end. handle_cdata(Data, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_handle_cdata, ServerHost, State, [Data]). handle_recv(El, Pkt, #{server_host := ServerHost} = State) -> State1 = set_idle_timeout(State), ejabberd_hooks:run_fold(s2s_in_handle_recv, ServerHost, State1, [El, Pkt]). handle_send(Pkt, Result, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_handle_send, ServerHost, State, [Pkt, Result]). init([State, Opts]) -> Shaper = proplists:get_value(shaper, Opts, none), TLSOpts1 = lists:filter( fun({certfile, _}) -> true; ({ciphers, _}) -> true; ({dhfile, _}) -> true; ({cafile, _}) -> true; ({protocol_options, _}) -> true; (_) -> false end, Opts), TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of false -> [compression_none | TLSOpts1]; true -> TLSOpts1 end, Timeout = ejabberd_option:negotiation_timeout(), State1 = State#{tls_options => TLSOpts2, auth_domains => sets:new(), xmlns => ?NS_SERVER, lang => ejabberd_option:language(), server => ejabberd_config:get_myname(), lserver => ejabberd_config:get_myname(), server_host => ejabberd_config:get_myname(), established => false, shaper => Shaper}, State2 = xmpp_stream_in:set_timeout(State1, Timeout), ejabberd_hooks:run_fold(s2s_in_init, {ok, State2}, [Opts]). handle_call(Request, From, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_handle_call, ServerHost, State, [Request, From]). handle_cast({update_state, Fun}, State) -> case Fun of {M, F, A} -> erlang:apply(M, F, [State|A]); _ when is_function(Fun) -> Fun(State) end; handle_cast(Msg, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_handle_cast, ServerHost, State, [Msg]). handle_info(Info, #{server_host := ServerHost} = State) -> ejabberd_hooks:run_fold(s2s_in_handle_info, ServerHost, State, [Info]). terminate(Reason, #{auth_domains := AuthDomains, socket := Socket} = State) -> case maps:get(stop_reason, State, undefined) of {tls, _} = Err -> ?WARNING_MSG("(~ts) Failed to secure inbound s2s connection: ~ts", [xmpp_socket:pp(Socket), xmpp_stream_in:format_error(Err)]); _ -> ok end, case Reason of {process_limit, _} -> sets:fold( fun(Host, _) -> ejabberd_s2s:external_host_overloaded(Host) end, ok, AuthDomains); _ -> ok end. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec check_from_to(jid(), jid(), state()) -> ok | {error, stream_error()}. check_from_to(From, To, State) -> case check_from(From, State) of true -> case check_to(To, State) of true -> ok; false -> {error, xmpp:serr_host_unknown()} end; false -> {error, xmpp:serr_invalid_from()} end. -spec check_from(jid(), state()) -> boolean(). check_from(#jid{lserver = S1}, #{auth_domains := AuthDomains}) -> sets:is_element(S1, AuthDomains). -spec check_to(jid(), state()) -> boolean(). check_to(#jid{lserver = LServer}, _State) -> ejabberd_router:is_my_route(LServer). -spec set_idle_timeout(state()) -> state(). set_idle_timeout(#{server_host := ServerHost, established := true} = State) -> Timeout = ejabberd_s2s:get_idle_timeout(ServerHost), xmpp_stream_in:set_timeout(State, Timeout); set_idle_timeout(State) -> State. -spec change_shaper(state(), binary()) -> state(). change_shaper(#{shaper := ShaperName, server_host := ServerHost} = State, RServer) -> Shaper = ejabberd_shaper:match(ServerHost, ShaperName, jid:make(RServer)), xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)). listen_options() -> [{shaper, none}, {ciphers, undefined}, {dhfile, undefined}, {cafile, undefined}, {protocol_options, undefined}, {tls, false}, {tls_compression, false}, {max_stanza_size, infinity}, {max_fsm_queue, 10000}]. ejabberd-21.12/src/mod_sip_opt.erl0000644000232200023220000000322514154362354017443 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_sip_opt). -export([always_record_route/1]). -export([flow_timeout_tcp/1]). -export([flow_timeout_udp/1]). -export([record_route/1]). -export([routes/1]). -export([via/1]). -spec always_record_route(gen_mod:opts() | global | binary()) -> boolean(). always_record_route(Opts) when is_map(Opts) -> gen_mod:get_opt(always_record_route, Opts); always_record_route(Host) -> gen_mod:get_module_opt(Host, mod_sip, always_record_route). -spec flow_timeout_tcp(gen_mod:opts() | global | binary()) -> pos_integer(). flow_timeout_tcp(Opts) when is_map(Opts) -> gen_mod:get_opt(flow_timeout_tcp, Opts); flow_timeout_tcp(Host) -> gen_mod:get_module_opt(Host, mod_sip, flow_timeout_tcp). -spec flow_timeout_udp(gen_mod:opts() | global | binary()) -> pos_integer(). flow_timeout_udp(Opts) when is_map(Opts) -> gen_mod:get_opt(flow_timeout_udp, Opts); flow_timeout_udp(Host) -> gen_mod:get_module_opt(Host, mod_sip, flow_timeout_udp). -spec record_route(gen_mod:opts() | global | binary()) -> esip:uri(). record_route(Opts) when is_map(Opts) -> gen_mod:get_opt(record_route, Opts); record_route(Host) -> gen_mod:get_module_opt(Host, mod_sip, record_route). -spec routes(gen_mod:opts() | global | binary()) -> [esip:uri()]. routes(Opts) when is_map(Opts) -> gen_mod:get_opt(routes, Opts); routes(Host) -> gen_mod:get_module_opt(Host, mod_sip, routes). -spec via(gen_mod:opts() | global | binary()) -> [{'tcp' | 'tls' | 'udp',{binary(),1..65535}}]. via(Opts) when is_map(Opts) -> gen_mod:get_opt(via, Opts); via(Host) -> gen_mod:get_module_opt(Host, mod_sip, via). ejabberd-21.12/src/mod_ping_opt.erl0000644000232200023220000000224214154362354017603 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_ping_opt). -export([ping_ack_timeout/1]). -export([ping_interval/1]). -export([send_pings/1]). -export([timeout_action/1]). -spec ping_ack_timeout(gen_mod:opts() | global | binary()) -> 'undefined' | pos_integer(). ping_ack_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(ping_ack_timeout, Opts); ping_ack_timeout(Host) -> gen_mod:get_module_opt(Host, mod_ping, ping_ack_timeout). -spec ping_interval(gen_mod:opts() | global | binary()) -> pos_integer(). ping_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(ping_interval, Opts); ping_interval(Host) -> gen_mod:get_module_opt(Host, mod_ping, ping_interval). -spec send_pings(gen_mod:opts() | global | binary()) -> boolean(). send_pings(Opts) when is_map(Opts) -> gen_mod:get_opt(send_pings, Opts); send_pings(Host) -> gen_mod:get_module_opt(Host, mod_ping, send_pings). -spec timeout_action(gen_mod:opts() | global | binary()) -> 'kill' | 'none'. timeout_action(Opts) when is_map(Opts) -> gen_mod:get_opt(timeout_action, Opts); timeout_action(Host) -> gen_mod:get_module_opt(Host, mod_ping, timeout_action). ejabberd-21.12/src/mod_bosh_opt.erl0000644000232200023220000000546614154362354017614 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_bosh_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([json/1]). -export([max_concat/1]). -export([max_inactivity/1]). -export([max_pause/1]). -export([prebind/1]). -export([queue_type/1]). -export([ram_db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_bosh, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_bosh, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_bosh, cache_size). -spec json(gen_mod:opts() | global | binary()) -> boolean(). json(Opts) when is_map(Opts) -> gen_mod:get_opt(json, Opts); json(Host) -> gen_mod:get_module_opt(Host, mod_bosh, json). -spec max_concat(gen_mod:opts() | global | binary()) -> 'unlimited' | pos_integer(). max_concat(Opts) when is_map(Opts) -> gen_mod:get_opt(max_concat, Opts); max_concat(Host) -> gen_mod:get_module_opt(Host, mod_bosh, max_concat). -spec max_inactivity(gen_mod:opts() | global | binary()) -> pos_integer(). max_inactivity(Opts) when is_map(Opts) -> gen_mod:get_opt(max_inactivity, Opts); max_inactivity(Host) -> gen_mod:get_module_opt(Host, mod_bosh, max_inactivity). -spec max_pause(gen_mod:opts() | global | binary()) -> pos_integer(). max_pause(Opts) when is_map(Opts) -> gen_mod:get_opt(max_pause, Opts); max_pause(Host) -> gen_mod:get_module_opt(Host, mod_bosh, max_pause). -spec prebind(gen_mod:opts() | global | binary()) -> boolean(). prebind(Opts) when is_map(Opts) -> gen_mod:get_opt(prebind, Opts); prebind(Host) -> gen_mod:get_module_opt(Host, mod_bosh, prebind). -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_bosh, queue_type). -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_bosh, ram_db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_bosh, use_cache). ejabberd-21.12/src/translate.erl0000644000232200023220000002147214154362354017130 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : translate.erl %%% Author : Alexey Shchepin %%% Purpose : Localization helper %%% Created : 6 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(translate). -author('alexey@process-one.net'). -behaviour(gen_server). -export([start_link/0, reload/0, translate/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include_lib("kernel/include/file.hrl"). -define(ZERO_DATETIME, {{0,0,0}, {0,0,0}}). -type error_reason() :: file:posix() | {integer(), module(), term()} | badarg | terminated | system_limit | bad_file | bad_encoding. -record(state, {}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> process_flag(trap_exit, true), case load() of ok -> xmpp:set_tr_callback({?MODULE, translate}), {ok, #state{}}; {error, Reason} -> {stop, Reason} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> xmpp:set_tr_callback(undefined). code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec reload() -> ok | {error, error_reason()}. reload() -> load(true). -spec load() -> ok | {error, error_reason()}. load() -> load(false). -spec load(boolean()) -> ok | {error, error_reason()}. load(ForceCacheRebuild) -> {MsgsDirMTime, MsgsDir} = get_msg_dir(), {CacheMTime, CacheFile} = get_cache_file(), {FilesMTime, MsgFiles} = get_msg_files(MsgsDir), LastModified = lists:max([MsgsDirMTime, FilesMTime]), if ForceCacheRebuild orelse CacheMTime < LastModified -> case load(MsgFiles, MsgsDir) of ok -> dump_to_file(CacheFile); Err -> Err end; true -> case ets:file2tab(CacheFile) of {ok, _} -> ok; {error, {read_error, {file_error, _, enoent}}} -> load(MsgFiles, MsgsDir); {error, {read_error, {file_error, _, Reason}}} -> ?WARNING_MSG("Failed to read translation cache from ~ts: ~ts", [CacheFile, format_error(Reason)]), load(MsgFiles, MsgsDir); {error, Reason} -> ?WARNING_MSG("Failed to read translation cache from ~ts: ~p", [CacheFile, Reason]), load(MsgFiles, MsgsDir) end end. -spec load([file:filename()], file:filename()) -> ok | {error, error_reason()}. load(Files, Dir) -> try ets:new(translations, [named_table, public]) of _ -> ok catch _:badarg -> ok end, case Files of [] -> ?WARNING_MSG("No translation files found in ~ts, " "check directory access", [Dir]); _ -> ?INFO_MSG("Building language translation cache", []), Objs = lists:flatten(misc:pmap(fun load_file/1, Files)), case lists:keyfind(error, 1, Objs) of false -> ets:delete_all_objects(translations), ets:insert(translations, Objs), ?DEBUG("Language translation cache built successfully", []); {error, File, Reason} -> ?ERROR_MSG("Failed to read translation file ~ts: ~ts", [File, format_error(Reason)]), {error, Reason} end end. -spec load_file(file:filename()) -> [{{binary(), binary()}, binary()} | {error, file:filename(), error_reason()}]. load_file(File) -> Lang = lang_of_file(File), try file:consult(File) of {ok, Lines} -> lists:map( fun({In, Out}) -> try {unicode:characters_to_binary(In), unicode:characters_to_binary(Out)} of {InB, OutB} when is_binary(InB), is_binary(OutB) -> {{Lang, InB}, OutB}; _ -> {error, File, bad_encoding} catch _:badarg -> {error, File, bad_encoding} end; (_) -> {error, File, bad_file} end, Lines); {error, Reason} -> [{error, File, Reason}] catch _:{case_clause, {error, _}} -> %% At the moment of the writing there was a bug in %% file:consult_stream/3 - it doesn't process {error, term()} %% result from io:read/3 [{error, File, bad_file}] end. -spec translate(binary(), binary()) -> binary(). translate(Lang, Msg) -> LLang = ascii_tolower(Lang), case ets:lookup(translations, {LLang, Msg}) of [{_, Trans}] -> Trans; _ -> ShortLang = case str:tokens(LLang, <<"-">>) of [] -> LLang; [SL | _] -> SL end, case ShortLang of <<"en">> -> Msg; LLang -> translate(Msg); _ -> case ets:lookup(translations, {ShortLang, Msg}) of [{_, Trans}] -> Trans; _ -> translate(Msg) end end end. -spec translate(binary()) -> binary(). translate(Msg) -> case ejabberd_option:language() of <<"en">> -> Msg; Lang -> LLang = ascii_tolower(Lang), case ets:lookup(translations, {LLang, Msg}) of [{_, Trans}] -> Trans; _ -> ShortLang = case str:tokens(LLang, <<"-">>) of [] -> LLang; [SL | _] -> SL end, case ShortLang of <<"en">> -> Msg; Lang -> Msg; _ -> case ets:lookup(translations, {ShortLang, Msg}) of [{_, Trans}] -> Trans; _ -> Msg end end end end. -spec ascii_tolower(list() | binary()) -> binary(). ascii_tolower(B) when is_binary(B) -> << <<(if X >= $A, X =< $Z -> X + 32; true -> X end)>> || <> <= B >>; ascii_tolower(S) -> ascii_tolower(unicode:characters_to_binary(S)). -spec get_msg_dir() -> {calendar:datetime(), file:filename()}. get_msg_dir() -> Dir = misc:msgs_dir(), case file:read_file_info(Dir) of {ok, #file_info{mtime = MTime}} -> {MTime, Dir}; {error, Reason} -> ?ERROR_MSG("Failed to read directory ~ts: ~ts", [Dir, format_error(Reason)]), {?ZERO_DATETIME, Dir} end. -spec get_msg_files(file:filename()) -> {calendar:datetime(), [file:filename()]}. get_msg_files(MsgsDir) -> Res = filelib:fold_files( MsgsDir, ".+\\.msg", false, fun(File, {MTime, Files} = Acc) -> case xmpp_lang:is_valid(lang_of_file(File)) of true -> case file:read_file_info(File) of {ok, #file_info{mtime = Time}} -> {lists:max([MTime, Time]), [File|Files]}; {error, Reason} -> ?ERROR_MSG("Failed to read translation file ~ts: ~ts", [File, format_error(Reason)]), Acc end; false -> ?WARNING_MSG("Ignoring translation file ~ts: file name " "must be a valid language tag", [File]), Acc end end, {?ZERO_DATETIME, []}), case Res of {_, []} -> case file:list_dir(MsgsDir) of {ok, _} -> ok; {error, Reason} -> ?ERROR_MSG("Failed to read directory ~ts: ~ts", [MsgsDir, format_error(Reason)]) end; _ -> ok end, Res. -spec get_cache_file() -> {calendar:datetime(), file:filename()}. get_cache_file() -> MnesiaDir = mnesia:system_info(directory), CacheFile = filename:join(MnesiaDir, "translations.cache"), CacheMTime = case file:read_file_info(CacheFile) of {ok, #file_info{mtime = Time}} -> Time; {error, _} -> ?ZERO_DATETIME end, {CacheMTime, CacheFile}. -spec dump_to_file(file:filename()) -> ok. dump_to_file(CacheFile) -> case ets:tab2file(translations, CacheFile) of ok -> ok; {error, Reason} -> ?WARNING_MSG("Failed to create translation cache in ~ts: ~p", [CacheFile, Reason]) end. -spec lang_of_file(file:filename()) -> binary(). lang_of_file(FileName) -> BaseName = filename:basename(FileName), ascii_tolower(filename:rootname(BaseName)). -spec format_error(error_reason()) -> string(). format_error(bad_file) -> "corrupted or invalid translation file"; format_error(bad_encoding) -> "cannot translate from UTF-8"; format_error({_, _, _} = Reason) -> "at line " ++ file:format_error(Reason); format_error(Reason) -> file:format_error(Reason). ejabberd-21.12/src/ejabberd_auth_mnesia.erl0000644000232200023220000002067314154362354021250 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_mnesia.erl %%% Author : Alexey Shchepin %%% Purpose : Authentication via mnesia %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_mnesia). -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -export([start/1, stop/1, set_password/3, try_register/3, get_users/2, init_db/0, count_users/2, get_password/2, remove_user/2, store_type/1, import/2, plain_password_required/1, use_cache/1]). -export([need_transform/1, transform/1]). -include("logger.hrl"). -include_lib("xmpp/include/scram.hrl"). -include("ejabberd_auth.hrl"). -record(reg_users_counter, {vhost = <<"">> :: binary(), count = 0 :: integer() | '$1'}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(Host) -> init_db(), update_reg_users_counter_table(Host), ok. stop(_Host) -> ok. init_db() -> ejabberd_mnesia:create(?MODULE, passwd, [{disc_only_copies, [node()]}, {attributes, record_info(fields, passwd)}]), ejabberd_mnesia:create(?MODULE, reg_users_counter, [{ram_copies, [node()]}, {attributes, record_info(fields, reg_users_counter)}]). update_reg_users_counter_table(Server) -> Set = get_users(Server, []), Size = length(Set), LServer = jid:nameprep(Server), F = fun () -> mnesia:write(#reg_users_counter{vhost = LServer, count = Size}) end, mnesia:sync_dirty(F). use_cache(Host) -> case mnesia:table_info(passwd, storage_type) of disc_only_copies -> ejabberd_option:auth_use_cache(Host); _ -> false end. plain_password_required(Server) -> store_type(Server) == scram. store_type(Server) -> ejabberd_auth:password_format(Server). set_password(User, Server, Password) -> US = {User, Server}, F = fun () -> mnesia:write(#passwd{us = US, password = Password}) end, case mnesia:transaction(F) of {atomic, ok} -> {cache, {ok, Password}}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} end. try_register(User, Server, Password) -> US = {User, Server}, F = fun () -> case mnesia:read({passwd, US}) of [] -> mnesia:write(#passwd{us = US, password = Password}), mnesia:dirty_update_counter(reg_users_counter, Server, 1), {ok, Password}; [_] -> {error, exists} end end, case mnesia:transaction(F) of {atomic, Res} -> {cache, Res}; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {nocache, {error, db_failure}} end. get_users(Server, []) -> mnesia:dirty_select(passwd, [{#passwd{us = '$1', _ = '_'}, [{'==', {element, 2, '$1'}, Server}], ['$1']}]); get_users(Server, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> get_users(Server, [{limit, End - Start + 1}, {offset, Start}]); get_users(Server, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> case get_users(Server, []) of [] -> []; Users -> Set = lists:keysort(1, Users), L = length(Set), Start = if Offset < 1 -> 1; Offset > L -> L; true -> Offset end, lists:sublist(Set, Start, Limit) end; get_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], lists:keysort(1, Set); get_users(Server, [{prefix, Prefix}, {from, Start}, {to, End}]) when is_binary(Prefix) and is_integer(Start) and is_integer(End) -> get_users(Server, [{prefix, Prefix}, {limit, End - Start + 1}, {offset, Start}]); get_users(Server, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> case [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)] of [] -> []; Users -> Set = lists:keysort(1, Users), L = length(Set), Start = if Offset < 1 -> 1; Offset > L -> L; true -> Offset end, lists:sublist(Set, Start, Limit) end; get_users(Server, _) -> get_users(Server, []). count_users(Server, []) -> case mnesia:dirty_select( reg_users_counter, [{#reg_users_counter{vhost = Server, count = '$1'}, [], ['$1']}]) of [Count] -> Count; _ -> 0 end; count_users(Server, [{prefix, Prefix}]) when is_binary(Prefix) -> Set = [{U, S} || {U, S} <- get_users(Server, []), str:prefix(Prefix, U)], length(Set); count_users(Server, _) -> count_users(Server, []). get_password(User, Server) -> case mnesia:dirty_read(passwd, {User, Server}) of [{passwd, _, {scram, SK, SEK, Salt, IC}}] -> {cache, {ok, #scram{storedkey = SK, serverkey = SEK, salt = Salt, hash = sha, iterationcount = IC}}}; [#passwd{password = Password}] -> {cache, {ok, Password}}; _ -> {cache, error} end. remove_user(User, Server) -> US = {User, Server}, F = fun () -> mnesia:delete({passwd, US}), mnesia:dirty_update_counter(reg_users_counter, Server, -1), ok end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. need_transform(#reg_users_counter{}) -> false; need_transform({passwd, {U, S}, Pass}) -> case Pass of _ when is_binary(Pass) -> case store_type(S) of scram -> ?INFO_MSG("Passwords in Mnesia table 'passwd' " "will be SCRAM'ed", []), true; plain -> false end; {scram, _, _, _, _} -> case store_type(S) of scram -> false; plain -> ?WARNING_MSG("Some passwords were stored in the database " "as SCRAM, but 'auth_password_format' " "is not configured as 'scram': some " "authentication mechanisms such as DIGEST-MD5 " "would *fail*", []), false end; #scram{} -> case store_type(S) of scram -> false; plain -> ?WARNING_MSG("Some passwords were stored in the database " "as SCRAM, but 'auth_password_format' " "is not configured as 'scram': some " "authentication mechanisms such as DIGEST-MD5 " "would *fail*", []), false end; _ when is_list(U) orelse is_list(S) orelse is_list(Pass) -> ?INFO_MSG("Mnesia table 'passwd' will be converted to binary", []), true end. transform({passwd, {U, S}, Pass}) when is_list(U) orelse is_list(S) orelse is_list(Pass) -> NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, NewPass = case Pass of #scram{storedkey = StoredKey, serverkey = ServerKey, salt = Salt} -> Pass#scram{ storedkey = iolist_to_binary(StoredKey), serverkey = iolist_to_binary(ServerKey), salt = iolist_to_binary(Salt)}; _ -> iolist_to_binary(Pass) end, transform(#passwd{us = NewUS, password = NewPass}); transform(#passwd{us = {U, S}, password = Password} = P) when is_binary(Password) -> case store_type(S) of scram -> case jid:resourceprep(Password) of error -> ?ERROR_MSG("SASLprep failed for password of user ~ts@~ts", [U, S]), P; _ -> Scram = ejabberd_auth:password_to_scram(S, Password), P#passwd{password = Scram} end; plain -> P end; transform({passwd, _, {scram, _, _, _, _}} = P) -> P; transform(#passwd{password = #scram{}} = P) -> P. import(LServer, [LUser, Password, _TimeStamp]) -> mnesia:dirty_write( #passwd{us = {LUser, LServer}, password = Password}). ejabberd-21.12/src/mod_blocking.erl0000644000232200023220000002140214154362354017553 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_blocking.erl %%% Author : Stephan Maka %%% Purpose : XEP-0191: Simple Communications Blocking %%% Created : 24 Aug 2008 by Stephan Maka %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_blocking). -behaviour(gen_mod). -protocol({xep, 191, '1.2'}). -export([start/2, stop/1, reload/3, process_iq/1, depends/2, disco_features/5, mod_options/1, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("translate.hrl"). start(Host, _Opts) -> ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_BLOCKING, ?MODULE, process_iq). stop(Host) -> ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_BLOCKING). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> [{mod_privacy, hard}]. -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. disco_features({error, Err}, _From, _To, _Node, _Lang) -> {error, Err}; disco_features(empty, _From, _To, <<"">>, _Lang) -> {result, [?NS_BLOCKING]}; disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> {result, [?NS_BLOCKING|Feats]}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec process_iq(iq()) -> iq(). process_iq(#iq{type = Type, from = #jid{luser = U, lserver = S}, to = #jid{luser = U, lserver = S}} = IQ) -> case Type of get -> process_iq_get(IQ); set -> process_iq_set(IQ) end; process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). -spec process_iq_get(iq()) -> iq(). process_iq_get(#iq{sub_els = [#block_list{}]} = IQ) -> process_get(IQ); process_iq_get(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_iq_set(iq()) -> iq(). process_iq_set(#iq{lang = Lang, sub_els = [SubEl]} = IQ) -> case SubEl of #block{items = []} -> Txt = ?T("No items found in this query"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); #block{items = Items} -> JIDs = [jid:tolower(JID) || #block_item{jid = JID} <- Items], process_block(IQ, JIDs); #unblock{items = []} -> process_unblock_all(IQ); #unblock{items = Items} -> JIDs = [jid:tolower(JID) || #block_item{jid = JID} <- Items], process_unblock(IQ, JIDs); _ -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)) end. -spec listitems_to_jids([listitem()], [ljid()]) -> [ljid()]. listitems_to_jids([], JIDs) -> JIDs; listitems_to_jids([#listitem{type = jid, action = deny, value = JID} = Item | Items], JIDs) -> Match = case Item of #listitem{match_all = true} -> true; #listitem{match_iq = true, match_message = true, match_presence_in = true, match_presence_out = true} -> true; _ -> false end, if Match -> listitems_to_jids(Items, [JID | JIDs]); true -> listitems_to_jids(Items, JIDs) end; % Skip Privacy List items than cannot be mapped to Blocking items listitems_to_jids([_ | Items], JIDs) -> listitems_to_jids(Items, JIDs). -spec process_block(iq(), [ljid()]) -> iq(). process_block(#iq{from = From} = IQ, LJIDs) -> #jid{luser = LUser, lserver = LServer} = From, case mod_privacy:get_user_list(LUser, LServer, default) of {error, _} -> err_db_failure(IQ); Res -> {Name, List} = case Res of error -> {<<"Blocked contacts">>, []}; {ok, NameList} -> NameList end, AlreadyBlocked = listitems_to_jids(List, []), NewList = lists:foldr( fun(LJID, List1) -> case lists:member(LJID, AlreadyBlocked) of true -> List1; false -> [#listitem{type = jid, value = LJID, action = deny, order = 0, match_all = true}|List1] end end, List, LJIDs), case mod_privacy:set_list(LUser, LServer, Name, NewList) of ok -> case (if Res == error -> mod_privacy:set_default_list( LUser, LServer, Name); true -> ok end) of ok -> mod_privacy:push_list_update(From, Name), Items = [#block_item{jid = jid:make(LJID)} || LJID <- LJIDs], broadcast_event(From, #block{items = Items}), xmpp:make_iq_result(IQ); {error, notfound} -> ?ERROR_MSG("Failed to set default list '~ts': " "the list should exist, but not found", [Name]), err_db_failure(IQ); {error, _} -> err_db_failure(IQ) end; {error, _} -> err_db_failure(IQ) end end. -spec process_unblock_all(iq()) -> iq(). process_unblock_all(#iq{from = From} = IQ) -> #jid{luser = LUser, lserver = LServer} = From, case mod_privacy:get_user_list(LUser, LServer, default) of {ok, {Name, List}} -> NewList = lists:filter( fun(#listitem{action = A}) -> A /= deny end, List), case mod_privacy:set_list(LUser, LServer, Name, NewList) of ok -> mod_privacy:push_list_update(From, Name), broadcast_event(From, #unblock{}), xmpp:make_iq_result(IQ); {error, _} -> err_db_failure(IQ) end; error -> broadcast_event(From, #unblock{}), xmpp:make_iq_result(IQ); {error, _} -> err_db_failure(IQ) end. -spec process_unblock(iq(), [ljid()]) -> iq(). process_unblock(#iq{from = From} = IQ, LJIDs) -> #jid{luser = LUser, lserver = LServer} = From, case mod_privacy:get_user_list(LUser, LServer, default) of {ok, {Name, List}} -> NewList = lists:filter( fun(#listitem{action = deny, type = jid, value = LJID}) -> not lists:member(LJID, LJIDs); (_) -> true end, List), case mod_privacy:set_list(LUser, LServer, Name, NewList) of ok -> mod_privacy:push_list_update(From, Name), Items = [#block_item{jid = jid:make(LJID)} || LJID <- LJIDs], broadcast_event(From, #unblock{items = Items}), xmpp:make_iq_result(IQ); {error, _} -> err_db_failure(IQ) end; error -> Items = [#block_item{jid = jid:make(LJID)} || LJID <- LJIDs], broadcast_event(From, #unblock{items = Items}), xmpp:make_iq_result(IQ); {error, _} -> err_db_failure(IQ) end. -spec broadcast_event(jid(), block() | unblock()) -> ok. broadcast_event(#jid{luser = LUser, lserver = LServer} = From, Event) -> BFrom = jid:remove_resource(From), lists:foreach( fun(R) -> To = jid:replace_resource(From, R), IQ = #iq{type = set, from = BFrom, to = To, id = <<"push", (p1_rand:get_string())/binary>>, sub_els = [Event]}, ejabberd_router:route(IQ) end, ejabberd_sm:get_user_resources(LUser, LServer)). -spec process_get(iq()) -> iq(). process_get(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> case mod_privacy:get_user_list(LUser, LServer, default) of {ok, {_, List}} -> LJIDs = listitems_to_jids(List, []), Items = [#block_item{jid = jid:make(J)} || J <- LJIDs], xmpp:make_iq_result(IQ, #block_list{items = Items}); error -> xmpp:make_iq_result(IQ, #block_list{}); {error, _} -> err_db_failure(IQ) end. -spec err_db_failure(iq()) -> iq(). err_db_failure(#iq{lang = Lang} = IQ) -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)). mod_options(_Host) -> []. mod_doc() -> #{desc => [?T("The module implements " "https://xmpp.org/extensions/xep-0191.html" "[XEP-0191: Blocking Command]."), "", ?T("This module depends on 'mod_privacy' where " "all the configuration is performed.")]}. ejabberd-21.12/src/ejabberd_hooks.erl0000644000232200023220000001762314154362354020077 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_hooks.erl %%% Author : Alexey Shchepin %%% Purpose : Manage hooks %%% Created : 8 Aug 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_hooks). -author('alexey@process-one.net'). -behaviour(gen_server). %% External exports -export([start_link/0, add/3, add/4, add/5, delete/3, delete/4, delete/5, run/2, run/3, run_fold/3, run_fold/4]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, code_change/3, handle_info/2, terminate/2]). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {}). -type hook() :: {Seq :: integer(), Module :: atom(), Function :: atom() | fun()}. %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec add(atom(), fun(), integer()) -> ok. %% @doc See add/4. add(Hook, Function, Seq) when is_function(Function) -> add(Hook, global, undefined, Function, Seq). -spec add(atom(), HostOrModule :: binary() | atom(), fun() | atom() , integer()) -> ok. add(Hook, Host, Function, Seq) when is_function(Function) -> add(Hook, Host, undefined, Function, Seq); %% @doc Add a module and function to this hook. %% The integer sequence is used to sort the calls: low number is called before high number. add(Hook, Module, Function, Seq) -> add(Hook, global, Module, Function, Seq). -spec add(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok. add(Hook, Host, Module, Function, Seq) -> gen_server:call(?MODULE, {add, Hook, Host, Module, Function, Seq}). -spec delete(atom(), fun(), integer()) -> ok. %% @doc See del/4. delete(Hook, Function, Seq) when is_function(Function) -> delete(Hook, global, undefined, Function, Seq). -spec delete(atom(), binary() | atom(), atom() | fun(), integer()) -> ok. delete(Hook, Host, Function, Seq) when is_function(Function) -> delete(Hook, Host, undefined, Function, Seq); %% @doc Delete a module and function from this hook. %% It is important to indicate exactly the same information than when the call was added. delete(Hook, Module, Function, Seq) -> delete(Hook, global, Module, Function, Seq). -spec delete(atom(), binary() | global, atom(), atom() | fun(), integer()) -> ok. delete(Hook, Host, Module, Function, Seq) -> gen_server:call(?MODULE, {delete, Hook, Host, Module, Function, Seq}). -spec run(atom(), list()) -> ok. %% @doc Run the calls of this hook in order, don't care about function results. %% If a call returns stop, no more calls are performed. run(Hook, Args) -> run(Hook, global, Args). -spec run(atom(), binary() | global, list()) -> ok. run(Hook, Host, Args) -> try ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> run1(Ls, Hook, Args); [] -> ok catch _:badarg -> ok end. -spec run_fold(atom(), T, list()) -> T. %% @doc Run the calls of this hook in order. %% The arguments passed to the function are: [Val | Args]. %% The result of a call is used as Val for the next call. %% If a call returns 'stop', no more calls are performed. %% If a call returns {stop, NewVal}, no more calls are performed and NewVal is returned. run_fold(Hook, Val, Args) -> run_fold(Hook, global, Val, Args). -spec run_fold(atom(), binary() | global, T, list()) -> T. run_fold(Hook, Host, Val, Args) -> try ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> run_fold1(Ls, Hook, Val, Args); [] -> Val catch _:badarg -> Val end. %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- init([]) -> _ = ets:new(hooks, [named_table, {read_concurrency, true}]), {ok, #state{}}. handle_call({add, Hook, Host, Module, Function, Seq}, _From, State) -> HookFormat = {Seq, Module, Function}, Reply = handle_add(Hook, Host, HookFormat), {reply, Reply, State}; handle_call({delete, Hook, Host, Module, Function, Seq}, _From, State) -> HookFormat = {Seq, Module, Function}, Reply = handle_delete(Hook, Host, HookFormat), {reply, Reply, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_add(atom(), atom(), hook()) -> ok. handle_add(Hook, Host, El) -> case ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> case lists:member(El, Ls) of true -> ok; false -> NewLs = lists:merge(Ls, [El]), ets:insert(hooks, {{Hook, Host}, NewLs}), ok end; [] -> NewLs = [El], ets:insert(hooks, {{Hook, Host}, NewLs}), ok end. -spec handle_delete(atom(), atom(), hook()) -> ok. handle_delete(Hook, Host, El) -> case ets:lookup(hooks, {Hook, Host}) of [{_, Ls}] -> NewLs = lists:delete(El, Ls), ets:insert(hooks, {{Hook, Host}, NewLs}), ok; [] -> ok end. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- -spec run1([hook()], atom(), list()) -> ok. run1([], _Hook, _Args) -> ok; run1([{_Seq, Module, Function} | Ls], Hook, Args) -> Res = safe_apply(Hook, Module, Function, Args), case Res of 'EXIT' -> run1(Ls, Hook, Args); stop -> ok; _ -> run1(Ls, Hook, Args) end. -spec run_fold1([hook()], atom(), T, list()) -> T. run_fold1([], _Hook, Val, _Args) -> Val; run_fold1([{_Seq, Module, Function} | Ls], Hook, Val, Args) -> Res = safe_apply(Hook, Module, Function, [Val | Args]), case Res of 'EXIT' -> run_fold1(Ls, Hook, Val, Args); stop -> Val; {stop, NewVal} -> NewVal; NewVal -> run_fold1(Ls, Hook, NewVal, Args) end. -spec safe_apply(atom(), atom(), atom() | fun(), list()) -> any(). safe_apply(Hook, Module, Function, Args) -> ?DEBUG("Running hook ~p: ~p:~p/~B", [Hook, Module, Function, length(Args)]), try if is_function(Function) -> apply(Function, Args); true -> apply(Module, Function, Args) end catch ?EX_RULE(E, R, St) when E /= exit; R /= normal -> Stack = ?EX_STACK(St), ?ERROR_MSG("Hook ~p crashed when running ~p:~p/~p:~n" ++ string:join( ["** ~ts"| ["** Arg " ++ integer_to_list(I) ++ " = ~p" || I <- lists:seq(1, length(Args))]], "~n"), [Hook, Module, Function, length(Args), misc:format_exception(2, E, R, Stack)|Args]), 'EXIT' end. ejabberd-21.12/src/mod_block_strangers_opt.erl0000644000232200023220000000312014154362354022024 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_block_strangers_opt). -export([access/1]). -export([allow_local_users/1]). -export([allow_transports/1]). -export([captcha/1]). -export([drop/1]). -export([log/1]). -spec access(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, access). -spec allow_local_users(gen_mod:opts() | global | binary()) -> boolean(). allow_local_users(Opts) when is_map(Opts) -> gen_mod:get_opt(allow_local_users, Opts); allow_local_users(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, allow_local_users). -spec allow_transports(gen_mod:opts() | global | binary()) -> boolean(). allow_transports(Opts) when is_map(Opts) -> gen_mod:get_opt(allow_transports, Opts); allow_transports(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, allow_transports). -spec captcha(gen_mod:opts() | global | binary()) -> boolean(). captcha(Opts) when is_map(Opts) -> gen_mod:get_opt(captcha, Opts); captcha(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, captcha). -spec drop(gen_mod:opts() | global | binary()) -> boolean(). drop(Opts) when is_map(Opts) -> gen_mod:get_opt(drop, Opts); drop(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, drop). -spec log(gen_mod:opts() | global | binary()) -> boolean(). log(Opts) when is_map(Opts) -> gen_mod:get_opt(log, Opts); log(Host) -> gen_mod:get_module_opt(Host, mod_block_strangers, log). ejabberd-21.12/src/mod_fail2ban.erl0000644000232200023220000002471314154362354017451 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_fail2ban.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 15 Aug 2014 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2014-2021 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(mod_fail2ban). -behaviour(gen_mod). -behaviour(gen_server). %% API -export([start/2, stop/1, reload/3, c2s_auth_result/3, c2s_stream_started/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). %% ejabberd command. -export([get_commands_spec/0, unban/1]). -include_lib("stdlib/include/ms_transform.hrl"). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(CLEAN_INTERVAL, timer:minutes(10)). -record(state, {host = <<"">> :: binary()}). %%%=================================================================== %%% API %%%=================================================================== -spec c2s_auth_result(ejabberd_c2s:state(), true | {false, binary()}, binary()) -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. c2s_auth_result(#{sasl_mech := Mech} = State, {false, _}, _User) when Mech == <<"EXTERNAL">> -> State; c2s_auth_result(#{ip := {Addr, _}, lserver := LServer} = State, {false, _}, _User) -> case is_whitelisted(LServer, Addr) of true -> State; false -> BanLifetime = mod_fail2ban_opt:c2s_auth_ban_lifetime(LServer), MaxFailures = mod_fail2ban_opt:c2s_max_auth_failures(LServer), UnbanTS = current_time() + BanLifetime, Attempts = case ets:lookup(failed_auth, Addr) of [{Addr, N, _, _}] -> ets:insert(failed_auth, {Addr, N+1, UnbanTS, MaxFailures}), N+1; [] -> ets:insert(failed_auth, {Addr, 1, UnbanTS, MaxFailures}), 1 end, if Attempts >= MaxFailures -> log_and_disconnect(State, Attempts, UnbanTS); true -> State end end; c2s_auth_result(#{ip := {Addr, _}} = State, true, _User) -> ets:delete(failed_auth, Addr), State. -spec c2s_stream_started(ejabberd_c2s:state(), stream_start()) -> ejabberd_c2s:state() | {stop, ejabberd_c2s:state()}. c2s_stream_started(#{ip := {Addr, _}} = State, _) -> case ets:lookup(failed_auth, Addr) of [{Addr, N, TS, MaxFailures}] when N >= MaxFailures -> case TS > current_time() of true -> log_and_disconnect(State, N, TS); false -> ets:delete(failed_auth, Addr), State end; _ -> State end. %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> catch ets:new(failed_auth, [named_table, public, {heir, erlang:group_leader(), none}]), ejabberd_commands:register_commands(?MODULE, get_commands_spec()), gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end, gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host|_]) -> process_flag(trap_exit, true), ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {ok, #state{host = Host}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(_Msg, State) -> ?WARNING_MSG("Unexpected cast = ~p", [_Msg]), {noreply, State}. handle_info(clean, State) -> ?DEBUG("Cleaning ~p ETS table", [failed_auth]), Now = current_time(), ets:select_delete( failed_auth, ets:fun2ms(fun({_, _, UnbanTS, _}) -> UnbanTS =< Now end)), erlang:send_after(?CLEAN_INTERVAL, self(), clean), {noreply, State}; handle_info(_Info, State) -> ?WARNING_MSG("Unexpected info = ~p", [_Info]), {noreply, State}. terminate(_Reason, #state{host = Host}) -> ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, c2s_auth_result, 100), ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 100), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of true -> ok; false -> ets:delete(failed_auth) end. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %% ejabberd command callback. %%-------------------------------------------------------------------- -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> [#ejabberd_commands{name = unban_ip, tags = [accounts], desc = "Remove banned IP addresses from the fail2ban table", longdesc = "Accepts an IP address with a network mask. " "Returns the number of unbanned addresses, or a negative integer if there were any error.", module = ?MODULE, function = unban, args = [{address, binary}], args_example = [<<"::FFFF:127.0.0.1/128">>], args_desc = ["IP address, optionally with network mask."], result_example = 3, result_desc = "Amount of unbanned entries, or negative in case of error.", result = {unbanned, integer}}]. -spec unban(binary()) -> integer(). unban(S) -> case misc:parse_ip_mask(S) of {ok, {Net, Mask}} -> unban(Net, Mask); error -> ?WARNING_MSG("Invalid network address when trying to unban: ~p", [S]), -1 end. -spec unban(inet:ip_address(), 0..128) -> non_neg_integer(). unban(Net, Mask) -> ets:foldl( fun({Addr, _, _, _}, Acc) -> case misc:match_ip_mask(Addr, Net, Mask) of true -> ets:delete(failed_auth, Addr), Acc+1; false -> Acc end end, 0, failed_auth). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec log_and_disconnect(ejabberd_c2s:state(), pos_integer(), non_neg_integer()) -> {stop, ejabberd_c2s:state()}. log_and_disconnect(#{ip := {Addr, _}, lang := Lang} = State, Attempts, UnbanTS) -> IP = misc:ip_to_list(Addr), UnbanDate = format_date( calendar:now_to_universal_time(msec_to_now(UnbanTS))), Format = ?T("Too many (~p) failed authentications " "from this IP address (~s). The address " "will be unblocked at ~s UTC"), Args = [Attempts, IP, UnbanDate], ?WARNING_MSG("Connection attempt from blacklisted IP ~ts: ~ts", [IP, io_lib:fwrite(Format, Args)]), Err = xmpp:serr_policy_violation({Format, Args}, Lang), {stop, ejabberd_c2s:send(State, Err)}. -spec is_whitelisted(binary(), inet:ip_address()) -> boolean(). is_whitelisted(Host, Addr) -> Access = mod_fail2ban_opt:access(Host), acl:match_rule(Host, Access, Addr) == allow. -spec msec_to_now(pos_integer()) -> erlang:timestamp(). msec_to_now(MSecs) -> Secs = MSecs div 1000, {Secs div 1000000, Secs rem 1000000, 0}. -spec format_date(calendar:datetime()) -> iolist(). format_date({{Year, Month, Day}, {Hour, Minute, Second}}) -> io_lib:format("~2..0w:~2..0w:~2..0w ~2..0w.~2..0w.~4..0w", [Hour, Minute, Second, Day, Month, Year]). current_time() -> erlang:system_time(millisecond). mod_opt_type(access) -> econf:acl(); mod_opt_type(c2s_auth_ban_lifetime) -> econf:timeout(second); mod_opt_type(c2s_max_auth_failures) -> econf:pos_int(). mod_options(_Host) -> [{access, none}, {c2s_auth_ban_lifetime, timer:hours(1)}, {c2s_max_auth_failures, 20}]. mod_doc() -> #{desc => [?T("The module bans IPs that show the malicious signs. " "Currently only C2S authentication failures are detected."), "", ?T("Unlike the standalone program, 'mod_fail2ban' clears the " "record of authentication failures after some time since the " "first failure or on a successful authentication. " "It also does not simply block network traffic, but " "provides the client with a descriptive error message."), "", ?T("WARNING: You should not use this module behind a proxy or load " "balancer. ejabberd will see the failures as coming from the " "load balancer and, when the threshold of auth failures is " "reached, will reject all connections coming from the load " "balancer. You can lock all your user base out of ejabberd " "when using this module behind a proxy.")], opts => [{access, #{value => ?T("AccessName"), desc => ?T("Specify an access rule for whitelisting IP " "addresses or networks. If the rule returns 'allow' " "for a given IP address, that address will never be " "banned. The 'AccessName' should be of type 'ip'. " "The default value is 'none'.")}}, {c2s_auth_ban_lifetime, #{value => "timeout()", desc => ?T("The lifetime of the IP ban caused by too many " "C2S authentication failures. The default value is " "'1' hour.")}}, {c2s_max_auth_failures, #{value => ?T("Number"), desc => ?T("The number of C2S authentication failures to " "trigger the IP ban. The default value is '20'.")}}]}. ejabberd-21.12/src/ejabberd_options_doc.erl0000644000232200023220000020575214154362354021276 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_options_doc). %% API -export([doc/0]). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== doc() -> [{hosts, #{value => ?T("[Domain1, Domain2, ...]"), desc => ?T("The option defines a list containing one or more " "domains that 'ejabberd' will serve. This is a " "**mandatory** option.")}}, {listen, #{value => "[Options, ...]", desc => ?T("The option for listeners configuration. See the " "http://../listen/[Listen Modules] section " "for details.")}}, {modules, #{value => "{Module: Options}", desc => ?T("The option for modules configuration. See " "http://../modules/[Modules] section " "for details.")}}, {loglevel, #{value => "none | emergency | alert | critical | " "error | warning | notice | info | debug", desc => ?T("Verbosity of log files generated by ejabberd. " "The default value is 'info'. " "NOTE: previous versions of ejabberd had log levels " "defined in numeric format ('0..5'). The numeric values " "are still accepted for backward compatibility, but " "are not recommended.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("The time of a cached item to keep in cache. " "Once it's expired, the corresponding item is " "erased from cache. The default value is '1 hour'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " "_`auth_cache_life_time`_, _`oauth_cache_life_time`_, " "_`router_cache_life_time`_, and _`sm_cache_life_time`_.")}}, {cache_missed, #{value => "true | false", desc => ?T("Whether or not to cache missed lookups. When there is " "an attempt to lookup for a value in a database and " "this value is not found and the option is set to 'true', " "this attempt will be cached and no attempts will be " "performed until the cache expires (see _`cache_life_time`_). " "Usually you don't want to change it. Default is 'true'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " "_`auth_cache_missed`_, _`oauth_cache_missed`_, " "_`router_cache_missed`_, and _`sm_cache_missed`_.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("A maximum number of items (not memory!) in cache. " "The rule of thumb, for all tables except rosters, " "you should set it to the number of maximum online " "users you expect. For roster multiply this number " "by 20 or so. If the cache size reaches this threshold, " "it's fully cleared, i.e. all items are deleted, and " "the corresponding warning is logged. You should avoid " "frequent cache clearance, because this degrades " "performance. The default value is '1000'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " "_`auth_cache_size`_, _`oauth_cache_size`_, " "_`router_cache_size`_, and _`sm_cache_size`_.")}}, {use_cache, #{value => "true | false", desc => ?T("Enable or disable cache. The default is 'true'. " "Several modules have a similar option; and some core " "ejabberd parts support similar options too, see " "_`auth_use_cache`_, _`oauth_use_cache`_, _`router_use_cache`_, " "and _`sm_use_cache`_.")}}, {default_db, #{value => "mnesia | sql", desc => ?T("Default persistent storage for ejabberd. " "Modules and other components (e.g. authentication) " "may have its own value. The default value is 'mnesia'.")}}, {default_ram_db, #{value => "mnesia | redis | sql", desc => ?T("Default volatile (in-memory) storage for ejabberd. " "Modules and other components (e.g. session management) " "may have its own value. The default value is 'mnesia'.")}}, {queue_type, #{value => "ram | file", desc => ?T("Default type of queues in ejabberd. " "Modules may have its own value of the option. " "The value of 'ram' means that queues will be kept in memory. " "If value 'file' is set, you may also specify directory " "in _`queue_dir`_ option where file queues will be placed. " "The default value is 'ram'.")}}, {version, #{value => "string()", desc => ?T("The option can be used to set custom ejabberd version, " "that will be used by different parts of ejabberd, for " "example by _`mod_version`_ module. The default value is " "obtained at compile time from the underlying version " "control system.")}}, {acl, #{value => "{ACLName: {ACLType: ACLValue}}", desc => ?T("The option defines access control lists: named sets " "of rules which are used to match against different targets " "(such as a JID or an IP address). Every set of rules " "has name 'ACLName': it can be any string except 'all' or 'none' " "(those are predefined names for the rules that match all or nothing " "respectively). The name 'ACLName' can be referenced from other " "parts of the configuration file, for example in _`access_rules`_ " "option. The rules of 'ACLName' are represented by mapping " "'pass:[{ACLType: ACLValue}]'. These can be one of the following:")}, [{user, #{value => ?T("Username"), desc => ?T("If 'Username' is in the form of \"user@server\", " "the rule matches a JID against this value. " "Otherwise, if 'Username' is in the form of \"user\", " "the rule matches any JID that has 'Username' in the node part " "as long as the server part of this JID is any virtual " "host served by ejabberd.")}}, {server, #{value => ?T("Server"), desc => ?T("The rule matches any JID from server 'Server'. " "The value of 'Server' must be a valid " "hostname or an IP address.")}}, {resource, #{value => ?T("Resource"), desc => ?T("The rule matches any JID with a resource 'Resource'.")}}, {ip, #{value => ?T("Network"), desc => ?T("The rule matches any IP address from the 'Network'.")}}, {user_regexp, #{value => ?T("Regexp"), desc => ?T("If 'Regexp' is in the form of \"regexp@server\", the rule " "matches any JID with node part matching regular expression " "\"regexp\" as long as the server part of this JID is equal " "to \"server\". If 'Regexp' is in the form of \"regexp\", the rule " "matches any JID with node part matching regular expression " "\"regexp\" as long as the server part of this JID is any virtual " "host served by ejabberd.")}}, {server_regexp, #{value => ?T("Regexp"), desc => ?T("The rule matches any JID from the server that " "matches regular expression 'Regexp'.")}}, {resource_regexp, #{value => ?T("Regexp"), desc => ?T("The rule matches any JID with a resource that " "matches regular expression 'Regexp'.")}}, {node_regexp, #{value => ?T("user_regexp@server_regexp"), desc => ?T("The rule matches any JID with node part matching regular " "expression 'user_regexp' and server part matching regular " "expression 'server_regexp'.")}}, {user_glob, #{value => ?T("Pattern"), desc => ?T("Same as 'user_regexp', but matching is performed on a " "specified 'Pattern' according to the rules used by the " "Unix shell.")}}, {server_glob, #{value => ?T("Pattern"), desc => ?T("Same as 'server_regexp', but matching is performed on a " "specified 'Pattern' according to the rules used by the " "Unix shell.")}}, {resource_glob, #{value => ?T("Pattern"), desc => ?T("Same as 'resource_regexp', but matching is performed on a " "specified 'Pattern' according to the rules used by the " "Unix shell.")}}, {node_glob, #{value => ?T("Pattern"), desc => ?T("Same as 'node_regexp', but matching is performed on a " "specified 'Pattern' according to the rules used by the " "Unix shell.")}}]}, {access_rules, #{value => "{AccessName: {allow|deny: ACLRules|ACLName}}", desc => ?T("The option specifies access rules. Each access rule is " "assigned a name that can be referenced from other parts " "of the configuration file (mostly from 'access' options of " "ejabberd modules). Each rule definition may contain " "arbitrary number of 'allow' or 'deny' sections, and each " "section may contain any number of ACL rules (see _`acl`_ option). " "There are no access rules defined by default."), example => ["access_rules:", " configure:", " allow: admin", " something:", " deny: someone", " allow: all", " s2s_banned:", " deny: problematic_hosts", " deny: banned_forever", " deny:", " ip: 222.111.222.111/32", " deny:", " ip: 111.222.111.222/32", " allow: all", " xmlrpc_access:", " allow:", " user: peter@example.com", " allow:", " user: ivone@example.com", " allow:", " user: bot@example.com", " ip: 10.0.0.0/24"]}}, {acme, #{value => ?T("Options"), desc => ?T("http://../basic/#acme[ACME] configuration, to automatically " "obtain SSL certificates for the domains served by ejabberd, " "which means that certificate requests and renewals are " "performed to some CA server (aka \"ACME server\") in a fully " "automated mode. The 'Options' are:"), example => ["acme:", " ca_url: https://acme-v02.api.letsencrypt.org/directory", " contact:", " - mailto:admin@domain.tld", " - mailto:bot@domain.tld", " auto: true", " cert_type: rsa"]}, [{ca_url, #{value => ?T("URL"), desc => ?T("The ACME directory URL used as an entry point " "for the ACME server. The default value is " " - " "the directory URL of Let's Encrypt authority.")}}, {contact, #{value => ?T("[Contact, ...]"), desc => ?T("A list of contact addresses (typically emails) " "where an ACME server will send notifications " "when problems occur. The value of 'Contact' must " "be in the form of \"scheme:address\" (e.g. " "\"mailto:user@domain.tld\"). The default " "is an empty list which means an ACME server " "will send no notices.")}}, {auto, #{value => "true | false", desc => ?T("Whether to automatically request certificates for " "all configured domains (that yet have no a certificate) " "on server start or configuration reload. The default is 'true'.")}}, {cert_type, #{value => "rsa | ec", desc => ?T("A type of a certificate key. Available values are " "'ec' and 'rsa' for EC and RSA certificates respectively. " "It's better to have RSA certificates for the purpose " "of backward compatibility with legacy clients and servers, " "thus the default is 'rsa'.")}}]}, {allow_contrib_modules, #{value => "true | false", desc => ?T("Whether to allow installation of third-party modules or not. " "The default value is 'true'.")}}, {allow_multiple_connections, #{value => "true | false", desc => ?T("This option is only used when the anonymous mode is enabled. " "Setting it to 'true' means that the same username can be " "taken multiple times in anonymous login mode if different " "resource are used to connect. This option is only useful " "in very special occasions. The default value is 'false'.")}}, {anonymous_protocol, #{value => "login_anon | sasl_anon | both", desc => [?T("Define what anonymous protocol will be used: "), "", ?T("* 'login_anon' means that the anonymous login method will be used. "), "", ?T("* 'sasl_anon' means that the SASL Anonymous method will be used. "), "", ?T("* 'both' means that SASL Anonymous and login anonymous are both " "enabled."), "", ?T("The default value is 'sasl_anon'."), ""]}}, {api_permissions, #{value => "[Permission, ...]", desc => ?T("Define the permissions for API access. Please consult the " "ejabberd Docs web -> For Developers -> ejabberd ReST API -> " "https://docs.ejabberd.im/developer/ejabberd-api/permissions/" "[API Permissions].")}}, {append_host_config, #{value => "{Host: Options}", desc => ?T("To define specific ejabberd modules in a virtual host, " "you can define the global 'modules' option with the common modules, " "and later add specific modules to certain virtual hosts. " "To accomplish that, 'append_host_config' option can be used.")}}, {auth_cache_life_time, #{value => "timeout()", desc => ?T("Same as _`cache_life_time`_, but applied to authentication cache " "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {auth_cache_missed, #{value => "true | false", desc => ?T("Same as _`cache_missed`_, but applied to authentication cache " "only. If not set, the value from _`cache_missed`_ will be used.")}}, {auth_cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as _`cache_size`_, but applied to authentication cache " "only. If not set, the value from _`cache_size`_ will be used.")}}, {auth_method, #{value => "[mnesia | sql | anonymous | external | jwt | ldap | pam, ...]", desc => ?T("A list of authentication methods to use. " "If several methods are defined, authentication is " "considered successful as long as authentication of " "at least one of the methods succeeds. " "The default value is '[mnesia]'.")}}, {auth_opts, #{value => "[Option, ...]", desc => ?T("This is used by the contributed module " "'ejabberd_auth_http' that can be installed from the " "https://github.com/processone/ejabberd-contrib[ejabberd-contrib] " "Git repository. Please refer to that " "module's README file for details.")}}, {auth_password_format, #{value => "plain | scram", note => "improved in 20.01", desc => [?T("The option defines in what format the users passwords " "are stored:"), "", ?T("* 'plain': The password is stored as plain text " "in the database. This is risky because the passwords " "can be read if your database gets compromised. " "This is the default value. This format allows clients to " "authenticate using: the old Jabber Non-SASL (XEP-0078), " "SASL PLAIN, SASL DIGEST-MD5, and SASL SCRAM-SHA-1. "), "", ?T("* 'scram': The password is not stored, only some information " "that allows to verify the hash provided by the client. " "It is impossible to obtain the original plain password " "from the stored information; for this reason, when this " "value is configured it cannot be changed to plain anymore. " "This format allows clients to authenticate using: " "SASL PLAIN and SASL SCRAM-SHA-1."), ?T("The default value is 'plain'.")]}}, {auth_scram_hash, #{value => "sha | sha256 | sha512", desc => ?T("Hash algorith that should be used to store password in SCRAM format. " "You shouldn't change this if you already have passwords generated with " "a different algorithm - users that have such passwords will not be able " "to authenticate. The default value is 'sha'.")}}, {auth_use_cache, #{value => "true | false", desc => ?T("Same as _`use_cache`_, but applied to authentication cache " "only. If not set, the value from _`use_cache`_ will be used.")}}, {c2s_cafile, #{value => ?T("Path"), desc => [?T("Full path to a file containing one or more CA certificates " "in PEM format. All client certificates should be signed by " "one of these root CA certificates and should contain the " "corresponding JID(s) in 'subjectAltName' field. " "There is no default value."), "", ?T("You can use http://../toplevel/#host-config[host_config] to specify this option per-vhost."), "", ?T("To set a specific file per listener, use the listener's http://../listen-options/#cafile[cafile] option. Please notice that 'c2s_cafile' overrides the listener's 'cafile' option."), "" ]}}, {c2s_ciphers, #{value => "[Cipher, ...]", desc => ?T("A list of OpenSSL ciphers to use for c2s connections. " "The default value is shown in the example below:"), example => ["c2s_ciphers:", " - HIGH", " - \"!aNULL\"", " - \"!eNULL\"", " - \"!3DES\"", " - \"@STRENGTH\""]}}, {c2s_dhfile, #{value => ?T("Path"), desc => ?T("Full path to a file containing custom DH parameters " "to use for c2s connections. " "Such a file could be created with the command \"openssl " "dhparam -out dh.pem 2048\". If this option is not specified, " "2048-bit MODP Group with 256-bit Prime Order Subgroup will be " "used as defined in RFC5114 Section 2.3.")}}, {c2s_protocol_options, #{value => "[Option, ...]", desc => ?T("List of general SSL options to use for c2s connections. " "These map to OpenSSL's 'set_options()'. The default value is " "shown in the example below:"), example => ["c2s_protocol_options:", " - no_sslv3", " - cipher_server_preference", " - no_compression"]}}, {c2s_tls_compression, #{value => "true | false", desc => ?T("Whether to enable or disable TLS compression for c2s connections. " "The default value is 'false'.")}}, {ca_file, #{value => ?T("Path"), desc => [?T("Path to a file of CA root certificates. " "The default is to use system defined file if possible."), "", ?T("For server conections, this 'ca_file' option is overriden by the http://../toplevel/#s2s-cafile[s2s_cafile] option."), "" ]}}, {captcha_cmd, #{value => ?T("Path"), desc => ?T("Full path to a script that generates http://../basic/#captcha[CAPTCHA] images. " "There is no default value: when this option is not " "set, CAPTCHA functionality is completely disabled.")}}, {captcha_limit, #{value => "pos_integer() | infinity", desc => ?T("Maximum number of http://../basic/#captcha[CAPTCHA] generated images per minute for " "any given JID. The option is intended to protect the server " "from CAPTCHA DoS. The default value is 'infinity'.")}}, {captcha_host, #{value => "String", desc => ?T("Deprecated. Use _`captcha_url`_ instead.")}}, {captcha_url, #{value => ?T("URL"), desc => ?T("An URL where http://../basic/#captcha[CAPTCHA] requests should be sent. NOTE: you need " "to configure 'request_handlers' for 'ejabberd_http' listener " "as well. There is no default value.")}}, {certfiles, #{value => "[Path, ...]", desc => ?T("The option accepts a list of file paths (optionally with " "wildcards) containing either PEM certificates or PEM private " "keys. At startup or configuration reload, ejabberd reads all " "certificates from these files, sorts them, removes duplicates, " "finds matching private keys and then rebuilds full certificate " "chains for the use in TLS connections. " "Use this option when TLS is enabled in either of " "ejabberd listeners: 'ejabberd_c2s', 'ejabberd_http' and so on. " "NOTE: if you modify the certificate files or change the value " "of the option, run 'ejabberdctl reload-config' in order to " "rebuild and reload the certificate chains."), example => [{?T("If you use https://letsencrypt.org[Let's Encrypt] certificates " "for your domain \"domain.tld\", the configuration will look " "like this:"), ["certfiles:", " - /etc/letsencrypt/live/domain.tld/fullchain.pem", " - /etc/letsencrypt/live/domain.tld/privkey.pem"]}]}}, {cluster_backend, #{value => ?T("Backend"), desc => ?T("A database backend to use for storing information about " "cluster. The only available value so far is 'mnesia'.")}}, {cluster_nodes, #{value => "[Node, ...]", desc => ?T("A list of Erlang nodes to connect on ejabberd startup. " "This option is mostly intended for ejabberd customization " "and sophisticated setups. The default value is an empty list.")}}, {define_macro, #{value => "{MacroName: MacroValue}", desc => ?T("Defines a macro. The value can be any valid arbitrary " "YAML value. For convenience, it's recommended to define " "a 'MacroName' in capital letters. Duplicated macros are not allowed. " "Macros are processed after additional configuration files have " "been included, so it is possible to use macros that are defined " "in configuration files included before the usage. " "It is possible to use a 'MacroValue' in the definition of another macro."), example => ["define_macro:", " DEBUG: debug", " LOG_LEVEL: DEBUG", " USERBOB:", " user: bob@localhost", "", "loglevel: LOG_LEVEL", "", "acl:", " admin: USERBOB"]}}, {disable_sasl_mechanisms, #{value => "[Mechanism, ...]", desc => ?T("Specify a list of SASL mechanisms (such as 'DIGEST-MD5' or " "'SCRAM-SHA1') that should not be offered to the client. " "For convenience, the value of 'Mechanism' is case-insensitive. " "The default value is an empty list, i.e. no mechanisms " "are disabled by default.")}}, {domain_balancing, #{value => "{Domain: Options}", desc => ?T("An algorithm to load balance the components that are plugged " "on an ejabberd cluster. It means that you can plug one or several " "instances of the same component on each ejabberd node and that " "the traffic will be automatically distributed. The algorithm " "to deliver messages to the component(s) can be specified by " "this option. For any component connected as 'Domain', available " "'Options' are:"), example => ["domain_balancing:", " component.domain.tld:", " type: destination", " component_number: 5", " transport.example.org:", " type: bare_source"]}, [{type, #{value => "random | source | destination | bare_source | bare_destination", desc => ?T("How to deliver stanzas to connected components: " "'random' - an instance is chosen at random; " "'destination' - an instance is chosen by the full JID of " "the packet's 'to' attribute; " "'source' - by the full JID of the packet's 'from' attribute; " "'bare_destination' - by the the bare JID (without resource) " "of the packet's 'to' attribute; " "'bare_source' - by the bare JID (without resource) of the " "packet's 'from' attribute is used. The default value is 'random'.")}}, {component_number, #{value => "2..1000", desc => ?T("The number of components to balance.")}}]}, {extauth_pool_name, #{value => ?T("Name"), desc => ?T("Define the pool name appendix, so the full pool name will be " "'extauth_pool_Name'. The default value is the hostname.")}}, {extauth_pool_size, #{value => ?T("Size"), desc => ?T("The option defines the number of instances of the same " "external program to start for better load balancing. " "The default is the number of available CPU cores.")}}, {extauth_program, #{value => ?T("Path"), desc => ?T("Indicate in this option the full path to the external " "authentication script. The script must be executable by ejabberd.")}}, {ext_api_headers, #{value => "Headers", desc => ?T("String of headers (separated with commas ',') that will be " "provided by ejabberd when sending ReST requests. " "The default value is an empty string of headers: '\"\"'.")}}, {ext_api_http_pool_size, #{value => "pos_integer()", desc => ?T("Define the size of the HTTP pool, that is, the maximum number " "of sessions that the ejabberd ReST service will handle " "simultaneously. The default value is: '100'.")}}, {ext_api_path_oauth, #{value => "Path", desc => ?T("Define the base URI path when performing OAUTH ReST requests. " "The default value is: '\"/oauth\"'.")}}, {ext_api_url, #{value => "URL", desc => ?T("Define the base URI when performing ReST requests. " "The default value is: '\"http://localhost/api\"'.")}}, {fqdn, #{value => ?T("Domain"), desc => ?T("A fully qualified domain name that will be used in " "SASL DIGEST-MD5 authentication. The default is detected " "automatically.")}}, {hide_sensitive_log_data, #{value => "true | false", desc => ?T("A privacy option to not log sensitive data " "(mostly IP addresses). The default value " "is 'false' for backward compatibility.")}}, {host_config, #{value => "{Host: Options}", desc => ?T("The option is used to redefine 'Options' for virtual host " "'Host'. In the example below LDAP authentication method " "will be used on virtual host 'domain.tld' and SQL method " "will be used on virtual host 'example.org'."), example => ["hosts:", " - domain.tld", " - example.org", "", "auth_method:", " - sql", "", "host_config:", " domain.tld:", " auth_method:", " - ldap"]}}, {include_config_file, #{value => "[Filename, ...\\] | {Filename: Options}", desc => ?T("Read additional configuration from 'Filename'. If the " "value is provided in 'pass:[{Filename: Options}]' format, the " "'Options' must be one of the following:")}, [{disallow, #{value => "[OptionName, ...]", desc => ?T("Disallows the usage of those options in the included " "file 'Filename'. The options that match this criteria " "are not accepted. The default value is an empty list.")}}, {allow_only, #{value => "[OptionName, ...]", desc => ?T("Allows only the usage of those options in the included " "file 'Filename'. The options that do not match this " "criteria are not accepted. The default value is to include " "all options.")}}]}, {jwt_auth_only_rule, #{value => ?T("AccessName"), desc => ?T("This ACL rule defines accounts that can use only this auth " "method, even if others are also defined in the ejabberd " "configuration file. In other words: if there are several auth " "methods enabled for this host (JWT, SQL, ...), users that " "match this rule can only use JWT. " "The default value is 'none'.")}}, {jwt_jid_field, #{value => ?T("FieldName"), desc => ?T("By default, the JID is defined in the '\"jid\"' JWT field. " "This option allows to specify other JWT field name " "where the JID is defined.")}}, {jwt_key, #{value => ?T("FilePath"), desc => ?T("Path to the file that contains the JWK Key. " "The default value is 'undefined'.")}}, {language, #{value => ?T("Language"), desc => ?T("The option defines the default language of server strings " "that can be seen by XMPP clients. If an XMPP client does not " "possess 'xml:lang' attribute, the specified language is used. " "The default value is '\"en\"'.")}}, {ldap_servers, #{value => "[Host, ...]", desc => ?T("A list of IP addresses or DNS names of your LDAP servers. " "The default value is '[localhost]'.")}}, {ldap_backups, #{value => "[Host, ...]", desc => ?T("A list of IP addresses or DNS names of LDAP backup servers. " "When no servers listed in _`ldap_servers`_ option are reachable, " "ejabberd will try to connect to these backup servers. " "The default is an empty list, i.e. no backup servers specified. " "WARNING: ejabberd doesn't try to reconnect back to the main " "servers when they become operational again, so the only way " "to restore these connections is to restart ejabberd. This " "limitation might be fixed in future releases.")}}, {ldap_encrypt, #{value => "tls | none", desc => ?T("Whether to encrypt LDAP connection using TLS or not. " "The default value is 'none'. NOTE: STARTTLS encryption " "is not supported.")}}, {ldap_tls_certfile, #{value => ?T("Path"), desc => ?T("A path to a file containing PEM encoded certificate " "along with PEM encoded private key. This certificate " "will be provided by ejabberd when TLS enabled for " "LDAP connections. There is no default value, which means " "no client certificate will be sent.")}}, {ldap_tls_verify, #{value => "false | soft | hard", desc => ?T("This option specifies whether to verify LDAP server " "certificate or not when TLS is enabled. When 'hard' is set, " "ejabberd doesn't proceed if the certificate is invalid. " "When 'soft' is set, ejabberd proceeds even if the check has failed. " "The default is 'false', which means no checks are performed.")}}, {ldap_tls_cacertfile, #{value => ?T("Path"), desc => ?T("A path to a file containing PEM encoded CA certificates. " "This option is required when TLS verification is enabled.")}}, {ldap_tls_depth, #{value => ?T("Number"), desc => ?T("Specifies the maximum verification depth when TLS verification " "is enabled, i.e. how far in a chain of certificates the " "verification process can proceed before the verification " "is considered to be failed. Peer certificate = 0, " "CA certificate = 1, higher level CA certificate = 2, etc. " "The value '2' thus means that a chain can at most contain " "peer cert, CA cert, next CA cert, and an additional CA cert. " "The default value is '1'.")}}, {ldap_port, #{value => "1..65535", desc => ?T("Port to connect to your LDAP server. The default port is " "'389' if encryption is disabled and '636' if encryption is " "enabled.")}}, {ldap_rootdn, #{value => "RootDN", desc => ?T("Bind Distinguished Name. The default value is an empty " "string, which means \"anonymous connection\".")}}, {ldap_password, #{value => ?T("Password"), desc => ?T("Bind password. The default value is an empty string.")}}, {ldap_deref_aliases, #{value => "never | always | finding | searching", desc => ?T("Whether to dereference aliases or not. " "The default value is 'never'.")}}, {ldap_base, #{value => "Base", desc => ?T("LDAP base directory which stores users accounts. " "There is no default value: you must set the option " "in order for LDAP connections to work properly.")}}, {ldap_uids, #{value => "[Attr\\] | {Attr: AttrFormat}", desc => ?T("LDAP attributes which hold a list of attributes to use " "as alternatives for getting the JID, where 'Attr' is " "an LDAP attribute which holds the user's part of the JID and " "'AttrFormat' must contain one and only one pattern variable " "\"%u\" which will be replaced by the user's part of the JID. " "For example, \"%u@example.org\". If the value is in the form " "of '[Attr]' then 'AttrFormat' is assumed to be \"%u\".")}}, {ldap_filter, #{value => ?T("Filter"), desc => ?T("An LDAP filter as defined in " "https://tools.ietf.org/html/rfc4515[RFC4515]. " "There is no default value. Example: " "\"(&(objectClass=shadowAccount)(memberOf=XMPP Users))\". " "NOTE: don't forget to close brackets and don't use superfluous " "whitespaces. Also you must not use \"uid\" attribute in the " "filter because this attribute will be appended to the filter " "automatically.")}}, {ldap_dn_filter, #{value => "{Filter: FilterAttrs}", desc => ?T("This filter is applied on the results returned by the main " "filter. The filter performs an additional LDAP lookup to make " "the complete result. This is useful when you are unable to " "define all filter rules in 'ldap_filter'. You can define " "\"%u\", \"%d\", \"%s\" and \"%D\" pattern variables in 'Filter': " "\"%u\" is replaced by a user's part of the JID, \"%d\" is " "replaced by the corresponding domain (virtual host), all \"%s\" " "variables are consecutively replaced by values from the attributes " "in 'FilterAttrs' and \"%D\" is replaced by Distinguished Name from " "the result set. There is no default value, which means the " "result is not filtered. WARNING: Since this filter makes " "additional LDAP lookups, use it only as the last resort: " "try to define all filter rules in _`ldap_filter`_ option if possible."), example => ["ldap_dn_filter:", " \"(&(name=%s)(owner=%D)(user=%u@%d))\": [sn]"]}}, {log_rotate_count, #{value => ?T("Number"), desc => ?T("The number of rotated log files to keep. " "The default value is '1', which means that only keeps " "`ejabberd.log.0`, `error.log.0` and `crash.log.0`.")}}, {log_rotate_size, #{value => "pos_integer() | infinity", desc => ?T("The size (in bytes) of a log file to trigger rotation. " "If set to 'infinity', log rotation is disabled. " "The default value is '10485760' (that is, 10 Mb).")}}, {max_fsm_queue, #{value => ?T("Size"), desc => ?T("This option specifies the maximum number of elements " "in the queue of the FSM (Finite State Machine). Roughly " "speaking, each message in such queues represents one " "XML stanza queued to be sent into its relevant outgoing " "stream. If queue size reaches the limit (because, for " "example, the receiver of stanzas is too slow), the FSM " "and the corresponding connection (if any) will be terminated " "and error message will be logged. The reasonable value for " "this option depends on your hardware configuration. " "The allowed values are positive integers. " "The default value is '10000'.")}}, {negotiation_timeout, #{value => "timeout()", desc => ?T("Time to wait for an XMPP stream negotiation to complete. " "When timeout occurs, the corresponding XMPP stream is closed. " "The default value is '30' seconds.")}}, {net_ticktime, #{value => "timeout()", desc => ?T("This option can be used to tune tick time parameter of " "'net_kernel'. It tells Erlang VM how often nodes should check " "if intra-node communication was not interrupted. This option " "must have identical value on all nodes, or it will lead to subtle " "bugs. Usually leaving default value of this is option is best, " "tweak it only if you know what you are doing. " "The default value is '1 minute'.")}}, {new_sql_schema, #{value => "true | false", desc => {?T("Whether to use 'new' SQL schema. All schemas are located " "at . " "There are two schemas available. The default legacy schema " "allows to store one XMPP domain into one ejabberd database. " "The 'new' schema allows to handle several XMPP domains in a " "single ejabberd database. Using this 'new' schema is best when " "serving several XMPP domains and/or changing domains from " "time to time. This avoid need to manage several databases and " "handle complex configuration changes. The default depends on " "configuration flag '--enable-new-sql-schema' which is set " "at compile time."), [binary:part(ejabberd_config:version(), {0,5})]}}}, {oauth_access, #{value => ?T("AccessName"), desc => ?T("By default creating OAuth tokens is not allowed. " "To define which users can create OAuth tokens, " "you can refer to an ejabberd access rule in the " "'oauth_access' option. Use 'all' to allow everyone " "to create tokens.")}}, {oauth_cache_life_time, #{value => "timeout()", desc => ?T("Same as _`cache_life_time`_, but applied to OAuth cache " "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {oauth_cache_missed, #{value => "true | false", desc => ?T("Same as _`cache_missed`_, but applied to OAuth cache " "only. If not set, the value from _`cache_missed`_ will be used.")}}, {oauth_cache_rest_failure_life_time, #{value => "timeout()", note => "added in 21.01", desc => ?T("The time that a failure in OAuth ReST is cached. " "The default value is 'infinity'.")}}, {oauth_cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as _`cache_size`_, but applied to OAuth cache " "only. If not set, the value from _`cache_size`_ will be used.")}}, {oauth_client_id_check, #{value => "allow | db | deny", desc => ?T("Define whether the client authentication is always allowed, " "denied, or it will depend if the client ID is present in the " "database. The default value is 'allow'.")}}, {oauth_use_cache, #{value => "true | false", desc => ?T("Same as _`use_cache`_, but applied to OAuth cache " "only. If not set, the value from _`use_cache`_ will be used.")}}, {oauth_db_type, #{value => "mnesia | sql", desc => ?T("Database backend to use for OAuth authentication. " "The default value is picked from _`default_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {oauth_expire, #{value => "timeout()", desc => ?T("Time during which the OAuth token is valid, in seconds. " "After that amount of time, the token expires and the delegated " "credential cannot be used and is removed from the database. " "The default is '4294967' seconds.")}}, {oom_killer, #{value => "true | false", desc => ?T("Enable or disable OOM (out-of-memory) killer. " "When system memory raises above the limit defined in " "_`oom_watermark`_ option, ejabberd triggers OOM killer " "to terminate most memory consuming Erlang processes. " "Note that in order to maintain functionality, ejabberd only " "attempts to kill transient processes, such as those managing " "client sessions, s2s or database connections. " "The default value is 'true'.")}}, {oom_queue, #{value => ?T("Size"), desc => ?T("Trigger OOM killer when some of the running Erlang processes " "have messages queue above this 'Size'. Note that " "such processes won't be killed if _`oom_killer`_ option is set " "to 'false' or if 'oom_watermark' is not reached yet.")}}, {oom_watermark, #{value => ?T("Percent"), desc => ?T("A percent of total system memory consumed at which " "OOM killer should be activated with some of the processes " "possibly be killed (see _`oom_killer`_ option). Later, when " "memory drops below this 'Percent', OOM killer is deactivated. " "The default value is '80' percents.")}}, {outgoing_s2s_families, #{value => "[ipv4 | ipv6, ...]", desc => ?T("Specify which address families to try, in what order. " "The default is '[ipv4, ipv6]' which means it first tries " "connecting with IPv4, if that fails it tries using IPv6.")}}, {outgoing_s2s_ipv4_address, #{value => "Address", note => "added in 20.12", desc => ?T("Specify the IPv4 address that will be used when establishing " "an outgoing S2S IPv4 connection, for example \"127.0.0.1\". " "The default value is 'undefined'.")}}, {outgoing_s2s_ipv6_address, #{value => "Address", note => "added in 20.12", desc => ?T("Specify the IPv6 address that will be used when establishing " "an outgoing S2S IPv6 connection, for example " "\"::FFFF:127.0.0.1\". The default value is 'undefined'.")}}, {outgoing_s2s_port, #{value => "1..65535", desc => ?T("A port number to use for outgoing s2s connections when the target " "server doesn't have an SRV record. The default value is '5269'.")}}, {outgoing_s2s_timeout, #{value => "timeout()", desc => ?T("The timeout in seconds for outgoing S2S connection attempts. " "The default value is '10' seconds.")}}, {pam_service, #{value => ?T("Name"), desc => ?T("This option defines the PAM service name. Refer to the PAM " "documentation of your operation system for more information. " "The default value is 'ejabberd'.")}}, {pam_userinfotype, #{value => "username | jid", desc => ?T("This option defines what type of information about the " "user ejabberd provides to the PAM service: only the username, " "or the user's JID. Default is 'username'.")}}, {pgsql_users_number_estimate, #{value => "true | false", desc => ?T("Whether to use PostgreSQL estimation when counting registered " "users. The default value is 'false'.")}}, {queue_dir, #{value => ?T("Directory"), desc => ?T("If _`queue_type`_ option is set to 'file', use this 'Directory' " "to store file queues. The default is to keep queues inside " "Mnesia directory.")}}, {redis_connect_timeout, #{value => "timeout()", desc => ?T("A timeout to wait for the connection to be re-established " "to the Redis server. The default is '1 second'.")}}, {redis_db, #{value => ?T("Number"), desc => ?T("Redis database number. The default is '0'.")}}, {redis_password, #{value => ?T("Password"), desc => ?T("The password to the Redis server. " "The default is an empty string, i.e. no password.")}}, {redis_pool_size, #{value => ?T("Number"), desc => ?T("The number of simultaneous connections to the Redis server. " "The default value is '10'.")}}, {redis_port, #{value => "1..65535", desc => ?T("The port where the Redis server is accepting connections. " "The default is '6379'.")}}, {redis_queue_type, #{value => "ram | file", desc => ?T("The type of request queue for the Redis server. " "See description of _`queue_type`_ option for the explanation. " "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {redis_server, #{value => ?T("Hostname"), desc => ?T("A hostname or an IP address of the Redis server. " "The default is 'localhost'.")}}, {registration_timeout, #{value => "timeout()", desc => ?T("This is a global option for module _`mod_register`_. " "It limits the frequency of registrations from a given " "IP or username. So, a user that tries to register a " "new account from the same IP address or JID during " "this time after their previous registration " "will receive an error with the corresponding explanation. " "To disable this limitation, set the value to 'infinity'. " "The default value is '600 seconds'.")}}, {resource_conflict, #{value => "setresource | closeold | closenew", desc => ?T("NOTE: this option is deprecated and may be removed " "anytime in the future versions. The possible values " "match exactly the three possibilities described in " "https://tools.ietf.org/html/rfc6120#section-7.7.2.2" "[XMPP Core: section 7.7.2.2]. " "The default value is 'closeold'. If the client " "uses old Jabber Non-SASL authentication (XEP-0078), " "then this option is not respected, and the action performed " "is 'closeold'.")}}, {router_cache_life_time, #{value => "timeout()", desc => ?T("Same as _`cache_life_time`_, but applied to routing table cache " "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {router_cache_missed, #{value => "true | false", desc => ?T("Same as _`cache_missed`_, but applied to routing table cache " "only. If not set, the value from _`cache_missed`_ will be used.")}}, {router_cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as _`cache_size`_, but applied to routing table cache " "only. If not set, the value from _`cache_size`_ will be used.")}}, {router_db_type, #{value => "mnesia | redis | sql", desc => ?T("Database backend to use for routing information. " "The default value is picked from _`default_ram_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {router_use_cache, #{value => "true | false", desc => ?T("Same as _`use_cache`_, but applied to routing table cache " "only. If not set, the value from _`use_cache`_ will be used.")}}, {rpc_timeout, #{value => "timeout()", desc => ?T("A timeout for remote function calls between nodes " "in an ejabberd cluster. You should probably never change " "this value since those calls are used for internal needs " "only. The default value is '5' seconds.")}}, {s2s_access, #{value => ?T("Access"), desc => ?T("The access rule to restrict server-to-server connections. " "The default value is 'all' which means no restrictions " "are applied.")}}, {s2s_cafile, #{value => ?T("Path"), desc => [?T("A path to a file with CA root certificates that will " "be used to authenticate s2s connections. If not set, " "the value of http://../toplevel/#ca-file[ca_file] will be used."), "", ?T("You can use http://../toplevel/#host-config[host_config] to specify this option per-vhost."), "" ]}}, {s2s_ciphers, #{value => "[Cipher, ...]", desc => ?T("A list of OpenSSL ciphers to use for s2s connections. " "The default value is shown in the example below:"), example => ["s2s_ciphers:", " - HIGH", " - \"!aNULL\"", " - \"!eNULL\"", " - \"!3DES\"", " - \"@STRENGTH\""]}}, {s2s_dhfile, #{value => ?T("Path"), desc => ?T("Full path to a file containing custom DH parameters " "to use for s2s connections. " "Such a file could be created with the command \"openssl " "dhparam -out dh.pem 2048\". If this option is not specified, " "2048-bit MODP Group with 256-bit Prime Order Subgroup will be " "used as defined in RFC5114 Section 2.3.")}}, {s2s_protocol_options, #{value => "[Option, ...]", desc => ?T("List of general SSL options to use for s2s connections. " "These map to OpenSSL's 'set_options()'. The default value is " "shown in the example below:"), example => ["s2s_protocol_options:", " - no_sslv3", " - cipher_server_preference", " - no_compression"]}}, {s2s_tls_compression, #{value => "true | false", desc => ?T("Whether to enable or disable TLS compression for s2s connections. " "The default value is 'false'.")}}, {s2s_dns_retries, #{value => ?T("Number"), desc => ?T("DNS resolving retries. The default value is '2'.")}}, {s2s_dns_timeout, #{value => "timeout()", desc => ?T("The timeout for DNS resolving. The default value is '10' seconds.")}}, {s2s_max_retry_delay, #{value => "timeout()", desc => ?T("The maximum allowed delay for s2s connection retry to connect after a " "failed connection attempt. The default value is '300' seconds " "(5 minutes).")}}, {s2s_queue_type, #{value => "ram | file", desc => ?T("The type of a queue for s2s packets. " "See description of _`queue_type`_ option for the explanation. " "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {s2s_timeout, #{value => "timeout()", desc => ?T("A time to wait before closing an idle s2s connection. " "The default value is '10 minutes'.")}}, {s2s_use_starttls, #{value => "true | false | optional | required", desc => ?T("Whether to use STARTTLS for s2s connections. " "The value of 'false' means STARTTLS is prohibited. " "The value of 'true' or 'optional' means STARTTLS is enabled " "but plain connections are still allowed. And the value of " "'required' means that only STARTTLS connections are allowed. " "The default value is 'false' (for historical reasons).")}}, {s2s_zlib, #{value => "true | false", desc => ?T("Whether to use 'zlib' compression (as defined in " "https://xmpp.org/extensions/xep-0138.html[XEP-0138]) or not. " "The default value is 'false'. WARNING: this type " "of compression is nowadays considered insecure.")}}, {shaper, #{value => "{ShaperName: Rate}", desc => ?T("The option defines a set of shapers. Every shaper is assigned " "a name 'ShaperName' that can be used in other parts of the " "configuration file, such as _`shaper_rules`_ option. The shaper " "itself is defined by its 'Rate', where 'Rate' stands for the " "maximum allowed incoming rate in **bytes** per second. " "When a connection exceeds this limit, ejabberd stops reading " "from the socket until the average rate is again below the " "allowed maximum. In the example below shaper 'normal' limits " "the traffic speed to 1,000 bytes/sec and shaper 'fast' limits " "the traffic speed to 50,000 bytes/sec:"), example => ["shaper:", " normal: 1000", " fast: 50000"]}}, {shaper_rules, #{value => "{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}", desc => ?T("An entry allowing to declaring shaper to use for matching user/hosts. " "Semantics is similar to _`access_rules`_ option, the only difference is " "that instead using 'allow' or 'deny', a name of a shaper (defined in " "_`shaper`_ option) or a positive number should be used."), example => ["shaper_rules:", " connections_limit:", " 10:", " user: peter@example.com", " 100: admin", " 5: all", " download_speed:", " fast: admin", " slow: anonymous_users", " normal: all", " log_days: 30"]}}, {sm_cache_life_time, #{value => "timeout()", desc => ?T("Same as _`cache_life_time`_, but applied to client sessions table cache " "only. If not set, the value from _`cache_life_time`_ will be used.")}}, {sm_cache_missed, #{value => "true | false", desc => ?T("Same as _`cache_missed`_, but applied to client sessions table cache " "only. If not set, the value from _`cache_missed`_ will be used.")}}, {sm_cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as _`cache_size`_, but applied to client sessions table cache " "only. If not set, the value from _`cache_size`_ will be used.")}}, {sm_db_type, #{value => "mnesia | redis | sql", desc => ?T("Database backend to use for client sessions information. " "The default value is picked from _`default_ram_db`_ option, or " "if it's not set, 'mnesia' will be used.")}}, {sm_use_cache, #{value => "true | false", desc => ?T("Same as _`use_cache`_, but applied to client sessions table cache " "only. If not set, the value from _`use_cache`_ will be used.")}}, {sql_type, #{value => "mssql | mysql | odbc | pgsql | sqlite", desc => ?T("The type of an SQL connection. The default is 'odbc'.")}}, {sql_connect_timeout, #{value => "timeout()", desc => ?T("A time to wait for connection to an SQL server to be " "established. The default value is '5' seconds.")}}, {sql_database, #{value => ?T("Database"), desc => ?T("An SQL database name. For SQLite this must be a full " "path to a database file. The default value is 'ejabberd'.")}}, {sql_keepalive_interval, #{value => "timeout()", desc => ?T("An interval to make a dummy SQL request to keep alive the " "connections to the database. There is no default value, so no " "keepalive requests are made.")}}, {sql_odbc_driver, #{value => "Path", note => "added in 20.12", desc => ?T("Path to the ODBC driver to use to connect to a Microsoft SQL " "Server database. This option is only valid if the _`sql_type`_ " "option is set to 'mssql'. " "The default value is: 'libtdsodbc.so'")}}, {sql_password, #{value => ?T("Password"), desc => ?T("The password for SQL authentication. The default is empty string.")}}, {sql_pool_size, #{value => ?T("Size"), desc => ?T("Number of connections to the SQL server that ejabberd will " "open for each virtual host. The default value is 10. WARNING: " "for SQLite this value is '1' by default and it's not recommended " "to change it due to potential race conditions.")}}, {sql_port, #{value => "1..65535", desc => ?T("The port where the SQL server is accepting connections. " "The default is '3306' for MySQL, '5432' for PostgreSQL and " "'1433' for MS SQL. The option has no effect for SQLite.")}}, {sql_prepared_statements, #{value => "true | false", note => "added in 20.01", desc => ?T("This option is 'true' by default, and is useful to disable " "prepared statements. The option is valid for PostgreSQL.")}}, {sql_query_timeout, #{value => "timeout()", desc => ?T("A time to wait for an SQL query response. " "The default value is '60' seconds.")}}, {sql_queue_type, #{value => "ram | file", desc => ?T("The type of a request queue for the SQL server. " "See description of _`queue_type`_ option for the explanation. " "The default value is the value defined in _`queue_type`_ " "or 'ram' if the latter is not set.")}}, {sql_server, #{value => ?T("Host"), desc => ?T("A hostname or an IP address of the SQL server. " "The default value is 'localhost'.")}}, {sql_ssl, #{value => "true | false", note => "improved in 20.03", desc => ?T("Whether to use SSL encrypted connections to the " "SQL server. The option is only available for MySQL and " "PostgreSQL. The default value is 'false'.")}}, {sql_ssl_cafile, #{value => ?T("Path"), desc => ?T("A path to a file with CA root certificates that will " "be used to verify SQL connections. Implies _`sql_ssl`_ " "and _`sql_ssl_verify`_ options are set to 'true'. " "There is no default which means " "certificate verification is disabled.")}}, {sql_ssl_certfile, #{value => ?T("Path"), desc => ?T("A path to a certificate file that will be used " "for SSL connections to the SQL server. Implies _`sql_ssl`_ " "option is set to 'true'. There is no default which means " "ejabberd won't provide a client certificate to the SQL " "server.")}}, {sql_ssl_verify, #{value => "true | false", desc => ?T("Whether to verify SSL connection to the SQL server against " "CA root certificates defined in _`sql_ssl_cafile`_ option. " "Implies _`sql_ssl`_ option is set to 'true'. " "The default value is 'false'.")}}, {sql_start_interval, #{value => "timeout()", desc => ?T("A time to wait before retrying to restore failed SQL connection. " "The default value is '30' seconds.")}}, {sql_username, #{value => ?T("Username"), desc => ?T("A user name for SQL authentication. " "The default value is 'ejabberd'.")}}, {trusted_proxies, #{value => "all | [Network1, Network2, ...]", desc => ?T("Specify what proxies are trusted when an HTTP request " "contains the header 'X-Forwarded-For'. You can specify " "'all' to allow all proxies, or specify a list of IPs, " "possibly with masks. The default value is an empty list. " "This allows, if enabled, to be able to know the real IP " "of the request, for admin purpose, or security configuration " "(for example using 'mod_fail2ban'). IMPORTANT: The proxy MUST " "be configured to set the 'X-Forwarded-For' header if you " "enable this option as, otherwise, the client can set it " "itself and as a result the IP value cannot be trusted for " "security rules in ejabberd.")}}, {validate_stream, #{value => "true | false", desc => ?T("Whether to validate any incoming XML packet according " "to the schemas of " "https://github.com/processone/xmpp#supported-xmpp-elements" "[supported XMPP extensions]. WARNING: the validation is only " "intended for the use by client developers - don't enable " "it in production environment. The default value is 'false'.")}}, {websocket_origin, #{value => "ignore | URL", desc => ?T("This option enables validation for 'Origin' header to " "protect against connections from other domains than given " "in the configuration file. In this way, the lower layer load " "balancer can be chosen for a specific ejabberd implementation " "while still providing a secure Websocket connection. " "The default value is 'ignore'. An example value of the 'URL' is " "\"https://test.example.org:8081\".")}}, {websocket_ping_interval, #{value => "timeout()", desc => ?T("Defines time between pings sent by the server to a client " "(Websocket level protocol pings are used for this) to keep " "a connection active. If the client doesn't respond to two " "consecutive pings, the connection will be assumed as closed. " "The value of '0' can be used to disable the feature. This option " "makes the server sending pings only for connections using the RFC " "compliant protocol. For older style connections the server " "expects that whitespace pings would be used for this purpose. " "The default value is '60' seconds.")}}, {websocket_timeout, #{value => "timeout()", desc => ?T("Amount of time without any communication after which the " "connection would be closed. The default value is '300' seconds.")}}]. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_privacy_opt.erl0000644000232200023220000000256114154362354020327 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_privacy_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_privacy, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_privacy, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_privacy, use_cache). ejabberd-21.12/src/mod_stun_disco_opt.erl0000644000232200023220000000265114154362354021024 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_stun_disco_opt). -export([access/1]). -export([credentials_lifetime/1]). -export([offer_local_services/1]). -export([secret/1]). -export([services/1]). -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, access). -spec credentials_lifetime(gen_mod:opts() | global | binary()) -> pos_integer(). credentials_lifetime(Opts) when is_map(Opts) -> gen_mod:get_opt(credentials_lifetime, Opts); credentials_lifetime(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, credentials_lifetime). -spec offer_local_services(gen_mod:opts() | global | binary()) -> boolean(). offer_local_services(Opts) when is_map(Opts) -> gen_mod:get_opt(offer_local_services, Opts); offer_local_services(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, offer_local_services). -spec secret(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). secret(Opts) when is_map(Opts) -> gen_mod:get_opt(secret, Opts); secret(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, secret). -spec services(gen_mod:opts() | global | binary()) -> [tuple()]. services(Opts) when is_map(Opts) -> gen_mod:get_opt(services, Opts); services(Host) -> gen_mod:get_module_opt(Host, mod_stun_disco, services). ejabberd-21.12/src/ejabberd_systemd.erl0000644000232200023220000001554514154362354020445 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_systemd.erl %%% Author : Holger Weiss %%% Purpose : Integrate with systemd %%% Created : 5 Jan 2021 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_systemd). -author('holger@zedat.fu-berlin.de'). -behaviour(gen_server). -export([start_link/0, ready/0, reloading/0, stopping/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -record(state, {socket :: gen_udp:socket() | undefined, destination :: inet:local_address() | undefined, interval :: pos_integer() | undefined, last_ping :: integer() | undefined}). -type watchdog_timeout() :: pos_integer() | hibernate. -type state() :: #state{}. %%-------------------------------------------------------------------- %% API. %%-------------------------------------------------------------------- -spec start_link() -> {ok, pid()} | ignore | {error, term()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec ready() -> ok. ready() -> cast_notification(<<"READY=1">>). -spec reloading() -> ok. reloading() -> cast_notification(<<"RELOADING=1">>). -spec stopping() -> ok. stopping() -> cast_notification(<<"STOPPING=1">>). %%-------------------------------------------------------------------- %% gen_server callbacks. %%-------------------------------------------------------------------- -spec init(any()) -> {ok, state()} | {ok, state(), watchdog_timeout()} | {stop, term()}. init(_Opts) -> process_flag(trap_exit, true), case os:getenv("NOTIFY_SOCKET") of [$@ | _Abstract] -> ?CRITICAL_MSG("Abstract NOTIFY_SOCKET not supported", []), {stop, esocktnosupport}; Path when is_list(Path), length(Path) > 0 -> ?DEBUG("Got NOTIFY_SOCKET: ~s", [Path]), Destination = {local, Path}, case gen_udp:open(0, [local]) of {ok, Socket} -> Interval = get_watchdog_interval(), State = #state{socket = Socket, destination = Destination, interval = Interval}, if is_integer(Interval), Interval > 0 -> ?INFO_MSG("Watchdog notifications enabled", []), {ok, set_last_ping(State), Interval}; true -> ?INFO_MSG("Watchdog notifications disabled", []), {ok, State} end; {error, Reason} -> ?CRITICAL_MSG("Cannot open IPC socket: ~p", [Reason]), {stop, Reason} end; _ -> ?INFO_MSG("Got no NOTIFY_SOCKET, notifications disabled", []), {ok, #state{}} end. -spec handle_call(term(), {pid(), term()}, state()) -> {reply, {error, badarg}, state(), watchdog_timeout()}. handle_call(Request, From, State) -> ?ERROR_MSG("Got unexpected request from ~p: ~p", [From, Request]), {reply, {error, badarg}, State, get_timeout(State)}. -spec handle_cast({notify, binary()} | term(), state()) -> {noreply, state(), watchdog_timeout()}. handle_cast({notify, Notification}, #state{destination = undefined} = State) -> ?DEBUG("No NOTIFY_SOCKET, dropping ~s notification", [Notification]), {noreply, State, get_timeout(State)}; handle_cast({notify, Notification}, State) -> try notify(State, Notification) catch _:Err -> ?ERROR_MSG("Cannot send ~s notification: ~p", [Notification, Err]) end, {noreply, State, get_timeout(State)}; handle_cast(Msg, State) -> ?ERROR_MSG("Got unexpected message: ~p", [Msg]), {noreply, State, get_timeout(State)}. -spec handle_info(timeout | term(), state()) -> {noreply, state(), watchdog_timeout()}. handle_info(timeout, #state{interval = Interval} = State) when is_integer(Interval), Interval > 0 -> try notify(State, <<"WATCHDOG=1">>) catch _:Err -> ?ERROR_MSG("Cannot ping watchdog: ~p", [Err]) end, {noreply, set_last_ping(State), Interval}; handle_info(Info, State) -> ?ERROR_MSG("Got unexpected info: ~p", [Info]), {noreply, State, get_timeout(State)}. -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. terminate(Reason, #state{socket = undefined}) -> ?DEBUG("Terminating ~s (~p)", [?MODULE, Reason]), ok; terminate(Reason, #state{socket = Socket}) -> ?DEBUG("Closing socket and terminating ~s (~p)", [?MODULE, Reason]), ok = gen_udp:close(Socket). -spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> ?INFO_MSG("Got code change request", []), {ok, State}. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec get_watchdog_interval() -> integer() | undefined. get_watchdog_interval() -> case os:getenv("WATCHDOG_USEC") of WatchdogUSec when is_list(WatchdogUSec), length(WatchdogUSec) > 0 -> Interval = round(0.5 * list_to_integer(WatchdogUSec)), ?DEBUG("Watchdog interval: ~B microseconds", [Interval]), erlang:convert_time_unit(Interval, microsecond, millisecond); _ -> undefined end. -spec get_timeout(state()) -> watchdog_timeout(). get_timeout(#state{interval = undefined}) -> ?DEBUG("Watchdog interval is undefined, hibernating", []), hibernate; get_timeout(#state{interval = Interval, last_ping = LastPing}) -> case Interval - (erlang:monotonic_time(millisecond) - LastPing) of Timeout when Timeout > 0 -> ?DEBUG("Calculated new timeout value: ~B", [Timeout]), Timeout; _ -> ?DEBUG("Calculated new timeout value: 1", []), 1 end. -spec set_last_ping(state()) -> state(). set_last_ping(State) -> LastPing = erlang:monotonic_time(millisecond), State#state{last_ping = LastPing}. -spec notify(state(), binary()) -> ok. notify(#state{socket = Socket, destination = Destination}, Notification) -> ?DEBUG("Notifying systemd: ~s", [Notification]), ok = gen_udp:send(Socket, Destination, 0, Notification). -spec cast_notification(binary()) -> ok. cast_notification(Notification) -> gen_server:cast(?MODULE, {notify, Notification}). ejabberd-21.12/src/mod_sip_proxy.erl0000644000232200023220000003066514154362354020032 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_sip_proxy.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 21 Apr 2014 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2014-2021 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(mod_sip_proxy). -ifndef(SIP). -export([]). -else. -behaviour(p1_fsm). %% API -export([start/2, start_link/2, route/3, route/4]). -export([init/1, wait_for_request/2, wait_for_response/2, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -include("logger.hrl"). -include_lib("esip/include/esip.hrl"). -define(SIGN_LIFETIME, 300). %% in seconds. -record(state, {host = <<"">> :: binary(), opts = [] :: [{certfile, binary()}], orig_trid, responses = [] :: [#sip{}], tr_ids = [] :: list(), orig_req = #sip{} :: #sip{}}). %%%=================================================================== %%% API %%%=================================================================== start(LServer, Opts) -> supervisor:start_child(mod_sip_proxy_sup, [LServer, Opts]). start_link(LServer, Opts) -> p1_fsm:start_link(?MODULE, [LServer, Opts], []). route(SIPMsg, _SIPSock, TrID, Pid) -> p1_fsm:send_event(Pid, {SIPMsg, TrID}). route(#sip{hdrs = Hdrs} = Req, LServer, Opts) -> case proplists:get_bool(authenticated, Opts) of true -> route_statelessly(Req, LServer, Opts); false -> ConfiguredRRoute = get_configured_record_route(LServer), case esip:get_hdrs('route', Hdrs) of [{_, URI, _}|_] -> case cmp_uri(URI, ConfiguredRRoute) of true -> case is_signed_by_me(URI#uri.user, Hdrs) of true -> route_statelessly(Req, LServer, Opts); false -> error end; false -> error end; [] -> error end end. route_statelessly(Req, LServer, Opts) -> Req1 = prepare_request(LServer, Req), case connect(Req1, add_certfile(LServer, Opts)) of {ok, SIPSocketsWithURIs} -> lists:foreach( fun({SIPSocket, _URI}) -> Req2 = add_via(SIPSocket, LServer, Req1), esip:send(SIPSocket, Req2) end, SIPSocketsWithURIs); _ -> error end. %%%=================================================================== %%% gen_fsm callbacks %%%=================================================================== init([Host, Opts]) -> Opts1 = add_certfile(Host, Opts), {ok, wait_for_request, #state{opts = Opts1, host = Host}}. wait_for_request({#sip{type = request} = Req, TrID}, State) -> Opts = State#state.opts, Req1 = prepare_request(State#state.host, Req), case connect(Req1, Opts) of {ok, SIPSocketsWithURIs} -> NewState = lists:foldl( fun(_SIPSocketWithURI, {error, _} = Err) -> Err; ({SIPSocket, URI}, #state{tr_ids = TrIDs} = AccState) -> Req2 = add_record_route_and_set_uri( URI, State#state.host, Req1), Req3 = add_via(SIPSocket, State#state.host, Req2), case esip:request(SIPSocket, Req3, {?MODULE, route, [self()]}) of {ok, ClientTrID} -> NewTrIDs = [ClientTrID|TrIDs], AccState#state{tr_ids = NewTrIDs}; Err -> cancel_pending_transactions(AccState), Err end end, State, SIPSocketsWithURIs), case NewState of {error, _} = Err -> {Status, Reason} = esip:error_status(Err), esip:reply(TrID, mod_sip:make_response( Req, #sip{type = response, status = Status, reason = Reason})), {stop, normal, State}; _ -> {next_state, wait_for_response, NewState#state{orig_req = Req, orig_trid = TrID}} end; {error, notfound} -> esip:reply(TrID, mod_sip:make_response( Req, #sip{type = response, status = 480, reason = esip:reason(480)})), {stop, normal, State}; Err -> {Status, Reason} = esip:error_status(Err), esip:reply(TrID, mod_sip:make_response( Req, #sip{type = response, status = Status, reason = Reason})), {stop, normal, State} end; wait_for_request(_Event, State) -> {next_state, wait_for_request, State}. wait_for_response({#sip{method = <<"CANCEL">>, type = request}, _TrID}, State) -> cancel_pending_transactions(State), {next_state, wait_for_response, State}; wait_for_response({Resp, TrID}, #state{orig_req = #sip{method = Method} = Req} = State) -> case Resp of {error, timeout} when Method /= <<"INVITE">> -> %% Absorb useless 408. See RFC4320 choose_best_response(State), esip:stop_transaction(State#state.orig_trid), {stop, normal, State}; {error, _} -> {Status, Reason} = esip:error_status(Resp), State1 = mark_transaction_as_complete(TrID, State), SIPResp = mod_sip:make_response(Req, #sip{type = response, status = Status, reason = Reason}), State2 = collect_response(SIPResp, State1), case State2#state.tr_ids of [] -> choose_best_response(State2), {stop, normal, State2}; _ -> {next_state, wait_for_response, State2} end; #sip{status = 100} -> {next_state, wait_for_response, State}; #sip{status = Status} -> {[_|Vias], NewHdrs} = esip:split_hdrs('via', Resp#sip.hdrs), NewResp = case Vias of [] -> Resp#sip{hdrs = NewHdrs}; _ -> Resp#sip{hdrs = [{'via', Vias}|NewHdrs]} end, if Status < 300 -> esip:reply(State#state.orig_trid, NewResp); true -> ok end, State1 = if Status >= 200 -> mark_transaction_as_complete(TrID, State); true -> State end, State2 = if Status >= 300 -> collect_response(NewResp, State1); true -> State1 end, if Status >= 600 -> cancel_pending_transactions(State2); true -> ok end, case State2#state.tr_ids of [] -> choose_best_response(State2), {stop, normal, State2}; _ -> {next_state, wait_for_response, State2} end end; wait_for_response(_Event, State) -> {next_state, wait_for_response, State}. handle_event(_Event, StateName, State) -> {next_state, StateName, State}. handle_sync_event(_Event, _From, StateName, State) -> Reply = ok, {reply, Reply, StateName, State}. handle_info(_Info, StateName, State) -> {next_state, StateName, State}. terminate(_Reason, _StateName, _State) -> ok. code_change(_OldVsn, StateName, State, _Extra) -> {ok, StateName, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== connect(#sip{hdrs = Hdrs} = Req, Opts) -> {_, ToURI, _} = esip:get_hdr('to', Hdrs), case mod_sip:at_my_host(ToURI) of true -> LUser = jid:nodeprep(ToURI#uri.user), LServer = jid:nameprep(ToURI#uri.host), case mod_sip_registrar:find_sockets(LUser, LServer) of [_|_] = SIPSocks -> {ok, SIPSocks}; [] -> {error, notfound} end; false -> case esip:connect(Req, Opts) of {ok, SIPSock} -> {ok, [{SIPSock, Req#sip.uri}]}; {error, _} = Err -> Err end end. cancel_pending_transactions(State) -> lists:foreach(fun esip:cancel/1, State#state.tr_ids). add_certfile(LServer, Opts) -> case ejabberd_pkix:get_certfile(LServer) of {ok, CertFile} -> [{certfile, CertFile}|Opts]; error -> Opts end. add_via(#sip_socket{type = Transport}, LServer, #sip{hdrs = Hdrs} = Req) -> ConfiguredVias = get_configured_vias(LServer), {ViaHost, ViaPort} = proplists:get_value( Transport, ConfiguredVias, {LServer, undefined}), ViaTransport = case Transport of tls -> <<"TLS">>; tcp -> <<"TCP">>; udp -> <<"UDP">> end, Via = #via{transport = ViaTransport, host = ViaHost, port = ViaPort, params = [{<<"branch">>, esip:make_branch()}]}, Req#sip{hdrs = [{'via', [Via]}|Hdrs]}. add_record_route_and_set_uri(URI, LServer, #sip{hdrs = Hdrs} = Req) -> case is_request_within_dialog(Req) of false -> case need_record_route(LServer) of true -> RR_URI = get_configured_record_route(LServer), TS = (integer_to_binary(erlang:system_time(second))), Sign = make_sign(TS, Hdrs), User = <>, NewRR_URI = RR_URI#uri{user = User}, Hdrs1 = [{'record-route', [{<<>>, NewRR_URI, []}]}|Hdrs], Req#sip{uri = URI, hdrs = Hdrs1}; false -> Req end; true -> Req end. is_request_within_dialog(#sip{hdrs = Hdrs}) -> {_, _, Params} = esip:get_hdr('to', Hdrs), esip:has_param(<<"tag">>, Params). need_record_route(LServer) -> mod_sip_opt:always_record_route(LServer). make_sign(TS, Hdrs) -> {_, #uri{user = FUser, host = FServer}, FParams} = esip:get_hdr('from', Hdrs), {_, #uri{user = TUser, host = TServer}, _} = esip:get_hdr('to', Hdrs), LFUser = safe_nodeprep(FUser), LTUser = safe_nodeprep(TUser), LFServer = safe_nameprep(FServer), LTServer = safe_nameprep(TServer), FromTag = esip:get_param(<<"tag">>, FParams), CallID = esip:get_hdr('call-id', Hdrs), SharedKey = ejabberd_config:get_shared_key(), str:sha([SharedKey, LFUser, LFServer, LTUser, LTServer, FromTag, CallID, TS]). is_signed_by_me(TS_Sign, Hdrs) -> try [TSBin, Sign] = str:tokens(TS_Sign, <<"-">>), TS = (binary_to_integer(TSBin)), NowTS = erlang:system_time(second), true = (NowTS - TS) =< ?SIGN_LIFETIME, Sign == make_sign(TSBin, Hdrs) catch _:_ -> false end. get_configured_vias(LServer) -> mod_sip_opt:via(LServer). get_configured_record_route(LServer) -> mod_sip_opt:record_route(LServer). get_configured_routes(LServer) -> mod_sip_opt:routes(LServer). mark_transaction_as_complete(TrID, State) -> NewTrIDs = lists:delete(TrID, State#state.tr_ids), State#state{tr_ids = NewTrIDs}. collect_response(Resp, #state{responses = Resps} = State) -> State#state{responses = [Resp|Resps]}. choose_best_response(#state{responses = Responses} = State) -> SortedResponses = lists:keysort(#sip.status, Responses), case lists:filter( fun(#sip{status = Status}) -> Status >= 600 end, SortedResponses) of [Resp|_] -> esip:reply(State#state.orig_trid, Resp); [] -> case SortedResponses of [Resp|_] -> esip:reply(State#state.orig_trid, Resp); [] -> ok end end. %% Just compare host part only. cmp_uri(#uri{host = H1}, #uri{host = H2}) -> jid:nameprep(H1) == jid:nameprep(H2). is_my_route(URI, URIs) -> lists:any(fun(U) -> cmp_uri(URI, U) end, URIs). prepare_request(LServer, #sip{hdrs = Hdrs} = Req) -> ConfiguredRRoute = get_configured_record_route(LServer), ConfiguredRoutes = get_configured_routes(LServer), Hdrs1 = lists:flatmap( fun({Hdr, HdrList}) when Hdr == 'route'; Hdr == 'record-route' -> case lists:filter( fun({_, URI, _}) -> not cmp_uri(URI, ConfiguredRRoute) and not is_my_route(URI, ConfiguredRoutes) end, HdrList) of [] -> []; HdrList1 -> [{Hdr, HdrList1}] end; (Hdr) -> [Hdr] end, Hdrs), MF = esip:get_hdr('max-forwards', Hdrs1), Hdrs2 = esip:set_hdr('max-forwards', MF-1, Hdrs1), Hdrs3 = lists:filter( fun({'proxy-authorization', {_, Params}}) -> Realm = esip:unquote(esip:get_param(<<"realm">>, Params)), not mod_sip:is_my_host(jid:nameprep(Realm)); (_) -> true end, Hdrs2), Req#sip{hdrs = Hdrs3}. safe_nodeprep(S) -> case jid:nodeprep(S) of error -> S; S1 -> S1 end. safe_nameprep(S) -> case jid:nameprep(S) of error -> S; S1 -> S1 end. -endif. ejabberd-21.12/src/ejabberd_pkix.erl0000644000232200023220000003302314154362354017717 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 4 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_pkix). -behaviour(gen_server). %% API -export([start_link/0]). -export([certs_dir/0]). -export([add_certfile/1, del_certfile/1, commit/0]). -export([notify_expired/1]). -export([try_certfile/1, get_certfile/0, get_certfile/1]). -export([get_certfile_no_default/1]). %% Hooks -export([ejabberd_started/0, config_reloaded/0, cert_expired/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). -include("logger.hrl"). -define(CALL_TIMEOUT, timer:minutes(1)). -record(state, {files = sets:new() :: sets:set(filename())}). -type state() :: #state{}. -type filename() :: binary(). %%%=================================================================== %%% API %%%=================================================================== -spec start_link() -> {ok, pid()} | {error, {already_started, pid()} | term()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec add_certfile(file:filename_all()) -> {ok, filename()} | {error, pkix:error_reason()}. add_certfile(Path0) -> Path = prep_path(Path0), try gen_server:call(?MODULE, {add_certfile, Path}, ?CALL_TIMEOUT) catch exit:{noproc, _} -> case add_file(Path) of ok -> {ok, Path}; Err -> Err end end. -spec del_certfile(file:filename_all()) -> ok. del_certfile(Path0) -> Path = prep_path(Path0), try gen_server:call(?MODULE, {del_certfile, Path}, ?CALL_TIMEOUT) catch exit:{noproc, _} -> pkix:del_file(Path) end. -spec try_certfile(file:filename_all()) -> filename(). try_certfile(Path0) -> Path = prep_path(Path0), case pkix:is_pem_file(Path) of true -> Path; {false, Reason} -> ?ERROR_MSG("Failed to read PEM file ~ts: ~ts", [Path, pkix:format_error(Reason)]), erlang:error(badarg) end. -spec get_certfile(binary()) -> {ok, filename()} | error. get_certfile(Domain) -> case get_certfile_no_default(Domain) of {ok, Path} -> {ok, Path}; error -> get_certfile() end. -spec get_certfile_no_default(binary()) -> {ok, filename()} | error. get_certfile_no_default(Domain) -> try list_to_binary(idna:utf8_to_ascii(Domain)) of ASCIIDomain -> case pkix:get_certfile(ASCIIDomain) of error -> error; Ret -> {ok, select_certfile(Ret)} end catch _:_ -> error end. -spec get_certfile() -> {ok, filename()} | error. get_certfile() -> case pkix:get_certfile() of error -> error; Ret -> {ok, select_certfile(Ret)} end. -spec certs_dir() -> file:filename_all(). certs_dir() -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "certs"). -spec commit() -> ok. commit() -> gen_server:call(?MODULE, commit, ?CALL_TIMEOUT). -spec ejabberd_started() -> ok. ejabberd_started() -> gen_server:call(?MODULE, ejabberd_started, ?CALL_TIMEOUT). -spec config_reloaded() -> ok. config_reloaded() -> gen_server:call(?MODULE, config_reloaded, ?CALL_TIMEOUT). -spec notify_expired(pkix:notify_event()) -> ok. notify_expired(Event) -> gen_server:cast(?MODULE, Event). -spec cert_expired(_, pkix:cert_info()) -> ok. cert_expired(_Cert, #{domains := Domains, expiry := Expiry, files := [{Path, Line}|_]}) -> ?WARNING_MSG("Certificate in ~ts (at line: ~B)~ts ~ts", [Path, Line, case Domains of [] -> ""; _ -> " for " ++ misc:format_hosts_list(Domains) end, format_expiration_date(Expiry)]). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -spec init([]) -> {ok, state()}. init([]) -> process_flag(trap_exit, true), ejabberd_hooks:add(cert_expired, ?MODULE, cert_expired, 50), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 100), ejabberd_hooks:add(ejabberd_started, ?MODULE, ejabberd_started, 30), case add_files() of {_Files, []} -> {ok, #state{}}; {Files, [_|_]} -> case ejabberd:is_loaded() of true -> {ok, #state{}}; false -> del_files(Files), stop_ejabberd() end end. -spec handle_call(term(), {pid(), term()}, state()) -> {reply, ok, state()} | {noreply, state()}. handle_call({add_certfile, Path}, _From, State) -> case add_file(Path) of ok -> {reply, {ok, Path}, State}; {error, _} = Err -> {reply, Err, State} end; handle_call({del_certfile, Path}, _From, State) -> pkix:del_file(Path), {reply, ok, State}; handle_call(ejabberd_started, _From, State) -> case do_commit() of {ok, []} -> check_domain_certfiles(), {reply, ok, State}; _ -> stop_ejabberd() end; handle_call(config_reloaded, _From, State) -> Files = get_certfiles_from_config_options(), _ = add_files(Files), case do_commit() of {ok, _} -> check_domain_certfiles(), {reply, ok, State}; error -> {reply, ok, State} end; handle_call(commit, From, State) -> handle_call(config_reloaded, From, State); handle_call(Request, _From, State) -> ?WARNING_MSG("Unexpected call: ~p", [Request]), {noreply, State}. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({cert_expired, Cert, CertInfo}, State) -> ejabberd_hooks:run(cert_expired, [Cert, CertInfo]), {noreply, State}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> any(). terminate(_Reason, State) -> ejabberd_hooks:delete(cert_expired, ?MODULE, cert_expired, 50), ejabberd_hooks:delete(ejabberd_started, ?MODULE, ejabberd_started, 30), ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 100), del_files(State#state.files). -spec code_change(term() | {down, term()}, state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec format_status(normal | terminate, list()) -> term(). format_status(_Opt, Status) -> Status. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec add_files() -> {sets:set(filename()), [{filename(), pkix:error_reason()}]}. add_files() -> Files = get_certfiles_from_config_options(), add_files(sets:to_list(Files), sets:new(), []). -spec add_files(sets:set(filename())) -> {sets:set(filename()), [{filename(), pkix:error_reason()}]}. add_files(Files) -> add_files(sets:to_list(Files), sets:new(), []). -spec add_files([filename()], sets:set(filename()), [{filename(), pkix:error_reason()}]) -> {sets:set(filename()), [{filename(), pkix:error_reason()}]}. add_files([File|Files], Set, Errs) -> case add_file(File) of ok -> Set1 = sets:add_element(File, Set), add_files(Files, Set1, Errs); {error, Reason} -> Errs1 = [{File, Reason}|Errs], add_files(Files, Set, Errs1) end; add_files([], Set, Errs) -> {Set, Errs}. -spec add_file(filename()) -> ok | {error, pkix:error_reason()}. add_file(File) -> case pkix:add_file(File) of ok -> ok; {error, Reason} = Err -> ?ERROR_MSG("Failed to read PEM file ~ts: ~ts", [File, pkix:format_error(Reason)]), Err end. -spec del_files(sets:set(filename())) -> ok. del_files(Files) -> lists:foreach(fun pkix:del_file/1, sets:to_list(Files)). -spec do_commit() -> {ok, [{filename(), pkix:error_reason()}]} | error. do_commit() -> CAFile = ejabberd_option:ca_file(), ?DEBUG("Using CA root certificates from: ~ts", [CAFile]), Opts = [{cafile, CAFile}, {notify_before, [7*24*60*60, % 1 week 24*60*60, % 1 day 60*60, % 1 hour 0]}, {notify_fun, fun ?MODULE:notify_expired/1}], case pkix:commit(certs_dir(), Opts) of {ok, Errors, Warnings, CAError} -> log_errors(Errors), log_cafile_error(CAError), log_warnings(Warnings), fast_tls_add_certfiles(), {ok, Errors}; {error, File, Reason} -> ?CRITICAL_MSG("Failed to write to ~ts: ~ts", [File, file:format_error(Reason)]), error end. -spec check_domain_certfiles() -> ok. check_domain_certfiles() -> Hosts = ejabberd_option:hosts(), Routes = ejabberd_router:get_all_routes(), check_domain_certfiles(Hosts ++ Routes). -spec check_domain_certfiles([binary()]) -> ok. check_domain_certfiles(Hosts) -> case ejabberd_listener:tls_listeners() of [] -> ok; _ -> lists:foreach( fun(Host) -> case get_certfile_no_default(Host) of error -> ?WARNING_MSG( "No certificate found matching ~ts", [Host]); _ -> ok end end, Hosts) end. -spec get_certfiles_from_config_options() -> sets:set(filename()). get_certfiles_from_config_options() -> case ejabberd_option:certfiles() of undefined -> sets:new(); Paths -> lists:foldl( fun(Path, Acc) -> Files = wildcard(Path), lists:foldl(fun sets:add_element/2, Acc, Files) end, sets:new(), Paths) end. -spec prep_path(file:filename_all()) -> filename(). prep_path(Path0) -> case filename:pathtype(Path0) of relative -> case file:get_cwd() of {ok, CWD} -> unicode:characters_to_binary(filename:join(CWD, Path0)); {error, Reason} -> ?WARNING_MSG("Failed to get current directory name: ~ts", [file:format_error(Reason)]), unicode:characters_to_binary(Path0) end; _ -> unicode:characters_to_binary(Path0) end. -spec stop_ejabberd() -> no_return(). stop_ejabberd() -> ?CRITICAL_MSG("ejabberd initialization was aborted due to " "invalid certificates configuration", []), ejabberd:halt(). -spec wildcard(file:filename_all()) -> [filename()]. wildcard(Path) when is_binary(Path) -> wildcard(binary_to_list(Path)); wildcard(Path) -> case filelib:wildcard(Path) of [] -> ?WARNING_MSG("Path ~ts is empty, please make sure ejabberd has " "sufficient rights to read it", [Path]), []; Files -> [prep_path(File) || File <- Files] end. -spec select_certfile({filename() | undefined, filename() | undefined, filename() | undefined}) -> filename(). select_certfile({EC, _, _}) when EC /= undefined -> EC; select_certfile({_, RSA, _}) when RSA /= undefined -> RSA; select_certfile({_, _, DSA}) when DSA /= undefined -> DSA. -spec fast_tls_add_certfiles() -> ok. fast_tls_add_certfiles() -> lists:foreach( fun({Domain, Files}) -> fast_tls:add_certfile(Domain, select_certfile(Files)) end, pkix:get_certfiles()), fast_tls:clear_cache(). reason_to_fmt({invalid_cert, _, _}) -> "Invalid certificate in ~ts: ~ts"; reason_to_fmt(_) -> "Failed to read PEM file ~ts: ~ts". -spec log_warnings([{filename(), pkix:error_reason()}]) -> ok. log_warnings(Warnings) -> lists:foreach( fun({File, Reason}) -> ?WARNING_MSG(reason_to_fmt(Reason), [File, pkix:format_error(Reason)]) end, Warnings). -spec log_errors([{filename(), pkix:error_reason()}]) -> ok. log_errors(Errors) -> lists:foreach( fun({File, Reason}) -> ?ERROR_MSG(reason_to_fmt(Reason), [File, pkix:format_error(Reason)]) end, Errors). -spec log_cafile_error({filename(), pkix:error_reason()} | undefined) -> ok. log_cafile_error({File, Reason}) -> ?CRITICAL_MSG("Failed to read CA certitificates from ~ts: ~ts. " "Try to change/set option 'ca_file'", [File, pkix:format_error(Reason)]); log_cafile_error(_) -> ok. -spec time_before_expiration(calendar:datetime()) -> {non_neg_integer(), string()}. time_before_expiration(Expiry) -> T1 = calendar:datetime_to_gregorian_seconds(Expiry), T2 = calendar:datetime_to_gregorian_seconds( calendar:now_to_datetime(erlang:timestamp())), Secs = max(0, T1 - T2), if Secs == {0, ""}; Secs >= 220752000 -> {round(Secs/220752000), "year"}; Secs >= 2592000 -> {round(Secs/2592000), "month"}; Secs >= 604800 -> {round(Secs/604800), "week"}; Secs >= 86400 -> {round(Secs/86400), "day"}; Secs >= 3600 -> {round(Secs/3600), "hour"}; Secs >= 60 -> {round(Secs/60), "minute"}; true -> {Secs, "second"} end. -spec format_expiration_date(calendar:datetime()) -> string(). format_expiration_date(DateTime) -> case time_before_expiration(DateTime) of {0, _} -> "is expired"; {1, Unit} -> "will expire in a " ++ Unit; {Int, Unit} -> "will expire in " ++ integer_to_list(Int) ++ " " ++ Unit ++ "s" end. ejabberd-21.12/src/ejabberd_config_transformer.erl0000644000232200023220000005024314154362354022636 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_config_transformer). %% API -export([map_reduce/1]). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== map_reduce(Y) -> F = fun(Y1) -> Y2 = (validator())(Y1), Y3 = transform(Y2), case application:get_env(ejabberd, custom_config_transformer) of {ok, TransMod} when is_atom(TransMod) -> TransMod:transform(Y3); _ -> Y3 end end, econf:validate(F, Y). %%%=================================================================== %%% Transformer %%%=================================================================== transform(Y) -> {Y1, Acc1} = transform(global, Y, #{}), {Y2, Acc2} = update(Y1, Acc1), filter(global, Y2, Acc2). transform(Host, Y, Acc) -> filtermapfoldr( fun({Opt, HostOpts}, Acc1) when (Opt == host_config orelse Opt == append_host_config) andalso Host == global -> case filtermapfoldr( fun({Host1, Opts}, Acc2) -> case transform(Host1, Opts, Acc2) of {[], Acc3} -> {false, Acc3}; {Opts1, Acc3} -> {{true, {Host1, Opts1}}, Acc3} end end, Acc1, HostOpts) of {[], Acc4} -> {false, Acc4}; {HostOpts1, Acc4} -> {{true, {Opt, HostOpts1}}, Acc4} end; ({Opt, Val}, Acc1) -> transform(Host, Opt, Val, Acc1) end, Acc, Y). transform(Host, modules, ModOpts, Acc) -> {ModOpts1, Acc2} = lists:mapfoldr( fun({Mod, Opts}, Acc1) -> Opts1 = transform_module_options(Opts), transform_module(Host, Mod, Opts1, Acc1) end, Acc, ModOpts), {{true, {modules, ModOpts1}}, Acc2}; transform(global, listen, Listeners, Acc) -> {Listeners1, Acc2} = lists:mapfoldr( fun(Opts, Acc1) -> transform_listener(Opts, Acc1) end, Acc, Listeners), {{true, {listen, Listeners1}}, Acc2}; transform(_Host, Opt, CertFile, Acc) when (Opt == domain_certfile) orelse (Opt == c2s_certfile) orelse (Opt == s2s_certfile) -> ?WARNING_MSG("Option '~ts' is deprecated and was automatically " "appended to 'certfiles' option. ~ts", [Opt, adjust_hint()]), CertFiles = maps:get(certfiles, Acc, []), Acc1 = maps:put(certfiles, CertFiles ++ [CertFile], Acc), {false, Acc1}; transform(_Host, certfiles, CertFiles1, Acc) -> CertFiles2 = maps:get(certfiles, Acc, []), Acc1 = maps:put(certfiles, CertFiles1 ++ CertFiles2, Acc), {true, Acc1}; transform(_Host, acme, ACME, Acc) -> ACME1 = lists:map( fun({ca_url, URL} = Opt) -> case misc:uri_parse(URL) of {ok, _, "acme-v01.api.letsencrypt.org", _, _} -> NewURL = ejabberd_acme:default_directory_url(), ?WARNING_MSG("ACME directory URL ~ts defined in " "option acme->ca_url is deprecated " "and was automatically replaced " "with ~ts. ~ts", [URL, NewURL, adjust_hint()]), {ca_url, NewURL}; _ -> Opt end; (Opt) -> Opt end, ACME), {{true, {acme, ACME1}}, Acc}; transform(Host, s2s_use_starttls, required_trusted, Acc) -> ?WARNING_MSG("The value 'required_trusted' of option " "'s2s_use_starttls' is deprecated and was " "automatically replaced with value 'required'. " "The module 'mod_s2s_dialback' has also " "been automatically removed from the configuration. ~ts", [adjust_hint()]), Hosts = maps:get(remove_s2s_dialback, Acc, []), Acc1 = maps:put(remove_s2s_dialback, [Host|Hosts], Acc), {{true, {s2s_use_starttls, required}}, Acc1}; transform(_Host, _Opt, _Val, Acc) -> {true, Acc}. update(Y, Acc) -> set_certfiles(Y, Acc). filter(Host, Y, Acc) -> lists:filtermap( fun({Opt, HostOpts}) when (Opt == host_config orelse Opt == append_host_config) andalso Host == global -> HostOpts1 = lists:map( fun({Host1, Opts1}) -> {Host1, filter(Host1, Opts1, Acc)} end, HostOpts), {true, {Opt, HostOpts1}}; ({Opt, Val}) -> filter(Host, Opt, Val, Acc) end, Y). filter(_Host, log_rotate_date, _, _) -> warn_removed_option(log_rotate_date), false; filter(_Host, log_rate_limit, _, _) -> warn_removed_option(log_rate_limit), false; filter(_Host, ca_path, _, _) -> warn_removed_option(ca_path, ca_file), false; filter(_Host, iqdisc, _, _) -> warn_removed_option(iqdisc), false; filter(_Host, access, _, _) -> warn_removed_option(access, access_rules), false; filter(_Host, commands, _, _) -> warn_removed_option(commands, api_permissions), false; filter(_Host, ejabberdctl_access_commands, _, _) -> warn_removed_option(ejabberdctl_access_commands, api_permissions), false; filter(_Host, commands_admin_access, _, _) -> warn_removed_option(commands_admin_access, api_permissions), false; filter(_Host, ldap_group_cache_size, _, _) -> warn_removed_option(ldap_group_cache_size, cache_size), false; filter(_Host, ldap_user_cache_size, _, _) -> warn_removed_option(ldap_user_cache_size, cache_size), false; filter(_Host, ldap_group_cache_validity, _, _) -> warn_removed_option(ldap_group_cache_validity, cache_life_time), false; filter(_Host, ldap_user_cache_validity, _, _) -> warn_removed_option(ldap_user_cache_validity, cache_life_time), false; filter(_Host, ldap_local_filter, _, _) -> warn_removed_option(ldap_local_filter), false; filter(_Host, deref_aliases, Val, _) -> warn_replaced_option(deref_aliases, ldap_deref_aliases), {true, {ldap_deref_aliases, Val}}; filter(_Host, default_db, internal, _) -> {true, {default_db, mnesia}}; filter(_Host, default_db, odbc, _) -> {true, {default_db, sql}}; filter(_Host, auth_method, Ms, _) -> Ms1 = lists:map( fun(internal) -> mnesia; (odbc) -> sql; (M) -> M end, Ms), {true, {auth_method, Ms1}}; filter(_Host, default_ram_db, internal, _) -> {true, {default_ram_db, mnesia}}; filter(_Host, default_ram_db, odbc, _) -> {true, {default_ram_db, sql}}; filter(_Host, extauth_cache, _, _) -> ?WARNING_MSG("Option 'extauth_cache' is deprecated " "and has no effect, use authentication " "or global cache configuration options: " "auth_use_cache, auth_cache_life_time, " "use_cache, cache_life_time, and so on", []), false; filter(_Host, extauth_instances, Val, _) -> warn_replaced_option(extauth_instances, extauth_pool_size), {true, {extauth_pool_size, Val}}; filter(_Host, Opt, Val, _) when Opt == outgoing_s2s_timeout; Opt == s2s_dns_timeout -> warn_huge_timeout(Opt, Val), true; filter(_Host, captcha_host, _, _) -> warn_deprecated_option(captcha_host, captcha_url), true; filter(_Host, route_subdomains, _, _) -> warn_removed_option(route_subdomains, s2s_access), false; filter(Host, modules, ModOpts, State) -> NoDialbackHosts = maps:get(remove_s2s_dialback, State, []), ModOpts1 = lists:filter( fun({mod_s2s_dialback, _}) -> not lists:member(Host, NoDialbackHosts); ({mod_echo, _}) -> warn_removed_module(mod_echo), false; (_) -> true end, ModOpts), {true, {modules, ModOpts1}}; filter(_, _, _, _) -> true. %%%=================================================================== %%% Listener transformers %%%=================================================================== transform_listener(Opts, Acc) -> Opts1 = transform_request_handlers(Opts), Opts2 = transform_turn_ip(Opts1), Opts3 = remove_inet_options(Opts2), collect_listener_certfiles(Opts3, Acc). transform_request_handlers(Opts) -> case lists:keyfind(module, 1, Opts) of {_, ejabberd_http} -> replace_request_handlers(Opts); {_, ejabberd_xmlrpc} -> remove_xmlrpc_access_commands(Opts); _ -> Opts end. transform_turn_ip(Opts) -> case lists:keyfind(module, 1, Opts) of {_, ejabberd_stun} -> replace_turn_ip(Opts); _ -> Opts end. replace_request_handlers(Opts) -> Handlers = proplists:get_value(request_handlers, Opts, []), Handlers1 = lists:foldl( fun({captcha, true}, Acc) -> Handler = {<<"/captcha">>, ejabberd_captcha}, warn_replaced_handler(captcha, Handler), [Handler|Acc]; ({register, true}, Acc) -> Handler = {<<"/register">>, mod_register_web}, warn_replaced_handler(register, Handler), [Handler|Acc]; ({web_admin, true}, Acc) -> Handler = {<<"/admin">>, ejabberd_web_admin}, warn_replaced_handler(web_admin, Handler), [Handler|Acc]; ({http_bind, true}, Acc) -> Handler = {<<"/bosh">>, mod_bosh}, warn_replaced_handler(http_bind, Handler), [Handler|Acc]; ({xmlrpc, true}, Acc) -> Handler = {<<"/">>, ejabberd_xmlrpc}, warn_replaced_handler(xmlrpc, Handler), Acc ++ [Handler]; (_, Acc) -> Acc end, Handlers, Opts), Handlers2 = lists:map( fun({Path, mod_http_bind}) -> warn_replaced_module(mod_http_bind, mod_bosh), {Path, mod_bosh}; (PathMod) -> PathMod end, Handlers1), Opts1 = lists:filtermap( fun({captcha, _}) -> false; ({register, _}) -> false; ({web_admin, _}) -> false; ({http_bind, _}) -> false; ({xmlrpc, _}) -> false; ({http_poll, _}) -> ?WARNING_MSG("Listening option 'http_poll' is " "ignored: HTTP Polling support was " "removed in ejabberd 15.04. ~ts", [adjust_hint()]), false; ({request_handlers, _}) -> false; (_) -> true end, Opts), case Handlers2 of [] -> Opts1; _ -> [{request_handlers, Handlers2}|Opts1] end. remove_xmlrpc_access_commands(Opts) -> lists:filter( fun({access_commands, _}) -> warn_removed_option(access_commands, api_permissions), false; (_) -> true end, Opts). replace_turn_ip(Opts) -> lists:filtermap( fun({turn_ip, Val}) -> warn_replaced_option(turn_ip, turn_ipv4_address), {true, {turn_ipv4_address, Val}}; (_) -> true end, Opts). remove_inet_options(Opts) -> lists:filter( fun({Opt, _}) when Opt == inet; Opt == inet6 -> warn_removed_option(Opt, ip), false; (_) -> true end, Opts). collect_listener_certfiles(Opts, Acc) -> Mod = proplists:get_value(module, Opts), if Mod == ejabberd_http; Mod == ejabberd_c2s; Mod == ejabberd_s2s_in -> case lists:keyfind(certfile, 1, Opts) of {_, CertFile} -> ?WARNING_MSG("Listening option 'certfile' of module ~ts " "is deprecated and was automatically " "appended to global 'certfiles' option. ~ts", [Mod, adjust_hint()]), CertFiles = maps:get(certfiles, Acc, []), {proplists:delete(certfile, Opts), maps:put(certfiles, [CertFile|CertFiles], Acc)}; false -> {Opts, Acc} end; true -> {Opts, Acc} end. %%%=================================================================== %%% Module transformers %%% NOTE: transform_module_options/1 is called before transform_module/4 %%%=================================================================== transform_module_options(Opts) -> lists:filtermap( fun({Opt, internal}) when Opt == db_type; Opt == ram_db_type -> {true, {Opt, mnesia}}; ({Opt, odbc}) when Opt == db_type; Opt == ram_db_type -> {true, {Opt, sql}}; ({deref_aliases, Val}) -> warn_replaced_option(deref_aliases, ldap_deref_aliases), {true, {ldap_deref_aliases, Val}}; ({ldap_group_cache_size, _}) -> warn_removed_option(ldap_group_cache_size, cache_size), false; ({ldap_user_cache_size, _}) -> warn_removed_option(ldap_user_cache_size, cache_size), false; ({ldap_group_cache_validity, _}) -> warn_removed_option(ldap_group_cache_validity, cache_life_time), false; ({ldap_user_cache_validity, _}) -> warn_removed_option(ldap_user_cache_validity, cache_life_time), false; ({iqdisc, _}) -> warn_removed_option(iqdisc), false; (_) -> true end, Opts). transform_module(Host, mod_http_bind, Opts, Acc) -> warn_replaced_module(mod_http_bind, mod_bosh), transform_module(Host, mod_bosh, Opts, Acc); transform_module(Host, mod_vcard_xupdate_odbc, Opts, Acc) -> warn_replaced_module(mod_vcard_xupdate_odbc, mod_vcard_xupdate), transform_module(Host, mod_vcard_xupdate, Opts, Acc); transform_module(Host, mod_vcard_ldap, Opts, Acc) -> warn_replaced_module(mod_vcard_ldap, mod_vcard, ldap), transform_module(Host, mod_vcard, [{db_type, ldap}|Opts], Acc); transform_module(Host, M, Opts, Acc) when (M == mod_announce_odbc orelse M == mod_blocking_odbc orelse M == mod_caps_odbc orelse M == mod_last_odbc orelse M == mod_muc_odbc orelse M == mod_offline_odbc orelse M == mod_privacy_odbc orelse M == mod_private_odbc orelse M == mod_pubsub_odbc orelse M == mod_roster_odbc orelse M == mod_shared_roster_odbc orelse M == mod_vcard_odbc) -> M1 = strip_odbc_suffix(M), warn_replaced_module(M, M1, sql), transform_module(Host, M1, [{db_type, sql}|Opts], Acc); transform_module(_Host, mod_blocking, Opts, Acc) -> Opts1 = lists:filter( fun({db_type, _}) -> warn_removed_module_option(db_type, mod_blocking), false; (_) -> true end, Opts), {{mod_blocking, Opts1}, Acc}; transform_module(_Host, mod_carboncopy, Opts, Acc) -> Opts1 = lists:filter( fun({Opt, _}) when Opt == ram_db_type; Opt == use_cache; Opt == cache_size; Opt == cache_missed; Opt == cache_life_time -> warn_removed_module_option(Opt, mod_carboncopy), false; (_) -> true end, Opts), {{mod_carboncopy, Opts1}, Acc}; transform_module(_Host, mod_http_api, Opts, Acc) -> Opts1 = lists:filter( fun({admin_ip_access, _}) -> warn_removed_option(admin_ip_access, api_permissions), false; (_) -> true end, Opts), {{mod_http_api, Opts1}, Acc}; transform_module(_Host, mod_http_upload, Opts, Acc) -> Opts1 = lists:filter( fun({service_url, _}) -> warn_deprecated_option(service_url, external_secret), true; (_) -> true end, Opts), {{mod_http_upload, Opts1}, Acc}; transform_module(_Host, mod_pubsub, Opts, Acc) -> Opts1 = lists:map( fun({plugins, Plugins}) -> {plugins, lists:filter( fun(Plugin) -> case lists:member( Plugin, [<<"buddy">>, <<"club">>, <<"dag">>, <<"dispatch">>, <<"hometree">>, <<"mb">>, <<"mix">>, <<"online">>, <<"private">>, <<"public">>]) of true -> ?WARNING_MSG( "Plugin '~ts' of mod_pubsub is not " "supported anymore and has been " "automatically removed from 'plugins' " "option. ~ts", [Plugin, adjust_hint()]), false; false -> true end end, Plugins)}; (Opt) -> Opt end, Opts), {{mod_pubsub, Opts1}, Acc}; transform_module(_Host, Mod, Opts, Acc) -> {{Mod, Opts}, Acc}. strip_odbc_suffix(M) -> [_|T] = lists:reverse(string:tokens(atom_to_list(M), "_")), list_to_atom(string:join(lists:reverse(T), "_")). %%%=================================================================== %%% Aux %%%=================================================================== filtermapfoldr(Fun, Init, List) -> lists:foldr( fun(X, {Ret, Acc}) -> case Fun(X, Acc) of {true, Acc1} -> {[X|Ret], Acc1}; {{true, X1}, Acc1} -> {[X1|Ret], Acc1}; {false, Acc1} -> {Ret, Acc1} end end, {[], Init}, List). set_certfiles(Y, #{certfiles := CertFiles} = Acc) -> {lists:keystore(certfiles, 1, Y, {certfiles, CertFiles}), Acc}; set_certfiles(Y, Acc) -> {Y, Acc}. %%%=================================================================== %%% Warnings %%%=================================================================== warn_replaced_module(From, To) -> ?WARNING_MSG("Module ~ts is deprecated and was automatically " "replaced by ~ts. ~ts", [From, To, adjust_hint()]). warn_replaced_module(From, To, Type) -> ?WARNING_MSG("Module ~ts is deprecated and was automatically " "replaced by ~ts with db_type: ~ts. ~ts", [From, To, Type, adjust_hint()]). warn_removed_module(Mod) -> ?WARNING_MSG("Module ~ts is deprecated and was automatically " "removed from the configuration. ~ts", [Mod, adjust_hint()]). warn_replaced_handler(Opt, {Path, Module}) -> ?WARNING_MSG("Listening option '~ts' is deprecated " "and was automatically replaced by " "HTTP request handler: \"~ts\" -> ~ts. ~ts", [Opt, Path, Module, adjust_hint()]). warn_deprecated_option(OldOpt, NewOpt) -> ?WARNING_MSG("Option '~ts' is deprecated. Use option '~ts' instead.", [OldOpt, NewOpt]). warn_replaced_option(OldOpt, NewOpt) -> ?WARNING_MSG("Option '~ts' is deprecated and was automatically " "replaced by '~ts'. ~ts", [OldOpt, NewOpt, adjust_hint()]). warn_removed_option(Opt) -> ?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. " "Please remove it from the configuration.", [Opt]). warn_removed_option(OldOpt, NewOpt) -> ?WARNING_MSG("Option '~ts' is deprecated and has no effect anymore. " "Use option '~ts' instead.", [OldOpt, NewOpt]). warn_removed_module_option(Opt, Mod) -> ?WARNING_MSG("Option '~ts' of module ~ts is deprecated " "and has no effect anymore. ~ts", [Opt, Mod, adjust_hint()]). warn_huge_timeout(Opt, T) when is_integer(T), T >= 1000 -> ?WARNING_MSG("Value '~B' of option '~ts' is too big, " "are you sure you have set seconds?", [T, Opt]); warn_huge_timeout(_, _) -> ok. adjust_hint() -> "Please adjust your configuration file accordingly. " "Hint: run `ejabberdctl dump-config` command to view current " "configuration as it is seen by ejabberd.". %%%=================================================================== %%% Very raw validator: just to make sure we get properly typed terms %%% Expand it if you need to transform more options, but don't %%% abuse complex types: simple and composite types are preferred %%%=================================================================== validator() -> Validators = #{s2s_use_starttls => econf:atom(), certfiles => econf:list(econf:any()), c2s_certfile => econf:binary(), s2s_certfile => econf:binary(), domain_certfile => econf:binary(), default_db => econf:atom(), default_ram_db => econf:atom(), auth_method => econf:list_or_single(econf:atom()), acme => econf:options( #{ca_url => econf:binary(), '_' => econf:any()}, [unique]), listen => econf:list( econf:options( #{captcha => econf:bool(), register => econf:bool(), web_admin => econf:bool(), http_bind => econf:bool(), http_poll => econf:bool(), xmlrpc => econf:bool(), module => econf:atom(), certfile => econf:binary(), request_handlers => econf:map(econf:binary(), econf:atom()), '_' => econf:any()}, [])), modules => econf:options( #{'_' => econf:options( #{db_type => econf:atom(), plugins => econf:list(econf:binary()), '_' => econf:any()}, [])}, []), '_' => econf:any()}, econf:options( Validators#{host_config => econf:map(econf:binary(), econf:options(Validators, [])), append_host_config => econf:map(econf:binary(), econf:options(Validators, []))}, []). ejabberd-21.12/src/ejabberd_web.erl0000644000232200023220000000615414154362354017526 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_web.erl %%% Author : Alexey Shchepin %%% Purpose : %%% Purpose : %%% Created : 28 Feb 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_web). -author('alexey@process-one.net'). %% External exports -export([make_xhtml/1, make_xhtml/2, error/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). %% XXX bard: there are variants of make_xhtml in ejabberd_http and %% ejabberd_web_admin. It might be a good idea to centralize it here %% and also create an ejabberd_web.hrl file holding the macros, so %% that third parties can use ejabberd_web as an "utility" library. make_xhtml(Els) -> make_xhtml([], Els). make_xhtml(HeadEls, Els) -> #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}, {<<"xml:lang">>, <<"en">>}, {<<"lang">>, <<"en">>}], children = [#xmlel{name = <<"head">>, attrs = [], children = [#xmlel{name = <<"meta">>, attrs = [{<<"http-equiv">>, <<"Content-Type">>}, {<<"content">>, <<"text/html; charset=utf-8">>}], children = []} | HeadEls]}, #xmlel{name = <<"body">>, attrs = [], children = Els}]}. -define(X(Name), #xmlel{name = Name, attrs = [], children = []}). -define(XA(Name, Attrs), #xmlel{name = Name, attrs = Attrs, children = []}). -define(XE(Name, Els), #xmlel{name = Name, attrs = [], children = Els}). -define(XAE(Name, Attrs, Els), #xmlel{name = Name, attrs = Attrs, children = Els}). -define(C(Text), {xmlcdata, Text}). -define(XC(Name, Text), ?XE(Name, [?C(Text)])). -define(XAC(Name, Attrs, Text), ?XAE(Name, Attrs, [?C(Text)])). -define(LI(Els), ?XE(<<"li">>, Els)). -define(A(URL, Els), ?XAE(<<"a">>, [{<<"href">>, URL}], Els)). -define(AC(URL, Text), ?A(URL, [?C(Text)])). -define(P, ?X(<<"p">>)). -define(BR, ?X(<<"br">>)). -define(INPUT(Type, Name, Value), ?XA(<<"input">>, [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}])). error(not_found) -> {404, [], make_xhtml([?XC(<<"h1">>, <<"404 Not Found">>)])}; error(not_allowed) -> {401, [], make_xhtml([?XC(<<"h1">>, <<"401 Unauthorized">>)])}. ejabberd-21.12/src/ejabberd_router_multicast.erl0000644000232200023220000002330414154362354022352 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_router_multicast.erl %%% Author : Badlop %%% Purpose : Multicast router %%% Created : 11 Aug 2007 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_router_multicast). -author('alexey@process-one.net'). -author('badlop@process-one.net'). -behaviour(gen_server). %% API -export([route_multicast/5, register_route/1, unregister_route/1 ]). -export([start_link/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, update_to_in_wrapped/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -record(route_multicast, {domain = <<"">> :: binary() | '_', pid = self() :: pid()}). -record(state, {}). %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec route_multicast(jid(), binary(), [jid()], stanza(), boolean()) -> ok. route_multicast(From0, Domain0, Destinations0, Packet0, Wrapped0) -> {From, Domain, Destinations, Packet, Wrapped} = ejabberd_hooks:run_fold(multicast_route, Domain0, {From0, Domain0, Destinations0, Packet0, Wrapped0}, []), case catch do_route(Domain, Destinations, xmpp:set_from(Packet, From), Wrapped) of {'EXIT', Reason} -> ?ERROR_MSG("~p~nwhen processing: ~p", [Reason, {From, Domain, Destinations, Packet}]); _ -> ok end. -spec register_route(binary()) -> any(). register_route(Domain) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> Pid = self(), F = fun() -> mnesia:write(#route_multicast{domain = LDomain, pid = Pid}) end, mnesia:transaction(F) end. -spec unregister_route(binary()) -> any(). unregister_route(Domain) -> case jid:nameprep(Domain) of error -> erlang:error({invalid_domain, Domain}); LDomain -> Pid = self(), F = fun() -> case mnesia:select(route_multicast, [{#route_multicast{pid = Pid, domain = LDomain, _ = '_'}, [], ['$_']}]) of [R] -> mnesia:delete_object(R); _ -> ok end end, mnesia:transaction(F) end. %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- init([]) -> ejabberd_mnesia:create(?MODULE, route_multicast, [{ram_copies, [node()]}, {type, bag}, {attributes, record_info(fields, route_multicast)}]), mnesia:subscribe({table, route_multicast, simple}), lists:foreach( fun(Pid) -> erlang:monitor(process, Pid) end, mnesia:dirty_select(route_multicast, [{{route_multicast, '_', '$1'}, [], ['$1']}])), {ok, #state{}}. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({route_multicast, Domain, Destinations, Packet}, State) -> case catch do_route(Domain, Destinations, Packet, false) of {'EXIT', Reason} -> ?ERROR_MSG("~p~nwhen processing: ~p", [Reason, {Domain, Destinations, Packet}]); _ -> ok end, {noreply, State}; handle_info({mnesia_table_event, {write, #route_multicast{pid = Pid}, _ActivityId}}, State) -> erlang:monitor(process, Pid), {noreply, State}; handle_info({'DOWN', _Ref, _Type, Pid, _Info}, State) -> F = fun() -> Es = mnesia:select( route_multicast, [{#route_multicast{pid = Pid, _ = '_'}, [], ['$_']}]), lists:foreach( fun(E) -> mnesia:delete_object(E) end, Es) end, mnesia:transaction(F), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any necessary %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec update_to_in_wrapped(stanza(), jid()) -> stanza(). update_to_in_wrapped(Packet, To) -> case Packet of #message{sub_els = [#ps_event{ items = #ps_items{ items = [#ps_item{ sub_els = [Internal] } = PSItem] } = PSItems } = PSEvent]} -> Internal2 = xmpp:set_to(Internal, To), PSItem2 = PSItem#ps_item{sub_els = [Internal2]}, PSItems2 = PSItems#ps_items{items = [PSItem2]}, PSEvent2 = PSEvent#ps_event{items = PSItems2}, xmpp:set_to(Packet#message{sub_els = [PSEvent2]}, To); _ -> xmpp:set_to(Packet, To) end. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% From = #jid %% Destinations = [#jid] -spec do_route(binary(), [jid()], stanza(), boolean()) -> any(). do_route(Domain, Destinations, Packet, true) -> ?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n", [xmpp:pp(Packet), Domain, str:join([jid:encode(To) || To <- Destinations], <<", ">>)]), lists:foreach( fun(To) -> Packet2 = update_to_in_wrapped(Packet, To), ejabberd_router:route(Packet2) end, Destinations); do_route(Domain, Destinations, Packet, false) -> ?DEBUG("Route multicast:~n~ts~nDomain: ~ts~nDestinations: ~ts~n", [xmpp:pp(Packet), Domain, str:join([jid:encode(To) || To <- Destinations], <<", ">>)]), %% Try to find an appropriate multicast service case mnesia:dirty_read(route_multicast, Domain) of %% If no multicast service is available in this server, send manually [] -> do_route_normal(Destinations, Packet); %% If some is available, send the packet using multicast service Rs when is_list(Rs) -> Pid = pick_multicast_pid(Rs), Pid ! {route_trusted, Destinations, Packet} end. -spec pick_multicast_pid([#route_multicast{}]) -> pid(). pick_multicast_pid(Rs) -> List = case [R || R <- Rs, node(R#route_multicast.pid) == node()] of [] -> Rs; RLocals -> RLocals end, (hd(List))#route_multicast.pid. -spec do_route_normal([jid()], stanza()) -> any(). do_route_normal(Destinations, Packet) -> lists:foreach( fun(To) -> ejabberd_router:route(xmpp:set_to(Packet, To)) end, Destinations). ejabberd-21.12/src/mod_privilege_opt.erl0000644000232200023220000000170414154362354020636 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_privilege_opt). -export([message/1]). -export([presence/1]). -export([roster/1]). -spec message(gen_mod:opts() | global | binary()) -> [{'outgoing','none' | acl:acl()}]. message(Opts) when is_map(Opts) -> gen_mod:get_opt(message, Opts); message(Host) -> gen_mod:get_module_opt(Host, mod_privilege, message). -spec presence(gen_mod:opts() | global | binary()) -> [{'managed_entity','none' | acl:acl()} | {'roster','none' | acl:acl()}]. presence(Opts) when is_map(Opts) -> gen_mod:get_opt(presence, Opts); presence(Host) -> gen_mod:get_module_opt(Host, mod_privilege, presence). -spec roster(gen_mod:opts() | global | binary()) -> [{'both','none' | acl:acl()} | {'get','none' | acl:acl()} | {'set','none' | acl:acl()}]. roster(Opts) when is_map(Opts) -> gen_mod:get_opt(roster, Opts); roster(Host) -> gen_mod:get_module_opt(Host, mod_privilege, roster). ejabberd-21.12/src/mod_shared_roster.erl0000644000232200023220000013247514154362354020644 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_shared_roster.erl %%% Author : Alexey Shchepin %%% Purpose : Shared roster management %%% Created : 5 Mar 2005 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_shared_roster). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, export/1, import_info/0, webadmin_menu/3, webadmin_page/3, get_user_roster/2, get_jid_info/4, import/5, process_item/2, import_start/2, in_subscription/2, out_subscription/1, c2s_self_presence/1, unset_presence/4, register_user/2, remove_user/2, list_groups/1, create_group/2, create_group/3, delete_group/2, get_group_opts/2, set_group_opts/3, get_group_users/2, get_group_explicit_users/2, is_user_in_group/3, add_user_to_group/3, opts_to_binary/1, remove_user_from_group/3, mod_opt_type/1, mod_options/1, mod_doc/0, depends/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("mod_shared_roster.hrl"). -include("translate.hrl"). -type group_options() :: [{atom(), any()}]. -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback list_groups(binary()) -> [binary()]. -callback groups_with_opts(binary()) -> [{binary(), group_options()}]. -callback create_group(binary(), binary(), group_options()) -> {atomic, any()}. -callback delete_group(binary(), binary()) -> {atomic, any()}. -callback get_group_opts(binary(), binary()) -> group_options() | error. -callback set_group_opts(binary(), binary(), group_options()) -> {atomic, any()}. -callback get_user_groups({binary(), binary()}, binary()) -> [binary()]. -callback get_group_explicit_users(binary(), binary()) -> [{binary(), binary()}]. -callback get_user_displayed_groups(binary(), binary(), group_options()) -> [{binary(), group_options()}]. -callback is_user_in_group({binary(), binary()}, binary(), binary()) -> boolean(). -callback add_user_to_group(binary(), {binary(), binary()}, binary()) -> any(). -callback remove_user_from_group(binary(), {binary(), binary()}, binary()) -> {atomic, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). -define(GROUP_OPTS_CACHE, shared_roster_group_opts_cache). -define(USER_GROUPS_CACHE, shared_roster_user_groups_cache). -define(GROUP_EXPLICIT_USERS_CACHE, shared_roster_group_explicit_cache). -define(SPECIAL_GROUPS_CACHE, shared_roster_special_groups_cache). start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 70), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:add(roster_out_subscription, Host, ?MODULE, out_subscription, 30), ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:add(roster_process_item, Host, ?MODULE, process_item, 50), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:add(unset_presence_hook, Host, ?MODULE, unset_presence, 50), ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). stop(Host) -> ejabberd_hooks:delete(webadmin_menu_host, Host, ?MODULE, webadmin_menu, 70), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_user_roster, 70), ejabberd_hooks:delete(roster_in_subscription, Host, ?MODULE, in_subscription, 30), ejabberd_hooks:delete(roster_out_subscription, Host, ?MODULE, out_subscription, 30), ejabberd_hooks:delete(roster_get_jid_info, Host, ?MODULE, get_jid_info, 70), ejabberd_hooks:delete(roster_process_item, Host, ?MODULE, process_item, 50), ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE, unset_presence, 50), ejabberd_hooks:delete(register_user, Host, ?MODULE, register_user, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts), ok. depends(_Host, _Opts) -> []. -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> ets_cache:new(?SPECIAL_GROUPS_CACHE, [{max_size, 4}]), case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?GROUP_OPTS_CACHE, CacheOpts), ets_cache:new(?USER_GROUPS_CACHE, CacheOpts), ets_cache:new(?GROUP_EXPLICIT_USERS_CACHE, CacheOpts); false -> ets_cache:delete(?GROUP_OPTS_CACHE), ets_cache:delete(?USER_GROUPS_CACHE), ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_shared_roster_opt:cache_size(Opts), CacheMissed = mod_shared_roster_opt:cache_missed(Opts), LifeTime = mod_shared_roster_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_shared_roster_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. get_user_roster(Items, {U, S} = US) -> {DisplayedGroups, Cache} = get_user_displayed_groups(US), SRUsers = lists:foldl( fun(Group, Acc1) -> GroupLabel = get_group_label_cached(S, Group, Cache), lists:foldl( fun(User, Acc2) -> if User == US -> Acc2; true -> dict:append(User, GroupLabel, Acc2) end end, Acc1, get_group_users_cached(S, Group, Cache)) end, dict:new(), DisplayedGroups), {NewItems1, SRUsersRest} = lists:mapfoldl( fun(Item, SRUsers1) -> {_, _, {U1, S1, _}} = Item#roster.usj, US1 = {U1, S1}, case dict:find(US1, SRUsers1) of {ok, GroupLabels} -> {Item#roster{subscription = both, groups = Item#roster.groups ++ GroupLabels, ask = none}, dict:erase(US1, SRUsers1)}; error -> {Item, SRUsers1} end end, SRUsers, Items), SRItems = [#roster{usj = {U, S, {U1, S1, <<"">>}}, us = US, jid = {U1, S1, <<"">>}, name = get_rosteritem_name(U1, S1), subscription = both, ask = none, groups = GroupLabels} || {{U1, S1}, GroupLabels} <- dict:to_list(SRUsersRest)], SRItems ++ NewItems1. get_rosteritem_name(U, S) -> case gen_mod:is_loaded(S, mod_vcard) of true -> SubEls = mod_vcard:get_vcard(U, S), get_rosteritem_name_vcard(SubEls); false -> <<"">> end. -spec get_rosteritem_name_vcard([xmlel()]) -> binary(). get_rosteritem_name_vcard([Vcard|_]) -> case fxml:get_path_s(Vcard, [{elem, <<"NICKNAME">>}, cdata]) of <<"">> -> fxml:get_path_s(Vcard, [{elem, <<"FN">>}, cdata]); Nickname -> Nickname end; get_rosteritem_name_vcard(_) -> <<"">>. %% This function rewrites the roster entries when moving or renaming %% them in the user contact list. -spec process_item(#roster{}, binary()) -> #roster{}. process_item(RosterItem, Host) -> USFrom = {UserFrom, ServerFrom} = RosterItem#roster.us, {UserTo, ServerTo, ResourceTo} = RosterItem#roster.jid, NameTo = RosterItem#roster.name, USTo = {UserTo, ServerTo}, {DisplayedGroups, Cache} = get_user_displayed_groups(USFrom), CommonGroups = lists:filter(fun (Group) -> is_user_in_group(USTo, Group, Host) end, DisplayedGroups), case CommonGroups of [] -> RosterItem; %% Roster item cannot be removed: We simply reset the original groups: _ when RosterItem#roster.subscription == remove -> GroupLabels = lists:map(fun (Group) -> get_group_label_cached(Host, Group, Cache) end, CommonGroups), RosterItem#roster{subscription = both, ask = none, groups = GroupLabels}; %% Both users have at least a common shared group, %% So each user can see the other _ -> case lists:subtract(RosterItem#roster.groups, CommonGroups) of %% If it doesn't, then remove this user from any %% existing roster groups. [] -> Pres = #presence{from = jid:make(UserTo, ServerTo), to = jid:make(UserFrom, ServerFrom), type = unsubscribe}, mod_roster:out_subscription(Pres), mod_roster:in_subscription(false, Pres), RosterItem#roster{subscription = both, ask = none}; %% If so, it means the user wants to add that contact %% to his personal roster PersonalGroups -> set_new_rosteritems(UserFrom, ServerFrom, UserTo, ServerTo, ResourceTo, NameTo, PersonalGroups) end end. build_roster_record(User1, Server1, User2, Server2, Name2, Groups) -> USR2 = {User2, Server2, <<"">>}, #roster{usj = {User1, Server1, USR2}, us = {User1, Server1}, jid = USR2, name = Name2, subscription = both, ask = none, groups = Groups}. set_new_rosteritems(UserFrom, ServerFrom, UserTo, ServerTo, ResourceTo, NameTo, GroupsFrom) -> RIFrom = build_roster_record(UserFrom, ServerFrom, UserTo, ServerTo, NameTo, GroupsFrom), set_item(UserFrom, ServerFrom, ResourceTo, RIFrom), JIDTo = jid:make(UserTo, ServerTo), JIDFrom = jid:make(UserFrom, ServerFrom), RITo = build_roster_record(UserTo, ServerTo, UserFrom, ServerFrom, UserFrom, []), set_item(UserTo, ServerTo, <<"">>, RITo), mod_roster:out_subscription( #presence{from = JIDFrom, to = JIDTo, type = subscribe}), mod_roster:in_subscription( false, #presence{to = JIDTo, from = JIDFrom, type = subscribe}), mod_roster:out_subscription( #presence{from = JIDTo, to = JIDFrom, type = subscribed}), mod_roster:in_subscription( false, #presence{to = JIDFrom, from = JIDTo, type = subscribed}), mod_roster:out_subscription( #presence{from = JIDTo, to = JIDFrom, type = subscribe}), mod_roster:in_subscription( false, #presence{to = JIDFrom, from = JIDTo, type = subscribe}), mod_roster:out_subscription( #presence{from = JIDFrom, to = JIDTo, type = subscribed}), mod_roster:in_subscription( false, #presence{to = JIDTo, from = JIDFrom, type = subscribed}), RIFrom. set_item(User, Server, Resource, Item) -> ResIQ = #iq{from = jid:make(User, Server, Resource), to = jid:make(Server), type = set, id = <<"push", (p1_rand:get_string())/binary>>, sub_els = [#roster_query{ items = [mod_roster:encode_item(Item)]}]}, ejabberd_router:route(ResIQ). -spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> {subscription(), ask(), [binary()]}. get_jid_info({Subscription, Ask, Groups}, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jid:tolower(JID), US1 = {U1, S1}, {DisplayedGroups, Cache} = get_user_displayed_groups(US), SRUsers = lists:foldl( fun(Group, Acc1) -> GroupLabel = get_group_label_cached(LServer, Group, Cache), %++ lists:foldl( fun(User1, Acc2) -> dict:append(User1, GroupLabel, Acc2) end, Acc1, get_group_users_cached(LServer, Group, Cache)) end, dict:new(), DisplayedGroups), case dict:find(US1, SRUsers) of {ok, GroupLabels} -> NewGroups = if Groups == [] -> GroupLabels; true -> Groups end, {both, none, NewGroups}; error -> {Subscription, Ask, Groups} end. -spec in_subscription(boolean(), presence()) -> boolean(). in_subscription(Acc, #presence{to = To, from = JID, type = Type}) -> #jid{user = User, server = Server} = To, process_subscription(in, User, Server, JID, Type, Acc). -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = To, type = unsubscribed} = Pres) -> #jid{user = User, server = Server} = From, mod_roster:out_subscription(Pres#presence{type = unsubscribe}), mod_roster:in_subscription(false, xmpp:set_from_to( Pres#presence{type = unsubscribe}, To, From)), process_subscription(out, User, Server, To, unsubscribed, false); out_subscription(#presence{from = From, to = To, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, To, Type, false). process_subscription(Direction, User, Server, JID, _Type, Acc) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, {U1, S1, _} = jid:tolower(jid:remove_resource(JID)), US1 = {U1, S1}, {DisplayedGroups, _} = get_user_displayed_groups(US), SRUsers = lists:usort(lists:flatmap(fun (Group) -> get_group_users(LServer, Group) end, DisplayedGroups)), case lists:member(US1, SRUsers) of true -> case Direction of in -> {stop, false}; out -> stop end; false -> Acc end. list_groups(Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), Mod:list_groups(Host). groups_with_opts(Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), Mod:groups_with_opts(Host). create_group(Host, Group) -> create_group(Host, Group, []). create_group(Host, Group, Opts) -> Mod = gen_mod:db_mod(Host, ?MODULE), case proplists:get_value(all_users, Opts, false) orelse proplists:get_value(online_users, Opts, false) of true -> update_wildcard_cache(Host, Group, Opts); _ -> ok end, case use_cache(Mod, Host) of true -> ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); _ -> ok end, Mod:create_group(Host, Group, Opts). delete_group(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), update_wildcard_cache(Host, Group, []), case use_cache(Mod, Host) of true -> ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), ets_cache:clear(?USER_GROUPS_CACHE, cache_nodes(Mod, Host)), ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); _ -> ok end, Mod:delete_group(Host, Group). get_groups_opts_cached(Host1, Group1, Cache) -> {Host, Group} = split_grouphost(Host1, Group1), Key = {Group, Host}, case Cache of #{Key := Opts} -> {Opts, Cache}; _ -> Opts = get_group_opts_int(Host, Group), {Opts, Cache#{Key => Opts}} end. get_group_opts(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), get_group_opts_int(Host, Group). get_group_opts_int(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), Mod = gen_mod:db_mod(Host, ?MODULE), Res = case use_cache(Mod, Host) of true -> ets_cache:lookup( ?GROUP_OPTS_CACHE, {Host, Group}, fun() -> case Mod:get_group_opts(Host, Group) of error -> error; V -> {cache, V} end end); false -> Mod:get_group_opts(Host, Group) end, case Res of {ok, Opts} -> Opts; error -> error end. set_group_opts(Host, Group, Opts) -> Mod = gen_mod:db_mod(Host, ?MODULE), update_wildcard_cache(Host, Group, Opts), case use_cache(Mod, Host) of true -> ets_cache:delete(?GROUP_OPTS_CACHE, {Host, Group}, cache_nodes(Mod, Host)), ets_cache:insert(?GROUP_OPTS_CACHE, {Host, Group}, Opts, cache_nodes(Mod, Host)); _ -> ok end, Mod:set_group_opts(Host, Group, Opts). get_user_groups(US) -> Host = element(2, US), Mod = gen_mod:db_mod(Host, ?MODULE), UG = case use_cache(Mod, Host) of true -> ets_cache:lookup( ?USER_GROUPS_CACHE, {Host, US}, fun() -> {cache, Mod:get_user_groups(US, Host)} end); false -> Mod:get_user_groups(US, Host) end, UG ++ get_groups_with_wildcards(Host, both). get_group_opt_cached(Host, Group, Opt, Default, Cache) -> case get_groups_opts_cached(Host, Group, Cache) of {error, _} -> Default; {Opts, _} -> proplists:get_value(Opt, Opts, Default) end. %% @spec (Host::string(), Group::string(), Opt::atom(), Default) -> OptValue | Default get_group_opt(Host, Group, Opt, Default) -> case get_group_opts(Host, Group) of error -> Default; Opts -> proplists:get_value(Opt, Opts, Default) end. get_online_users(Host) -> lists:usort([{U, S} || {U, S, _} <- ejabberd_sm:get_vh_session_list(Host)]). get_group_users_cached(Host, Group, Cache) -> {Opts, _} = get_groups_opts_cached(Host, Group, Cache), get_group_users(Host, Group, Opts). get_group_users(Host1, Group1) -> {Host, Group} = split_grouphost(Host1, Group1), get_group_users(Host, Group, get_group_opts(Host, Group)). get_group_users(Host, Group, GroupOpts) -> case proplists:get_value(all_users, GroupOpts, false) of true -> ejabberd_auth:get_users(Host); false -> [] end ++ case proplists:get_value(online_users, GroupOpts, false) of true -> get_online_users(Host); false -> [] end ++ get_group_explicit_users(Host, Group). get_group_explicit_users(Host, Group) -> Mod = gen_mod:db_mod(Host, ?MODULE), case use_cache(Mod, Host) of true -> ets_cache:lookup( ?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, fun() -> {cache, Mod:get_group_explicit_users(Host, Group)} end); false -> Mod:get_group_explicit_users(Host, Group) end. get_group_label_cached(Host, Group, Cache) -> get_group_opt_cached(Host, Group, label, Group, Cache). -spec update_wildcard_cache(binary(), binary(), list()) -> ok. update_wildcard_cache(Host, Group, NewOpts) -> Mod = gen_mod:db_mod(Host, ?MODULE), Online = get_groups_with_wildcards(Host, online), Both = get_groups_with_wildcards(Host, both), IsOnline = proplists:get_value(online_users, NewOpts, false), IsAll = proplists:get_value(all_users, NewOpts, false), OnlineUpdated = lists:member(Group, Online) /= IsOnline, BothUpdated = lists:member(Group, Both) /= (IsOnline orelse IsAll), if OnlineUpdated -> NewOnline = case IsOnline of true -> [Group | Online]; _ -> Online -- [Group] end, ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, online}, {ok, NewOnline}, fun() -> ok end, cache_nodes(Mod, Host)); true -> ok end, if BothUpdated -> NewBoth = case IsOnline orelse IsAll of true -> [Group | Both]; _ -> Both -- [Group] end, ets_cache:update(?SPECIAL_GROUPS_CACHE, {Host, both}, {ok, NewBoth}, fun() -> ok end, cache_nodes(Mod, Host)); true -> ok end, ok. -spec get_groups_with_wildcards(binary(), online | both) -> list(binary()). get_groups_with_wildcards(Host, Type) -> Res = ets_cache:lookup( ?SPECIAL_GROUPS_CACHE, {Host, Type}, fun() -> Res = lists:filtermap( fun({Group, Opts}) -> case proplists:get_value(online_users, Opts, false) orelse (Type == both andalso proplists:get_value(all_users, Opts, false)) of true -> {true, Group}; false -> false end end, groups_with_opts(Host)), {cache, {ok, Res}} end), case Res of {ok, List} -> List; _ -> [] end. %% Given two lists of groupnames and their options, %% return the list of displayed groups to the second list displayed_groups(GroupsOpts, SelectedGroupsOpts) -> DisplayedGroups = lists:usort(lists:flatmap( fun ({_Group, Opts}) -> [G || G <- proplists:get_value(displayed_groups, Opts, []), not lists:member(disabled, Opts)] end, SelectedGroupsOpts)), [G || G <- DisplayedGroups, not lists:member(disabled, proplists:get_value(G, GroupsOpts, []))]. %% Given a list of group names with options, %% for those that have @all@ in memberlist, %% get the list of groups displayed get_special_displayed_groups(GroupsOpts) -> Groups = lists:filter(fun ({_Group, Opts}) -> proplists:get_value(all_users, Opts, false) end, GroupsOpts), displayed_groups(GroupsOpts, Groups). %% Given a username and server, and a list of group names with options, %% for the list of groups of that server that user is member %% get the list of groups displayed get_user_displayed_groups(LUser, LServer, GroupsOpts) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Groups = Mod:get_user_displayed_groups(LUser, LServer, GroupsOpts), displayed_groups(GroupsOpts, Groups). %% @doc Get the list of groups that are displayed to this user get_user_displayed_groups(US) -> Host = element(2, US), {Groups, Cache} = lists:foldl( fun(Group, {Groups, Cache}) -> case get_groups_opts_cached(Host, Group, Cache) of {error, Cache2} -> {Groups, Cache2}; {Opts, Cache3} -> case lists:member(disabled, Opts) of false -> {proplists:get_value(displayed_groups, Opts, []) ++ Groups, Cache3}; _ -> {Groups, Cache3} end end end, {[], #{}}, get_user_groups(US)), lists:foldl( fun(Group, {Groups0, Cache0}) -> case get_groups_opts_cached(Host, Group, Cache0) of {error, Cache1} -> {Groups0, Cache1}; {Opts, Cache2} -> case lists:member(disabled, Opts) of false -> {[Group|Groups0], Cache2}; _ -> {Groups0, Cache2} end end end, {[], Cache}, lists:usort(Groups)). is_user_in_group(US, Group, Host) -> Mod = gen_mod:db_mod(Host, ?MODULE), case Mod:is_user_in_group(US, Group, Host) of false -> lists:member(US, get_group_users(Host, Group)); true -> true end. %% @spec (Host::string(), {User::string(), Server::string()}, Group::string()) -> {atomic, ok} | error add_user_to_group(Host, US, Group) -> {_LUser, LServer} = US, case lists:member(LServer, ejabberd_config:get_option(hosts)) of true -> add_user_to_group2(Host, US, Group); false -> ?INFO_MSG("Attempted adding to shared roster user of inexistent vhost ~ts", [LServer]), error end. add_user_to_group2(Host, US, Group) -> {LUser, LServer} = US, case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> GroupOpts = get_group_opts(Host, Group), MoreGroupOpts = case LUser of <<"@all@">> -> [{all_users, true}]; <<"@online@">> -> [{online_users, true}]; _ -> [] end, set_group_opts(Host, Group, GroupOpts ++ MoreGroupOpts); nomatch -> DisplayedToGroups = displayed_to_groups(Group, Host), DisplayedGroups = get_displayed_groups(Group, LServer), push_user_to_displayed(LUser, LServer, Group, Host, both, DisplayedToGroups), push_displayed_to_user(LUser, LServer, Host, both, DisplayedGroups), Mod = gen_mod:db_mod(Host, ?MODULE), Mod:add_user_to_group(Host, US, Group), case use_cache(Mod, Host) of true -> ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); false -> ok end end. get_displayed_groups(Group, LServer) -> get_group_opt(LServer, Group, displayed_groups, []). push_displayed_to_user(LUser, LServer, Host, Subscription, DisplayedGroups) -> [push_members_to_user(LUser, LServer, DGroup, Host, Subscription) || DGroup <- DisplayedGroups]. remove_user_from_group(Host, US, Group) -> {LUser, LServer} = US, case ejabberd_regexp:run(LUser, <<"^@.+@\$">>) of match -> GroupOpts = get_group_opts(Host, Group), NewGroupOpts = case LUser of <<"@all@">> -> lists:filter(fun (X) -> X /= {all_users, true} end, GroupOpts); <<"@online@">> -> lists:filter(fun (X) -> X /= {online_users, true} end, GroupOpts) end, set_group_opts(Host, Group, NewGroupOpts); nomatch -> Mod = gen_mod:db_mod(Host, ?MODULE), Result = Mod:remove_user_from_group(Host, US, Group), case use_cache(Mod, Host) of true -> ets_cache:delete(?USER_GROUPS_CACHE, {Host, US}, cache_nodes(Mod, Host)), ets_cache:delete(?GROUP_EXPLICIT_USERS_CACHE, {Host, Group}, cache_nodes(Mod, Host)); false -> ok end, DisplayedToGroups = displayed_to_groups(Group, Host), DisplayedGroups = get_displayed_groups(Group, LServer), push_user_to_displayed(LUser, LServer, Group, Host, remove, DisplayedToGroups), push_displayed_to_user(LUser, LServer, Host, remove, DisplayedGroups), Result end. push_members_to_user(LUser, LServer, Group, Host, Subscription) -> GroupOpts = get_group_opts(LServer, Group), GroupLabel = proplists:get_value(label, GroupOpts, Group), %++ Members = get_group_users(Host, Group), lists:foreach(fun ({U, S}) -> N = get_rosteritem_name(U, S), push_roster_item(LUser, LServer, U, S, N, GroupLabel, Subscription) end, Members). -spec register_user(binary(), binary()) -> ok. register_user(User, Server) -> Groups = get_user_groups({User, Server}), [push_user_to_displayed(User, Server, Group, Server, both, displayed_to_groups(Group, Server)) || Group <- Groups], ok. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> push_user_to_members(User, Server, remove). push_user_to_members(User, Server, Subscription) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), RosterName = get_rosteritem_name(LUser, LServer), GroupsOpts = groups_with_opts(LServer), SpecialGroups = get_special_displayed_groups(GroupsOpts), UserGroups = get_user_displayed_groups(LUser, LServer, GroupsOpts), lists:foreach(fun (Group) -> remove_user_from_group(LServer, {LUser, LServer}, Group), GroupOpts = proplists:get_value(Group, GroupsOpts, []), GroupLabel = proplists:get_value(label, GroupOpts, Group), lists:foreach(fun ({U, S}) -> push_roster_item(U, S, LUser, LServer, RosterName, GroupLabel, Subscription) end, get_group_users(LServer, Group, GroupOpts)) end, lists:usort(SpecialGroups ++ UserGroups)). push_user_to_displayed(LUser, LServer, Group, Host, Subscription, DisplayedToGroupsOpts) -> GroupLabel = get_group_opt(Host, Group, label, Group), %++ [push_user_to_group(LUser, LServer, GroupD, Host, GroupLabel, Subscription) || GroupD <- DisplayedToGroupsOpts]. push_user_to_group(LUser, LServer, Group, Host, GroupLabel, Subscription) -> RosterName = get_rosteritem_name(LUser, LServer), lists:foreach(fun ({U, S}) when (U == LUser) and (S == LServer) -> ok; ({U, S}) -> case lists:member(S, ejabberd_option:hosts()) of true -> push_roster_item(U, S, LUser, LServer, RosterName, GroupLabel, Subscription); _ -> ok end end, get_group_users(Host, Group)). %% Get list of groups to which this group is displayed displayed_to_groups(GroupName, LServer) -> GroupsOpts = groups_with_opts(LServer), Gs = lists:filter(fun ({_Group, Opts}) -> lists:member(GroupName, proplists:get_value(displayed_groups, Opts, [])) end, GroupsOpts), [Name || {Name, _} <- Gs]. push_item(User, Server, Item) -> mod_roster:push_item(jid:make(User, Server), Item#roster{subscription = none}, Item). push_roster_item(User, Server, ContactU, ContactS, ContactN, GroupLabel, Subscription) -> Item = #roster{usj = {User, Server, {ContactU, ContactS, <<"">>}}, us = {User, Server}, jid = {ContactU, ContactS, <<"">>}, name = ContactN, subscription = Subscription, ask = none, groups = [GroupLabel]}, push_item(User, Server, Item). -spec c2s_self_presence({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. c2s_self_presence(Acc) -> Acc. -spec unset_presence(binary(), binary(), binary(), binary()) -> ok. unset_presence(LUser, LServer, Resource, Status) -> Resources = ejabberd_sm:get_user_resources(LUser, LServer), ?DEBUG("Unset_presence for ~p @ ~p / ~p -> ~p " "(~p resources)", [LUser, LServer, Resource, Status, length(Resources)]), case length(Resources) of 0 -> lists:foreach( fun(OG) -> DisplayedToGroups = displayed_to_groups(OG, LServer), push_user_to_displayed(LUser, LServer, OG, LServer, remove, DisplayedToGroups), push_displayed_to_user(LUser, LServer, LServer, remove, DisplayedToGroups) end, get_groups_with_wildcards(LServer, online)); _ -> ok end. %%--------------------- %% Web Admin %%--------------------- webadmin_menu(Acc, _Host, Lang) -> [{<<"shared-roster">>, translate:translate(Lang, ?T("Shared Roster Groups"))} | Acc]. webadmin_page(_, Host, #request{us = _US, path = [<<"shared-roster">>], q = Query, lang = Lang} = _Request) -> Res = list_shared_roster_groups(Host, Query, Lang), {stop, Res}; webadmin_page(_, Host, #request{us = _US, path = [<<"shared-roster">>, Group], q = Query, lang = Lang} = _Request) -> Res = shared_roster_group(Host, Group, Query, Lang), {stop, Res}; webadmin_page(Acc, _, _) -> Acc. list_shared_roster_groups(Host, Query, Lang) -> Res = list_sr_groups_parse_query(Host, Query), SRGroups = list_groups(Host), FGroups = (?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?X(<<"td">>), ?XE(<<"td">>, [?CT(?T("Name:"))]) ])]++ (lists:map(fun (Group) -> ?XE(<<"tr">>, [?XE(<<"td">>, [?INPUT(<<"checkbox">>, <<"selected">>, Group)]), ?XE(<<"td">>, [?AC(<>, Group)])]) end, lists:sort(SRGroups)) ++ [?XE(<<"tr">>, [?X(<<"td">>), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"namenew">>, <<"">>), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"addnew">>, ?T("Add New"))])])]))])), (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))), <<"modules/#mod-shared-roster">>, <<"mod_shared_roster">>)) ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [FGroups, ?BR, ?INPUTTD(<<"submit">>, <<"delete">>, ?T("Delete Selected"))])]. list_sr_groups_parse_query(Host, Query) -> case lists:keysearch(<<"addnew">>, 1, Query) of {value, _} -> list_sr_groups_parse_addnew(Host, Query); _ -> case lists:keysearch(<<"delete">>, 1, Query) of {value, _} -> list_sr_groups_parse_delete(Host, Query); _ -> nothing end end. list_sr_groups_parse_addnew(Host, Query) -> case lists:keysearch(<<"namenew">>, 1, Query) of {value, {_, Group}} when Group /= <<"">> -> create_group(Host, Group), ok; _ -> error end. list_sr_groups_parse_delete(Host, Query) -> SRGroups = list_groups(Host), lists:foreach(fun (Group) -> case lists:member({<<"selected">>, Group}, Query) of true -> delete_group(Host, Group); _ -> ok end end, SRGroups), ok. shared_roster_group(Host, Group, Query, Lang) -> Res = shared_roster_group_parse_query(Host, Group, Query), GroupOpts = get_group_opts(Host, Group), Label = get_opt(GroupOpts, label, <<"">>), %%++ Description = get_opt(GroupOpts, description, <<"">>), AllUsers = get_opt(GroupOpts, all_users, false), OnlineUsers = get_opt(GroupOpts, online_users, false), DisplayedGroups = get_opt(GroupOpts, displayed_groups, []), Members = get_group_explicit_users(Host, Group), FMembers = iolist_to_binary( [if AllUsers -> <<"@all@\n">>; true -> <<"">> end, if OnlineUsers -> <<"@online@\n">>; true -> <<"">> end, [[us_to_list(Member), $\n] || Member <- Members]]), FDisplayedGroups = [<> || DG <- DisplayedGroups], DescNL = length(ejabberd_regexp:split(Description, <<"\n">>)), FGroup = (?XAE(<<"table">>, [{<<"class">>, <<"withtextareas">>}], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Name:")), ?XE(<<"td">>, [?C(Group)]), ?XE(<<"td">>, [?C(<<"">>)])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Label:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"label">>, Label)]), ?XE(<<"td">>, [?CT(?T("Name in the rosters where this group will be displayed"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Description:")), ?XE(<<"td">>, [?TEXTAREA(<<"description">>, integer_to_binary(lists:max([3, DescNL])), <<"20">>, Description)]), ?XE(<<"td">>, [?CT(?T("Only admins can see this"))]) ]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Members:")), ?XE(<<"td">>, [?TEXTAREA(<<"members">>, integer_to_binary(lists:max([3, length(Members)+3])), <<"20">>, FMembers)]), ?XE(<<"td">>, [?C(<<"JIDs, @all@, @online@">>)]) ]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Displayed:")), ?XE(<<"td">>, [?TEXTAREA(<<"dispgroups">>, integer_to_binary(lists:max([3, length(FDisplayedGroups)])), <<"20">>, list_to_binary(FDisplayedGroups))]), ?XE(<<"td">>, [?CT(?T("Groups that will be displayed to the members"))]) ])])])), (?H1GL((translate:translate(Lang, ?T("Shared Roster Groups"))), <<"modules/#mod-shared-roster">>, <<"mod_shared_roster">>)) ++ [?XC(<<"h2">>, translate:translate(Lang, ?T("Group")))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; {error_elements, NonAddedList1, NG1} -> make_error_el(Lang, ?T("Members not added (inexistent vhost!): "), [jid:encode({U,S,<<>>}) || {U,S} <- NonAddedList1]) ++ make_error_el(Lang, ?T("'Displayed groups' not added (they do not exist!): "), NG1); error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [FGroup, ?BR, ?INPUTT(<<"submit">>, <<"submit">>, ?T("Submit"))])]. make_error_el(_, _, []) -> []; make_error_el(Lang, Message, BinList) -> NG2 = str:join(BinList, <<", ">>), NG3 = translate:translate(Lang, Message), NG4 = str:concat(NG3, NG2), [?XRES(NG4)]. shared_roster_group_parse_query(Host, Group, Query) -> case lists:keysearch(<<"submit">>, 1, Query) of {value, _} -> {value, {_, Label}} = lists:keysearch(<<"label">>, 1, Query), %++ {value, {_, Description}} = lists:keysearch(<<"description">>, 1, Query), {value, {_, SMembers}} = lists:keysearch(<<"members">>, 1, Query), {value, {_, SDispGroups}} = lists:keysearch(<<"dispgroups">>, 1, Query), LabelOpt = if Label == <<"">> -> []; true -> [{label, Label}] %++ end, DescriptionOpt = if Description == <<"">> -> []; true -> [{description, Description}] end, DispGroups1 = str:tokens(SDispGroups, <<"\r\n">>), {DispGroups, WrongDispGroups} = filter_groups_existence(Host, DispGroups1), DispGroupsOpt = if DispGroups == [] -> []; true -> [{displayed_groups, DispGroups}] end, OldMembers = get_group_explicit_users(Host, Group), SJIDs = str:tokens(SMembers, <<", \r\n">>), NewMembers = lists:foldl(fun (_SJID, error) -> error; (SJID, USs) -> case SJID of <<"@all@">> -> USs; <<"@online@">> -> USs; _ -> try jid:decode(SJID) of JID -> [{JID#jid.luser, JID#jid.lserver} | USs] catch _:{bad_jid, _} -> error end end end, [], SJIDs), AllUsersOpt = case lists:member(<<"@all@">>, SJIDs) of true -> [{all_users, true}]; false -> [] end, OnlineUsersOpt = case lists:member(<<"@online@">>, SJIDs) of true -> [{online_users, true}]; false -> [] end, CurrentDisplayedGroups = get_displayed_groups(Group, Host), AddedDisplayedGroups = DispGroups -- CurrentDisplayedGroups, RemovedDisplayedGroups = CurrentDisplayedGroups -- DispGroups, displayed_groups_update(OldMembers, RemovedDisplayedGroups, remove), displayed_groups_update(OldMembers, AddedDisplayedGroups, both), set_group_opts(Host, Group, LabelOpt ++ DispGroupsOpt ++ DescriptionOpt ++ AllUsersOpt ++ OnlineUsersOpt), if NewMembers == error -> error; true -> AddedMembers = NewMembers -- OldMembers, RemovedMembers = OldMembers -- NewMembers, lists:foreach( fun(US) -> remove_user_from_group(Host, US, Group) end, RemovedMembers), NonAddedMembers = lists:filter( fun(US) -> error == add_user_to_group(Host, US, Group) end, AddedMembers), case (NonAddedMembers /= []) or (WrongDispGroups /= []) of true -> {error_elements, NonAddedMembers, WrongDispGroups}; false -> ok end end; _ -> nothing end. get_opt(Opts, Opt, Default) -> case lists:keysearch(Opt, 1, Opts) of {value, {_, Val}} -> Val; false -> Default end. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). split_grouphost(Host, Group) -> case str:tokens(Group, <<"@">>) of [GroupName, HostName] -> {HostName, GroupName}; [_] -> {Host, Group} end. filter_groups_existence(Host, Groups) -> lists:partition( fun(Group) -> error /= get_group_opts(Host, Group) end, Groups). displayed_groups_update(Members, DisplayedGroups, Subscription) -> lists:foreach( fun({U, S}) -> push_displayed_to_user(U, S, S, Subscription, DisplayedGroups) end, Members). opts_to_binary(Opts) -> lists:map( fun({label, Label}) -> {label, iolist_to_binary(Label)}; ({name, Label}) -> % For SQL backwards compat with ejabberd 20.03 and older {label, iolist_to_binary(Label)}; ({description, Desc}) -> {description, iolist_to_binary(Desc)}; ({displayed_groups, Gs}) -> {displayed_groups, [iolist_to_binary(G) || G <- Gs]}; (Opt) -> Opt end, Opts). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"sr_group">>, 3}, {<<"sr_user">>, 3}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module enables you to create shared roster groups: " "groups of accounts that can see members from (other) groups " "in their rosters."), "", ?T("The big advantages of this feature are that end users do not " "need to manually add all users to their rosters, and that they " "cannot permanently delete users from the shared roster groups. " "A shared roster group can have members from any XMPP server, " "but the presence will only be available from and to members of " "the same virtual host where the group is created. It still " "allows the users to have / add their own contacts, as it does " "not replace the standard roster. Instead, the shared roster " "contacts are merged to the relevant users at retrieval time. " "The standard user rosters thus stay unmodified."), "", ?T("Shared roster groups can be edited via the Web Admin, " "and some API commands called 'srg_*'. " "Each group has a unique name and those parameters:"), "", ?T("- Label: Used in the rosters where this group is displayed."),"", ?T("- Description: of the group, which has no effect."), "", ?T("- Members: A list of JIDs of group members, entered one per " "line in the Web Admin. The special member directive '@all@' " "represents all the registered users in the virtual host; " "which is only recommended for a small server with just a few " "hundred users. The special member directive '@online@' " "represents the online users in the virtual host. With those " "two directives, the actual list of members in those shared " "rosters is generated dynamically at retrieval time."), "", ?T("- Displayed: A list of groups that will be in the " "rosters of this group's members. A group of other vhost can " "be identified with 'groupid@vhost'."), "", ?T("This module depends on _`mod_roster`_. " "If not enabled, roster queries will return 503 errors.")], opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Define the type of storage where the module will create " "the tables and store user information. The default is " "the storage defined by the top-level _`default_db`_ option, " "or 'mnesia' if omitted. If 'sql' value is defined, " "make sure you have defined the database.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => [{?T("Take the case of a computer club that wants all its members " "seeing each other in their rosters. To achieve this, they " "need to create a shared roster group similar to this one:"), ["Name: club_members", "Label: Club Members", "Description: Members from the computer club", "Members: member1@example.org, member2@example.org, member3@example.org", "Displayed Groups: club_members"]}, {?T("In another case we have a company which has three divisions: " "Management, Marketing and Sales. All group members should see " "all other members in their rosters. Additionally, all managers " "should have all marketing and sales people in their roster. " "Simultaneously, all marketeers and the whole sales team " "should see all managers. This scenario can be achieved by " "creating shared roster groups as shown in the following lists:"), ["First list:", "Name: management", "Label: Management", "Description: Management", "Members: manager1@example.org, manager2@example.org", "Displayed: management, marketing, sales", "", "Second list:", "Name: marketing", "Label: Marketing", "Description: Marketing", "Members: marketeer1@example.org, marketeer2@example.org, marketeer3@example.org", "Displayed: management, marketing", "", "Third list:", "Name: sales", "Label: Sales", "Description: Sales", "Members: salesman1@example.org, salesman2@example.org, salesman3@example.org", "Displayed: management, sales" ]} ]}. ejabberd-21.12/src/mod_mix_pam_opt.erl0000644000232200023220000000256114154362354020304 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_mix_pam_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, db_type). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mix_pam, use_cache). ejabberd-21.12/src/mod_bosh_mnesia.erl0000644000232200023220000001762614154362354020267 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 12 Jan 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_bosh_mnesia). -behaviour(gen_server). -behaviour(mod_bosh). %% mod_bosh API -export([init/0, open_session/2, close_session/1, find_session/1, use_cache/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -define(CALL_TIMEOUT, timer:minutes(10)). -record(bosh, {sid = <<"">> :: binary(), timestamp = erlang:timestamp() :: erlang:timestamp(), pid = self() :: pid()}). -record(state, {nodes = #{} :: #{node() => {pid(), reference()}}}). -type state() :: #state{}. %%%=================================================================== %%% API %%%=================================================================== -spec init() -> ok | {error, any()}. init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; {error, {already_started, _}} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). use_cache() -> false. -spec open_session(binary(), pid()) -> ok. open_session(SID, Pid) -> Session = #bosh{sid = SID, timestamp = erlang:timestamp(), pid = Pid}, gen_server:call(?MODULE, {write, Session}, ?CALL_TIMEOUT). -spec close_session(binary()) -> ok. close_session(SID) -> case mnesia:dirty_read(bosh, SID) of [Session] -> gen_server:call(?MODULE, {delete, Session}, ?CALL_TIMEOUT); [] -> ok end. -spec find_session(binary()) -> {ok, pid()} | {error, notfound}. find_session(SID) -> case mnesia:dirty_read(bosh, SID) of [#bosh{pid = Pid}] -> {ok, Pid}; [] -> {error, notfound} end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -spec init([]) -> {ok, state()}. init([]) -> setup_database(), multicast({join, node(), self()}), mnesia:subscribe(system), {ok, #state{}}. -spec handle_call(_, _, state()) -> {reply, ok, state()} | {noreply, state()}. handle_call({write, Session} = Msg, _From, State) -> write_session(Session), multicast(Msg), {reply, ok, State}; handle_call({delete, Session} = Msg, _From, State) -> delete_session(Session), multicast(Msg), {reply, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(_, state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -spec handle_info(_, state()) -> {noreply, state()}. handle_info({write, Session}, State) -> write_session(Session), {noreply, State}; handle_info({delete, Session}, State) -> delete_session(Session), {noreply, State}; handle_info({join, Node, Pid}, State) -> ejabberd_cluster:send(Pid, {joined, node(), self()}), case maps:find(Node, State#state.nodes) of {ok, {Pid, _}} -> ok; _ -> ejabberd_cluster:send(Pid, {join, node(), self()}) end, {noreply, State}; handle_info({joined, Node, Pid}, State) -> case maps:find(Node, State#state.nodes) of {ok, {Pid, _}} -> {noreply, State}; Ret -> MRef = erlang:monitor(process, {?MODULE, Node}), Nodes = maps:put(Node, {Pid, MRef}, State#state.nodes), case Ret of error -> ejabberd_cluster:send(Pid, {first, self()}); _ -> ok end, {noreply, State#state{nodes = Nodes}} end; handle_info({first, From}, State) -> ejabberd_cluster:send(From, {replica, node(), first_session()}), {noreply, State}; handle_info({next, From, Key}, State) -> ejabberd_cluster:send(From, {replica, node(), next_session(Key)}), {noreply, State}; handle_info({replica, _From, '$end_of_table'}, State) -> {noreply, State}; handle_info({replica, From, Session}, State) -> write_session(Session), ejabberd_cluster:send(From, {next, self(), Session#bosh.sid}), {noreply, State}; handle_info({'DOWN', _, process, {?MODULE, _}, _Info}, State) -> {noreply, State}; handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> Sessions = ets:select( bosh, ets:fun2ms( fun(#bosh{pid = Pid} = S) when node(Pid) == Node -> S end)), lists:foreach( fun(S) -> mnesia:dirty_delete_object(S) end, Sessions), Nodes = maps:remove(Node, State#state.nodes), {noreply, State#state{nodes = Nodes}}; handle_info({mnesia_system_event, _}, State) -> {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec write_session(#bosh{}) -> ok. write_session(#bosh{pid = Pid1, sid = SID, timestamp = T1} = S1) -> case mnesia:dirty_read(bosh, SID) of [#bosh{pid = Pid2, timestamp = T2} = S2] -> if Pid1 == Pid2 -> mnesia:dirty_write(S1); T1 < T2 -> ejabberd_cluster:send(Pid2, replaced), mnesia:dirty_write(S1); true -> ejabberd_cluster:send(Pid1, replaced), mnesia:dirty_write(S2) end; [] -> mnesia:dirty_write(S1) end. -spec delete_session(#bosh{}) -> ok. delete_session(#bosh{sid = SID, pid = Pid1}) -> case mnesia:dirty_read(bosh, SID) of [#bosh{pid = Pid2}] -> if Pid1 == Pid2 -> mnesia:dirty_delete(bosh, SID); true -> ok end; [] -> ok end. -spec multicast(_) -> ok. multicast(Msg) -> lists:foreach( fun(Node) when Node /= node() -> ejabberd_cluster:send({?MODULE, Node}, Msg); (_) -> ok end, ejabberd_cluster:get_nodes()). setup_database() -> case catch mnesia:table_info(bosh, attributes) of [sid, pid] -> mnesia:delete_table(bosh); _ -> ok end, ejabberd_mnesia:create(?MODULE, bosh, [{ram_copies, [node()]}, {local_content, true}, {attributes, record_info(fields, bosh)}]). -spec first_session() -> #bosh{} | '$end_of_table'. first_session() -> case mnesia:dirty_first(bosh) of '$end_of_table' -> '$end_of_table'; First -> read_session(First) end. -spec next_session(binary()) -> #bosh{} | '$end_of_table'. next_session(Prev) -> case mnesia:dirty_next(bosh, Prev) of '$end_of_table' -> '$end_of_table'; Next -> read_session(Next) end. -spec read_session(binary()) -> #bosh{} | '$end_of_table'. read_session(Key) -> case mnesia:dirty_read(bosh, Key) of [#bosh{pid = Pid} = Session] when node(Pid) == node() -> Session; _ -> next_session(Key) end. ejabberd-21.12/src/mod_delegation_opt.erl0000644000232200023220000000057214154362354020765 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_delegation_opt). -export([namespaces/1]). -spec namespaces(gen_mod:opts() | global | binary()) -> [{binary(),[binary()],acl:acl()}]. namespaces(Opts) when is_map(Opts) -> gen_mod:get_opt(namespaces, Opts); namespaces(Host) -> gen_mod:get_module_opt(Host, mod_delegation, namespaces). ejabberd-21.12/src/xml_compress.erl0000644000232200023220000011403714154362354017646 0ustar debalancedebalance-module(xml_compress). -export([encode/3, decode/3]). % This file was generated by xml_compress_gen % % Rules used: % % [{<<"eu.siacs.conversations.axolotl">>,<<"key">>, % [{<<"prekey">>,[<<"true">>]},{<<"rid">>,[]}], % []}, % {<<"jabber:client">>,<<"message">>, % [{<<"from">>,[j2,{j1}]}, % {<<"id">>,[]}, % {<<"to">>,[j1,j2,{j1}]}, % {<<"type">>,[<<"chat">>,<<"groupchat">>,<<"normal">>]}, % {<<"xml:lang">>,[<<"en">>]}], % []}, % {<<"urn:xmpp:hints">>,<<"store">>,[],[]}, % {<<"jabber:client">>,<<"body">>,[], % [<<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69,77,79,32,101, % 110,99,114,121,112,116,101,100,32,109,101,115,115,97,103,101,32,98,117, % 116,32,121,111,117,114,32,99,108,105,101,110,116,32,100,111,101,115,110, % 226,128,153,116,32,115,101,101,109,32,116,111,32,115,117,112,112,111, % 114,116,32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32, % 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104,116,116, % 112,115,58,47,47,99,111,110,118,101,114,115,97,116,105,111,110,115,46, % 105,109,47,111,109,101,109,111>>]}, % {<<"urn:xmpp:sid:0">>,<<"origin-id">>,[{<<"id">>,[]}],[]}, % {<<"urn:xmpp:chat-markers:0">>,<<"markable">>,[],[]}, % {<<"eu.siacs.conversations.axolotl">>,<<"encrypted">>,[],[]}, % {<<"eu.siacs.conversations.axolotl">>,<<"header">>,[{<<"sid">>,[]}],[]}, % {<<"eu.siacs.conversations.axolotl">>,<<"iv">>,[],[]}, % {<<"eu.siacs.conversations.axolotl">>,<<"payload">>,[],[]}, % {<<"urn:xmpp:eme:0">>,<<"encryption">>, % [{<<"name">>,[<<"OMEMO">>]}, % {<<"namespace">>,[<<"eu.siacs.conversations.axolotl">>]}], % []}, % {<<"urn:xmpp:delay">>,<<"delay">>,[{<<"from">>,[j1]},{<<"stamp">>,[]}],[]}, % {<<"http://jabber.org/protocol/address">>,<<"address">>, % [{<<"jid">>,[{j1}]},{<<"type">>,[<<"ofrom">>]}], % []}, % {<<"http://jabber.org/protocol/address">>,<<"addresses">>,[],[]}, % {<<"urn:xmpp:chat-markers:0">>,<<"displayed">>, % [{<<"id">>,[]},{<<"sender">>,[{j1},{j2}]}], % []}, % {<<"urn:xmpp:mam:tmp">>,<<"archived">>,[{<<"by">>,[]},{<<"id">>,[]}],[]}, % {<<"urn:xmpp:sid:0">>,<<"stanza-id">>,[{<<"by">>,[]},{<<"id">>,[]}],[]}, % {<<"urn:xmpp:receipts">>,<<"request">>,[],[]}, % {<<"urn:xmpp:chat-markers:0">>,<<"received">>,[{<<"id">>,[]}],[]}, % {<<"urn:xmpp:receipts">>,<<"received">>,[{<<"id">>,[]}],[]}, % {<<"http://jabber.org/protocol/chatstates">>,<<"active">>,[],[]}, % {<<"http://jabber.org/protocol/muc#user">>,<<"invite">>, % [{<<"from">>,[{j1}]}], % []}, % {<<"http://jabber.org/protocol/muc#user">>,<<"reason">>,[],[]}, % {<<"http://jabber.org/protocol/muc#user">>,<<"x">>,[],[]}, % {<<"jabber:x:conference">>,<<"x">>,[{<<"jid">>,[j2]}],[]}, % {<<"jabber:client">>,<<"subject">>,[],[]}, % {<<"jabber:client">>,<<"thread">>,[],[]}, % {<<"http://jabber.org/protocol/pubsub#event">>,<<"event">>,[],[]}, % {<<"http://jabber.org/protocol/pubsub#event">>,<<"item">>,[{<<"id">>,[]}],[]}, % {<<"http://jabber.org/protocol/pubsub#event">>,<<"items">>, % [{<<"node">>,[<<"urn:xmpp:mucsub:nodes:messages">>]}], % []}, % {<<"p1:push:custom">>,<<"x">>,[{<<"key">>,[]},{<<"value">>,[]}],[]}, % {<<"p1:pushed">>,<<"x">>,[],[]}, % {<<"urn:xmpp:message-correct:0">>,<<"replace">>,[{<<"id">>,[]}],[]}, % {<<"http://jabber.org/protocol/chatstates">>,<<"composing">>,[],[]}] encode(El, J1, J2) -> encode_child(El, <<"jabber:client">>, J1, J2, byte_size(J1), byte_size(J2), <<1:8>>). encode_attr({<<"xmlns">>, _}, Acc) -> Acc; encode_attr({N, V}, Acc) -> <>. encode_attrs(Attrs, Acc) -> lists:foldl(fun encode_attr/2, Acc, Attrs). encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> E1 = if PNs == Ns -> encode_attrs(Attrs, <>); true -> encode_attrs(Attrs, <>) end, E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>. encode_child({xmlel, Name, Attrs, Children}, PNs, J1, J2, J1L, J2L, Pfx) -> case lists:keyfind(<<"xmlns">>, 1, Attrs) of false -> encode(PNs, PNs, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx); {_, Ns} -> encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode_child({xmlcdata, Data}, _PNs, _J1, _J2, _J1L, _J2L, Pfx) -> <>. encode_children(Children, PNs, J1, J2, J1L, J2L, Pfx) -> lists:foldl( fun(Child, Acc) -> encode_child(Child, PNs, J1, J2, J1L, J2L, Acc) end, Pfx, Children). encode_string(Data) -> <> = <<(byte_size(Data)):16/unsigned-big-integer>>, case {V1, V2, V3} of {0, 0, V3} -> <>; {0, V2, V3} -> <<(V3 bor 64):8, V2:8, Data/binary>>; _ -> <<(V3 bor 64):8, (V2 bor 64):8, V1:8, Data/binary>> end. encode(PNs, <<"eu.siacs.conversations.axolotl">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"key">> -> E = lists:foldl(fun ({<<"prekey">>, AVal}, Acc) -> case AVal of <<"true">> -> <>; _ -> <> end; ({<<"rid">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"encrypted">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"header">> -> E = lists:foldl(fun ({<<"sid">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"iv">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"payload">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"jabber:client">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"message">> -> E = lists:foldl(fun ({<<"from">>, AVal}, Acc) -> case AVal of J2 -> <>; <> -> <>; _ -> <> end; ({<<"id">>, AVal}, Acc) -> <>; ({<<"to">>, AVal}, Acc) -> case AVal of J1 -> <>; J2 -> <>; <> -> <>; _ -> <> end; ({<<"type">>, AVal}, Acc) -> case AVal of <<"chat">> -> <>; <<"groupchat">> -> <>; <<"normal">> -> <>; _ -> <> end; ({<<"xml:lang">>, AVal}, Acc) -> case AVal of <<"en">> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"body">> -> E = encode_attrs(Attrs, <>), E2 = lists:foldl(fun ({xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116,32, 116,104,97,116,46,32,70,105,110,100,32,109,111,114,101,32, 105,110,102,111,114,109,97,116,105,111,110,32,111,110,32,104, 116,116,112,115,58,47,47,99,111,110,118,101,114,115,97,116, 105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Acc) -> <>; (El, Acc) -> encode_child(El, Ns, J1, J2, J1L, J2L, Acc) end, <>, Children), <>; <<"subject">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"thread">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:hints">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"store">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:sid:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"origin-id">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"stanza-id">> -> E = lists:foldl(fun ({<<"by">>, AVal}, Acc) -> <>; ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:chat-markers:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"markable">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"displayed">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; ({<<"sender">>, AVal}, Acc) -> case AVal of <> -> <>; <> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"received">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:eme:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"encryption">> -> E = lists:foldl(fun ({<<"name">>, AVal}, Acc) -> case AVal of <<"OMEMO">> -> <>; _ -> <> end; ({<<"namespace">>, AVal}, Acc) -> case AVal of <<"eu.siacs.conversations.axolotl">> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:delay">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"delay">> -> E = lists:foldl(fun ({<<"from">>, AVal}, Acc) -> case AVal of J1 -> <>; _ -> <> end; ({<<"stamp">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"http://jabber.org/protocol/address">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"address">> -> E = lists:foldl(fun ({<<"jid">>, AVal}, Acc) -> case AVal of <> -> <>; _ -> <> end; ({<<"type">>, AVal}, Acc) -> case AVal of <<"ofrom">> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"addresses">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:mam:tmp">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"archived">> -> E = lists:foldl(fun ({<<"by">>, AVal}, Acc) -> <>; ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:receipts">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"request">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"received">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"http://jabber.org/protocol/chatstates">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"active">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"composing">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"http://jabber.org/protocol/muc#user">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"invite">> -> E = lists:foldl(fun ({<<"from">>, AVal}, Acc) -> case AVal of <> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"reason">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"x">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"jabber:x:conference">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"x">> -> E = lists:foldl(fun ({<<"jid">>, AVal}, Acc) -> case AVal of J2 -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"http://jabber.org/protocol/pubsub#event">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"event">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"item">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; <<"items">> -> E = lists:foldl(fun ({<<"node">>, AVal}, Acc) -> case AVal of <<"urn:xmpp:mucsub:nodes:messages">> -> <>; _ -> <> end; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"p1:push:custom">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"x">> -> E = lists:foldl(fun ({<<"key">>, AVal}, Acc) -> <>; ({<<"value">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"p1:pushed">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"x">> -> E = encode_attrs(Attrs, <>), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, <<"urn:xmpp:message-correct:0">> = Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> case Name of <<"replace">> -> E = lists:foldl(fun ({<<"id">>, AVal}, Acc) -> <>; (Attr, Acc) -> encode_attr(Attr, Acc) end, <>, Attrs), E2 = encode_children(Children, Ns, J1, J2, J1L, J2L, <>), <>; _ -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) end; encode(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx) -> encode_el(PNs, Ns, Name, Attrs, Children, J1, J2, J1L, J2L, Pfx). decode(<<$<, _/binary>> = Data, _J1, _J2) -> fxml_stream:parse_element(Data); decode(<<1:8, Rest/binary>>, J1, J2) -> {El, _} = decode(Rest, <<"jabber:client">>, J1, J2), El. decode_string(Data) -> case Data of <<0:2, L:6, Str:L/binary, Rest/binary>> -> {Str, Rest}; <<1:2, L1:6, 0:2, L2:6, Rest/binary>> -> L = L2*64 + L1, <> = Rest, {Str, Rest2}; <<1:2, L1:6, 1:2, L2:6, L3:8, Rest/binary>> -> L = (L3*64 + L2)*64 + L1, <> = Rest, {Str, Rest2} end. decode_child(<<1:8, Rest/binary>>, _PNs, _J1, _J2) -> {Text, Rest2} = decode_string(Rest), {{xmlcdata, Text}, Rest2}; decode_child(<<2:8, Rest/binary>>, PNs, J1, J2) -> {Name, Rest2} = decode_string(Rest), {Attrs, Rest3} = decode_attrs(Rest2), {Children, Rest4} = decode_children(Rest3, PNs, J1, J2), {{xmlel, Name, Attrs, Children}, Rest4}; decode_child(<<3:8, Rest/binary>>, PNs, J1, J2) -> {Ns, Rest2} = decode_string(Rest), {Name, Rest3} = decode_string(Rest2), {Attrs, Rest4} = decode_attrs(Rest3), {Children, Rest5} = decode_children(Rest4, Ns, J1, J2), {{xmlel, Name, add_ns(PNs, Ns, Attrs), Children}, Rest5}; decode_child(<<4:8, Rest/binary>>, _PNs, _J1, _J2) -> {stop, Rest}; decode_child(Other, PNs, J1, J2) -> decode(Other, PNs, J1, J2). decode_children(Data, PNs, J1, J2) -> prefix_map(fun(Data2) -> decode(Data2, PNs, J1, J2) end, Data). decode_attr(<<1:8, Rest/binary>>) -> {Name, Rest2} = decode_string(Rest), {Val, Rest3} = decode_string(Rest2), {{Name, Val}, Rest3}; decode_attr(<<2:8, Rest/binary>>) -> {stop, Rest}. decode_attrs(Data) -> prefix_map(fun decode_attr/1, Data). prefix_map(F, Data) -> prefix_map(F, Data, []). prefix_map(F, Data, Acc) -> case F(Data) of {stop, Rest} -> {lists:reverse(Acc), Rest}; {Val, Rest} -> prefix_map(F, Rest, [Val | Acc]) end. add_ns(Ns, Ns, Attrs) -> Attrs; add_ns(_, Ns, Attrs) -> [{<<"xmlns">>, Ns} | Attrs]. decode(<<5:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"prekey">>, <<"true">>}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"prekey">>, AVal}, Rest4}; (<<5:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"rid">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"key">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<12:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"encrypted">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<13:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"sid">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"header">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<14:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"iv">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<15:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"eu.siacs.conversations.axolotl">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"payload">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<6:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"jabber:client">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"from">>, J2}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"from">>, <>}, Rest4}; (<<5:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"from">>, AVal}, Rest4}; (<<6:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<7:8, Rest3/binary>>) -> {{<<"to">>, J1}, Rest3}; (<<8:8, Rest3/binary>>) -> {{<<"to">>, J2}, Rest3}; (<<9:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"to">>, <>}, Rest4}; (<<10:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"to">>, AVal}, Rest4}; (<<11:8, Rest3/binary>>) -> {{<<"type">>, <<"chat">>}, Rest3}; (<<12:8, Rest3/binary>>) -> {{<<"type">>, <<"groupchat">>}, Rest3}; (<<13:8, Rest3/binary>>) -> {{<<"type">>, <<"normal">>}, Rest3}; (<<14:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"type">>, AVal}, Rest4}; (<<15:8, Rest3/binary>>) -> {{<<"xml:lang">>, <<"en">>}, Rest3}; (<<16:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"xml:lang">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"message">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<8:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"jabber:client">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = prefix_map(fun (<<9:8, Rest5/binary>>) -> {{xmlcdata, <<73,32,115,101,110,116,32,121,111,117,32,97,110,32,79,77,69, 77,79,32,101,110,99,114,121,112,116,101,100,32,109,101,115, 115,97,103,101,32,98,117,116,32,121,111,117,114,32,99,108, 105,101,110,116,32,100,111,101,115,110,226,128,153,116,32, 115,101,101,109,32,116,111,32,115,117,112,112,111,114,116, 32,116,104,97,116,46,32,70,105,110,100,32,109,111,114,101, 32,105,110,102,111,114,109,97,116,105,111,110,32,111,110, 32,104,116,116,112,115,58,47,47,99,111,110,118,101,114,115, 97,116,105,111,110,115,46,105,109,47,111,109,101,109,111>>}, Rest5}; (Other) -> decode_child(Other, Ns, J1, J2) end, Rest2), {{xmlel, <<"body">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<31:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"jabber:client">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"subject">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<32:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"jabber:client">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"thread">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<7:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:hints">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"store">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<10:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:sid:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"origin-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<22:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:sid:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"by">>, AVal}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"stanza-id">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<11:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:chat-markers:0">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"markable">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<20:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:chat-markers:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"sender">>, <>}, Rest4}; (<<5:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"sender">>, <>}, Rest4}; (<<6:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"sender">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"displayed">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<24:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:chat-markers:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<16:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:eme:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"name">>, <<"OMEMO">>}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"name">>, AVal}, Rest4}; (<<5:8, Rest3/binary>>) -> {{<<"namespace">>, <<"eu.siacs.conversations.axolotl">>}, Rest3}; (<<6:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"namespace">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"encryption">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<17:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:delay">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"from">>, J1}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"from">>, AVal}, Rest4}; (<<5:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"stamp">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"delay">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<18:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/address">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"jid">>, <>}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"jid">>, AVal}, Rest4}; (<<5:8, Rest3/binary>>) -> {{<<"type">>, <<"ofrom">>}, Rest3}; (<<6:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"type">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"address">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<19:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/address">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"addresses">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<21:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:mam:tmp">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"by">>, AVal}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"archived">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<23:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:receipts">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"request">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<25:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:receipts">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"received">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<26:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/chatstates">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"active">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<39:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/chatstates">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"composing">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<27:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/muc#user">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"from">>, <>}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"from">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"invite">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<28:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/muc#user">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"reason">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<29:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/muc#user">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<30:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"jabber:x:conference">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"jid">>, J2}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"jid">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<33:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/pubsub#event">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"event">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<34:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/pubsub#event">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"item">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<35:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"http://jabber.org/protocol/pubsub#event">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {{<<"node">>, <<"urn:xmpp:mucsub:nodes:messages">>}, Rest3}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"node">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"items">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<36:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"p1:push:custom">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"key">>, AVal}, Rest4}; (<<4:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"value">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<37:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"p1:pushed">>, {Attrs, Rest2} = decode_attrs(Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"x">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(<<38:8, Rest/binary>>, PNs, J1, J2) -> Ns = <<"urn:xmpp:message-correct:0">>, {Attrs, Rest2} = prefix_map(fun (<<3:8, Rest3/binary>>) -> {AVal, Rest4} = decode_string(Rest3), {{<<"id">>, AVal}, Rest4}; (<<2:8, Rest3/binary>>) -> {stop, Rest3}; (Data) -> decode_attr(Data) end, Rest), {Children, Rest6} = decode_children(Rest2, Ns, J1, J2), {{xmlel, <<"replace">>, add_ns(PNs, Ns, Attrs), Children}, Rest6}; decode(Other, PNs, J1, J2) -> decode_child(Other, PNs, J1, J2). ejabberd-21.12/src/mod_privacy_mnesia.erl0000644000232200023220000001364014154362354021001 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_privacy_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_privacy_mnesia). -behaviour(mod_privacy). %% API -export([init/2, set_default/3, unset_default/2, set_lists/1, set_list/4, get_lists/2, get_list/3, remove_lists/2, remove_list/3, use_cache/1, import/1]). -export([need_transform/1, transform/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, privacy, [{disc_only_copies, [node()]}, {attributes, record_info(fields, privacy)}]). use_cache(Host) -> case mnesia:table_info(privacy, storage_type) of disc_only_copies -> mod_privacy_opt:use_cache(Host); _ -> false end. unset_default(LUser, LServer) -> F = fun () -> case mnesia:read({privacy, {LUser, LServer}}) of [] -> ok; [R] -> mnesia:write(R#privacy{default = none}) end end, transaction(F). set_default(LUser, LServer, Name) -> F = fun () -> case mnesia:read({privacy, {LUser, LServer}}) of [] -> {error, notfound}; [#privacy{lists = Lists} = P] -> case lists:keymember(Name, 1, Lists) of true -> mnesia:write(P#privacy{default = Name, lists = Lists}); false -> {error, notfound} end end end, transaction(F). remove_list(LUser, LServer, Name) -> F = fun () -> case mnesia:read({privacy, {LUser, LServer}}) of [] -> {error, notfound}; [#privacy{default = Default, lists = Lists} = P] -> if Name == Default -> {error, conflict}; true -> NewLists = lists:keydelete(Name, 1, Lists), mnesia:write(P#privacy{lists = NewLists}) end end end, transaction(F). set_lists(Privacy) -> mnesia:dirty_write(Privacy). set_list(LUser, LServer, Name, List) -> F = fun () -> case mnesia:wread({privacy, {LUser, LServer}}) of [] -> NewLists = [{Name, List}], mnesia:write(#privacy{us = {LUser, LServer}, lists = NewLists}); [#privacy{lists = Lists} = P] -> NewLists1 = lists:keydelete(Name, 1, Lists), NewLists = [{Name, List} | NewLists1], mnesia:write(P#privacy{lists = NewLists}) end end, transaction(F). get_list(LUser, LServer, Name) -> case mnesia:dirty_read(privacy, {LUser, LServer}) of [#privacy{default = Default, lists = Lists}] when Name == default -> case lists:keyfind(Default, 1, Lists) of {_, List} -> {ok, {Default, List}}; false -> error end; [#privacy{lists = Lists}] -> case lists:keyfind(Name, 1, Lists) of {_, List} -> {ok, {Name, List}}; false -> error end; [] -> error end. get_lists(LUser, LServer) -> case mnesia:dirty_read(privacy, {LUser, LServer}) of [#privacy{} = P] -> {ok, P}; _ -> error end. remove_lists(LUser, LServer) -> F = fun () -> mnesia:delete({privacy, {LUser, LServer}}) end, transaction(F). import(#privacy{} = P) -> mnesia:dirty_write(P). need_transform({privacy, {U, S}, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'privacy' will be converted to binary", []), true; need_transform(_) -> false. transform(#privacy{us = {U, S}, default = Def, lists = Lists} = R) -> NewLists = lists:map( fun({Name, Ls}) -> NewLs = lists:map( fun(#listitem{value = Val} = L) -> NewVal = case Val of {LU, LS, LR} -> {iolist_to_binary(LU), iolist_to_binary(LS), iolist_to_binary(LR)}; none -> none; both -> both; from -> from; to -> to; _ -> iolist_to_binary(Val) end, L#listitem{value = NewVal} end, Ls), {iolist_to_binary(Name), NewLs} end, Lists), NewDef = case Def of none -> none; _ -> iolist_to_binary(Def) end, NewUS = {iolist_to_binary(U), iolist_to_binary(S)}, R#privacy{us = NewUS, default = NewDef, lists = NewLists}. %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(F) -> case mnesia:transaction(F) of {atomic, Result} -> Result; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, db_failure} end. ejabberd-21.12/src/mod_pubsub_opt.erl0000644000232200023220000001123414154362354020147 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_pubsub_opt). -export([access_createnode/1]). -export([db_type/1]). -export([default_node_config/1]). -export([force_node_config/1]). -export([host/1]). -export([hosts/1]). -export([ignore_pep_from_offline/1]). -export([last_item_cache/1]). -export([max_item_expire_node/1]). -export([max_items_node/1]). -export([max_nodes_discoitems/1]). -export([max_subscriptions_node/1]). -export([name/1]). -export([nodetree/1]). -export([pep_mapping/1]). -export([plugins/1]). -export([vcard/1]). -spec access_createnode(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_createnode(Opts) when is_map(Opts) -> gen_mod:get_opt(access_createnode, Opts); access_createnode(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, access_createnode). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, db_type). -spec default_node_config(gen_mod:opts() | global | binary()) -> [{atom(),atom() | integer()}]. default_node_config(Opts) when is_map(Opts) -> gen_mod:get_opt(default_node_config, Opts); default_node_config(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, default_node_config). -spec force_node_config(gen_mod:opts() | global | binary()) -> [{re:mp(),[{atom(),atom() | integer()}]}]. force_node_config(Opts) when is_map(Opts) -> gen_mod:get_opt(force_node_config, Opts); force_node_config(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, force_node_config). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, hosts). -spec ignore_pep_from_offline(gen_mod:opts() | global | binary()) -> boolean(). ignore_pep_from_offline(Opts) when is_map(Opts) -> gen_mod:get_opt(ignore_pep_from_offline, Opts); ignore_pep_from_offline(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, ignore_pep_from_offline). -spec last_item_cache(gen_mod:opts() | global | binary()) -> boolean(). last_item_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(last_item_cache, Opts); last_item_cache(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, last_item_cache). -spec max_item_expire_node(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_item_expire_node(Opts) when is_map(Opts) -> gen_mod:get_opt(max_item_expire_node, Opts); max_item_expire_node(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_item_expire_node). -spec max_items_node(gen_mod:opts() | global | binary()) -> 'unlimited' | non_neg_integer(). max_items_node(Opts) when is_map(Opts) -> gen_mod:get_opt(max_items_node, Opts); max_items_node(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_items_node). -spec max_nodes_discoitems(gen_mod:opts() | global | binary()) -> 'infinity' | non_neg_integer(). max_nodes_discoitems(Opts) when is_map(Opts) -> gen_mod:get_opt(max_nodes_discoitems, Opts); max_nodes_discoitems(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_nodes_discoitems). -spec max_subscriptions_node(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). max_subscriptions_node(Opts) when is_map(Opts) -> gen_mod:get_opt(max_subscriptions_node, Opts); max_subscriptions_node(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, max_subscriptions_node). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, name). -spec nodetree(gen_mod:opts() | global | binary()) -> binary(). nodetree(Opts) when is_map(Opts) -> gen_mod:get_opt(nodetree, Opts); nodetree(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, nodetree). -spec pep_mapping(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. pep_mapping(Opts) when is_map(Opts) -> gen_mod:get_opt(pep_mapping, Opts); pep_mapping(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, pep_mapping). -spec plugins(gen_mod:opts() | global | binary()) -> [binary()]. plugins(Opts) when is_map(Opts) -> gen_mod:get_opt(plugins, Opts); plugins(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, plugins). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_pubsub, vcard). ejabberd-21.12/src/mod_announce_sql.erl0000644000232200023220000001152714154362354020457 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_announce_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_announce_sql). -behaviour(mod_announce). %% API -export([init/2, set_motd_users/2, set_motd/2, delete_motd/1, get_motd/1, is_motd_user/2, set_motd_user/2, import/3, export/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_announce.hrl"). -include("ejabberd_sql_pt.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. set_motd_users(LServer, USRs) -> F = fun() -> lists:foreach( fun({U, _S, _R}) -> ?SQL_UPSERT_T( "motd", ["!username=%(U)s", "!server_host=%(LServer)s", "xml=''"]) end, USRs) end, transaction(LServer, F). set_motd(LServer, Packet) -> XML = fxml:element_to_binary(Packet), F = fun() -> ?SQL_UPSERT_T( "motd", ["!username=''", "!server_host=%(LServer)s", "xml=%(XML)s"]) end, transaction(LServer, F). delete_motd(LServer) -> F = fun() -> ejabberd_sql:sql_query_t( ?SQL("delete from motd where %(LServer)H")) end, transaction(LServer, F). get_motd(LServer) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(xml)s from motd" " where username='' and %(LServer)H")) of {selected, [{XML}]} -> parse_element(XML); {selected, []} -> error; _ -> {error, db_failure} end. is_motd_user(LUser, LServer) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from motd" " where username=%(LUser)s and %(LServer)H")) of {selected, [_|_]} -> {ok, true}; {selected, []} -> {ok, false}; _ -> {error, db_failure} end. set_motd_user(LUser, LServer) -> F = fun() -> ?SQL_UPSERT_T( "motd", ["!username=%(LUser)s", "!server_host=%(LServer)s", "xml=''"]) end, transaction(LServer, F). export(_Server) -> [{motd, fun(Host, #motd{server = LServer, packet = El}) when LServer == Host -> XML = fxml:element_to_binary(El), [?SQL("delete from motd where username='' and %(LServer)H;"), ?SQL_INSERT( "motd", ["username=''", "server_host=%(LServer)s", "xml=%(XML)s"])]; (_Host, _R) -> [] end}, {motd_users, fun(Host, #motd_users{us = {LUser, LServer}}) when LServer == Host, LUser /= <<"">> -> [?SQL("delete from motd where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "motd", ["username=%(LUser)s", "server_host=%(LServer)s", "xml=''"])]; (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== transaction(LServer, F) -> case ejabberd_sql:sql_transaction(LServer, F) of {atomic, _} -> ok; _ -> {error, db_failure} end. parse_element(XML) -> case fxml_stream:parse_element(XML) of El when is_record(El, xmlel) -> {ok, El}; _ -> ?ERROR_MSG("Malformed XML element in SQL table " "'motd' for username='': ~ts", [XML]), {error, db_failure} end. ejabberd-21.12/src/mod_proxy65.erl0000644000232200023220000002510614154362354017324 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_proxy65.erl %%% Author : Evgeniy Khramtsov %%% Purpose : Main supervisor. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65). -author('xram@jabber.ru'). -protocol({xep, 65, '1.8'}). -behaviour(gen_mod). -behaviour(supervisor). %% gen_mod callbacks. -export([start/2, stop/1, reload/3]). %% supervisor callbacks. -export([init/1]). -export([start_link/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -define(PROCNAME, ejabberd_mod_proxy65). -include("translate.hrl"). -callback init() -> any(). -callback register_stream(binary(), pid()) -> ok | {error, any()}. -callback unregister_stream(binary()) -> ok | {error, any()}. -callback activate_stream(binary(), binary(), pos_integer() | infinity, node()) -> ok | {error, limit | conflict | notfound | term()}. start(Host, Opts) -> case mod_proxy65_service:add_listener(Host, Opts) of {error, _} = Err -> Err; _ -> Mod = gen_mod:ram_db_mod(global, ?MODULE), Mod:init(), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host]}, transient, infinity, supervisor, [?MODULE]}, supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec) end. stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> mod_proxy65_service:delete_listener(Host); true -> ok end, Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). reload(Host, NewOpts, OldOpts) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), Mod:init(), mod_proxy65_service:reload(Host, NewOpts, OldOpts). start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), supervisor:start_link({local, Proc}, ?MODULE, [Host]). init([Host]) -> Service = {mod_proxy65_service, {mod_proxy65_service, start_link, [Host]}, transient, 5000, worker, [mod_proxy65_service]}, {ok, {{one_for_one, 10, 1}, [Service]}}. depends(_Host, _Opts) -> []. mod_opt_type(access) -> econf:acl(); mod_opt_type(hostname) -> econf:host(); mod_opt_type(ip) -> econf:ip(); mod_opt_type(name) -> econf:binary(); mod_opt_type(port) -> econf:port(); mod_opt_type(max_connections) -> econf:pos_int(infinity); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(ram_db_type) -> econf:db_type(?MODULE); mod_opt_type(server_host) -> econf:binary(); mod_opt_type(auth_type) -> econf:enum([plain, anonymous]); mod_opt_type(recbuf) -> econf:pos_int(); mod_opt_type(shaper) -> econf:shaper(); mod_opt_type(sndbuf) -> econf:pos_int(); mod_opt_type(vcard) -> econf:vcard_temp(). mod_options(Host) -> [{ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, {access, all}, {host, <<"proxy.", Host/binary>>}, {hosts, []}, {hostname, undefined}, {ip, undefined}, {port, 7777}, {name, ?T("SOCKS5 Bytestreams")}, {vcard, undefined}, {max_connections, infinity}, {auth_type, anonymous}, {recbuf, 65536}, {sndbuf, 65536}, {shaper, none}]. mod_doc() -> #{desc => ?T("This module implements " "https://xmpp.org/extensions/xep-0065.html" "[XEP-0065: SOCKS5 Bytestreams]. It allows ejabberd " "to act as a file transfer proxy between two XMPP clients."), opts => [{host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"proxy.\". " "The keyword '@HOST@' is replaced with the real virtual host name.")}}, {name, #{value => ?T("Name"), desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. The default is \"SOCKS5 Bytestreams\".")}}, {access, #{value => ?T("AccessName"), desc => ?T("Defines an access rule for file transfer initiators. " "The default value is 'all'. You may want to restrict " "access to the users of your server only, in order to " "avoid abusing your proxy by the users of remote " "servers.")}}, {ram_db_type, #{value => "mnesia | redis | sql", desc => ?T("Define the type of volatile (in-memory) storage where the module " "will store room information.")}}, {ip, #{value => ?T("IPAddress"), desc => ?T("This option specifies which network interface to listen " "for. The default value is an IP address of the service's " "DNS name, or, if fails, '127.0.0.1'.")}}, {hostname, #{value => ?T("Host"), desc => ?T("Defines a hostname offered by the proxy when " "establishing a session with clients. This is useful " "when you run the proxy behind a NAT. The keyword " "'@HOST@' is replaced with the virtual host name. " "The default is to use the value of 'ip' option. " "Examples: 'proxy.mydomain.org', '200.150.100.50'.")}}, {port, #{value => "1..65535", desc => ?T("A port number to listen for incoming connections. " "The default value is '7777'.")}}, {auth_type, #{value => "anonymous | plain", desc => ?T("SOCKS5 authentication type. " "The default value is 'anonymous'. " "If set to 'plain', ejabberd will use " "authentication backend as it would " "for SASL PLAIN.")}}, {max_connections, #{value => "pos_integer() | infinity", desc => ?T("Maximum number of active connections per file transfer " "initiator. The default value is 'infinity'.")}}, {shaper, #{value => ?T("Shaper"), desc => ?T("This option defines a shaper for the file transfer peers. " "A shaper with the maximum bandwidth will be selected. " "The default is 'none', i.e. no shaper.")}}, {recbuf, #{value => ?T("Size"), desc => ?T("A size of the buffer for incoming packets. " "If you define a shaper, set the value of this " "option to the size of the shaper in order " "to avoid traffic spikes in file transfers. " "The default value is '65536' bytes.")}}, {sndbuf, #{value => ?T("Size"), desc => ?T("A size of the buffer for outgoing packets. " "If you define a shaper, set the value of this " "option to the size of the shaper in order " "to avoid traffic spikes in file transfers. " "The default value is '65536' bytes.")}}, {vcard, #{value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML representation " "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => [{?T("For example, the following XML representation of vCard:"), ["", " Conferences", " ", " ", " Elm Street", " ", ""]}, {?T("will be translated to:"), ["vcard:", " fn: Conferences", " adr:", " -", " work: true", " street: Elm Street"]}]}}], example => ["acl:", " admin:", " user: admin@example.org", " proxy_users:", " server: example.org", "", "access_rules:", " proxy65_access:", " allow: proxy_users", "", "shaper_rules:", " proxy65_shaper:", " none: admin", " proxyrate: proxy_users", "", "shaper:", " proxyrate: 10240", "", "modules:", " ...", " mod_proxy65:", " host: proxy1.example.org", " name: \"File Transfer Proxy\"", " ip: 200.150.100.1", " port: 7778", " max_connections: 5", " access: proxy65_access", " shaper: proxy65_shaper", " recbuf: 10240", " sndbuf: 10240", " ..."]}. ejabberd-21.12/src/mod_bosh_redis.erl0000644000232200023220000001026314154362354020107 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_bosh_redis.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_bosh_redis). -behaviour(mod_bosh). -behaviour(gen_server). %% API -export([init/0, open_session/2, close_session/1, find_session/1, cache_nodes/0]). %% gen_server callbacks -export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("logger.hrl"). -include("bosh.hrl"). -record(state, {}). -define(BOSH_KEY, <<"ejabberd:bosh">>). %%%=================================================================== %%% API %%%=================================================================== init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). open_session(SID, Pid) -> PidBin = term_to_binary(Pid), case ejabberd_redis:multi( fun() -> ejabberd_redis:hset(?BOSH_KEY, SID, PidBin), ejabberd_redis:publish(?BOSH_KEY, SID) end) of {ok, _} -> ok; {error, _} -> {error, db_failure} end. close_session(SID) -> case ejabberd_redis:multi( fun() -> ejabberd_redis:hdel(?BOSH_KEY, [SID]), ejabberd_redis:publish(?BOSH_KEY, SID) end) of {ok, _} -> ok; {error, _} -> {error, db_failure} end. find_session(SID) -> case ejabberd_redis:hget(?BOSH_KEY, SID) of {ok, undefined} -> {error, notfound}; {ok, Pid} -> try {ok, binary_to_term(Pid)} catch _:badarg -> ?ERROR_MSG("Malformed data in redis (key = '~ts'): ~p", [SID, Pid]), {error, db_failure} end; {error, _} -> {error, db_failure} end. cache_nodes() -> [node()]. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> clean_table(), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({redis_message, ?BOSH_KEY, SID}, State) -> ets_cache:delete(?BOSH_CACHE, SID), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== clean_table() -> ?DEBUG("Cleaning Redis BOSH sessions...", []), case ejabberd_redis:hgetall(?BOSH_KEY) of {ok, Vals} -> ejabberd_redis:multi( fun() -> lists:foreach( fun({SID, Pid}) when node(Pid) == node() -> ejabberd_redis:hdel(?BOSH_KEY, [SID]); (_) -> ok end, Vals) end), ok; {error, _} -> ?ERROR_MSG("Failed to clean bosh sessions in redis", []) end. ejabberd-21.12/src/ejabberd_redis.erl0000644000232200023220000004424014154362354020055 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_redis.erl %%% Author : Evgeny Khramtsov %%% Created : 8 May 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_redis). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). -compile({no_auto_import, [get/1, put/2]}). %% API -export([start_link/1, get_proc/1, get_connection/1, q/1, qp/1, format_error/1]). %% Commands -export([multi/1, get/1, set/2, del/1, info/1, sadd/2, srem/2, smembers/1, sismember/2, scard/1, hget/2, hset/3, hdel/2, hlen/1, hgetall/1, hkeys/1, subscribe/1, publish/2, script_load/1, evalsha/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -define(PROCNAME, 'ejabberd_redis_client'). -define(TR_STACK, redis_transaction_stack). -define(DEFAULT_MAX_QUEUE, 10000). -define(MAX_RETRIES, 1). -define(CALL_TIMEOUT, 60*1000). %% 60 seconds -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -record(state, {connection :: pid() | undefined, num :: pos_integer(), subscriptions = #{} :: subscriptions(), pending_q :: queue()}). -type queue() :: p1_queue:queue({{pid(), term()}, integer()}). -type subscriptions() :: #{binary() => [pid()]}. -type error_reason() :: binary() | timeout | disconnected | overloaded. -type redis_error() :: {error, error_reason()}. -type redis_reply() :: undefined | binary() | [binary()]. -type redis_command() :: [iodata() | integer()]. -type redis_pipeline() :: [redis_command()]. -type redis_info() :: server | clients | memory | persistence | stats | replication | cpu | commandstats | cluster | keyspace | default | all. -type state() :: #state{}. -export_type([error_reason/0]). %%%=================================================================== %%% API %%%=================================================================== start_link(I) -> ?GEN_SERVER:start_link({local, get_proc(I)}, ?MODULE, [I], []). get_proc(I) -> misc:binary_to_atom( iolist_to_binary( [atom_to_list(?MODULE), $_, integer_to_list(I)])). get_connection(I) -> misc:binary_to_atom( iolist_to_binary( [atom_to_list(?MODULE), "_connection_", integer_to_list(I)])). -spec q(redis_command()) -> {ok, redis_reply()} | redis_error(). q(Command) -> call(get_rnd_id(), {q, Command}, ?MAX_RETRIES). -spec qp(redis_pipeline()) -> [{ok, redis_reply()} | redis_error()] | redis_error(). qp(Pipeline) -> call(get_rnd_id(), {qp, Pipeline}, ?MAX_RETRIES). -spec multi(fun(() -> any())) -> {ok, redis_reply()} | redis_error(). multi(F) -> case erlang:get(?TR_STACK) of undefined -> erlang:put(?TR_STACK, []), try F() of _ -> Stack = erlang:erase(?TR_STACK), Command = [["MULTI"]|lists:reverse([["EXEC"]|Stack])], case qp(Command) of {error, _} = Err -> Err; Result -> get_result(Result) end catch ?EX_RULE(E, R, St) -> erlang:erase(?TR_STACK), erlang:raise(E, R, ?EX_STACK(St)) end; _ -> erlang:error(nested_transaction) end. -spec format_error(atom() | binary()) -> binary(). format_error(Reason) when is_atom(Reason) -> format_error(misc:atom_to_binary(Reason)); format_error(Reason) -> Reason. %%%=================================================================== %%% Redis commands API %%%=================================================================== -spec get(iodata()) -> {ok, undefined | binary()} | redis_error(). get(Key) -> case erlang:get(?TR_STACK) of undefined -> q([<<"GET">>, Key]); _ -> erlang:error(transaction_unsupported) end. -spec set(iodata(), iodata()) -> ok | redis_error() | queued. set(Key, Val) -> Cmd = [<<"SET">>, Key, Val], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, <<"OK">>} -> ok; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec del(list()) -> {ok, non_neg_integer()} | redis_error() | queued. del([]) -> reply(0); del(Keys) -> Cmd = [<<"DEL">>|Keys], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec sadd(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. sadd(_Set, []) -> reply(0); sadd(Set, Members) -> Cmd = [<<"SADD">>, Set|Members], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec srem(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. srem(_Set, []) -> reply(0); srem(Set, Members) -> Cmd = [<<"SREM">>, Set|Members], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec smembers(iodata()) -> {ok, [binary()]} | redis_error(). smembers(Set) -> case erlang:get(?TR_STACK) of undefined -> q([<<"SMEMBERS">>, Set]); _ -> erlang:error(transaction_unsupported) end. -spec sismember(iodata(), iodata()) -> boolean() | redis_error(). sismember(Set, Member) -> case erlang:get(?TR_STACK) of undefined -> case q([<<"SISMEMBER">>, Set, Member]) of {ok, Flag} -> {ok, dec_bool(Flag)}; {error, _} = Err -> Err end; _ -> erlang:error(transaction_unsupported) end. -spec scard(iodata()) -> {ok, non_neg_integer()} | redis_error(). scard(Set) -> case erlang:get(?TR_STACK) of undefined -> case q([<<"SCARD">>, Set]) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; _ -> erlang:error(transaction_unsupported) end. -spec hget(iodata(), iodata()) -> {ok, undefined | binary()} | redis_error(). hget(Key, Field) -> case erlang:get(?TR_STACK) of undefined -> q([<<"HGET">>, Key, Field]); _ -> erlang:error(transaction_unsupported) end. -spec hset(iodata(), iodata(), iodata()) -> {ok, boolean()} | redis_error() | queued. hset(Key, Field, Val) -> Cmd = [<<"HSET">>, Key, Field, Val], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, Flag} -> {ok, dec_bool(Flag)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec hdel(iodata(), list()) -> {ok, non_neg_integer()} | redis_error() | queued. hdel(_Key, []) -> reply(0); hdel(Key, Fields) -> Cmd = [<<"HDEL">>, Key|Fields], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec hgetall(iodata()) -> {ok, [{binary(), binary()}]} | redis_error(). hgetall(Key) -> case erlang:get(?TR_STACK) of undefined -> case q([<<"HGETALL">>, Key]) of {ok, Pairs} -> {ok, decode_pairs(Pairs)}; {error, _} = Err -> Err end; _ -> erlang:error(transaction_unsupported) end. -spec hlen(iodata()) -> {ok, non_neg_integer()} | redis_error(). hlen(Key) -> case erlang:get(?TR_STACK) of undefined -> case q([<<"HLEN">>, Key]) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; _ -> erlang:error(transaction_unsupported) end. -spec hkeys(iodata()) -> {ok, [binary()]} | redis_error(). hkeys(Key) -> case erlang:get(?TR_STACK) of undefined -> q([<<"HKEYS">>, Key]); _ -> erlang:error(transaction_unsupported) end. -spec subscribe([binary()]) -> ok | redis_error(). subscribe(Channels) -> try gen_server_call(get_proc(1), {subscribe, self(), Channels}) catch exit:{Why, {?GEN_SERVER, call, _}} -> Reason = case Why of timeout -> timeout; _ -> disconnected end, {error, Reason} end. -spec publish(iodata(), iodata()) -> {ok, non_neg_integer()} | redis_error() | queued. publish(Channel, Data) -> Cmd = [<<"PUBLISH">>, Channel, Data], case erlang:get(?TR_STACK) of undefined -> case q(Cmd) of {ok, N} -> {ok, binary_to_integer(N)}; {error, _} = Err -> Err end; Stack -> tr_enq(Cmd, Stack) end. -spec script_load(iodata()) -> {ok, binary()} | redis_error(). script_load(Data) -> case erlang:get(?TR_STACK) of undefined -> q([<<"SCRIPT">>, <<"LOAD">>, Data]); _ -> erlang:error(transaction_unsupported) end. -spec evalsha(binary(), [iodata()], [iodata() | integer()]) -> {ok, binary()} | redis_error(). evalsha(SHA, Keys, Args) -> case erlang:get(?TR_STACK) of undefined -> q([<<"EVALSHA">>, SHA, length(Keys)|Keys ++ Args]); _ -> erlang:error(transaction_unsupported) end. -spec info(redis_info()) -> {ok, [{atom(), binary()}]} | redis_error(). info(Type) -> case erlang:get(?TR_STACK) of undefined -> case q([<<"INFO">>, misc:atom_to_binary(Type)]) of {ok, Info} -> Lines = binary:split(Info, <<"\r\n">>, [global]), KVs = [binary:split(Line, <<":">>) || Line <- Lines], {ok, [{misc:binary_to_atom(K), V} || [K, V] <- KVs]}; {error, _} = Err -> Err end; _ -> erlang:error(transaction_unsupported) end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([I]) -> process_flag(trap_exit, true), QueueType = get_queue_type(), Limit = max_fsm_queue(), self() ! connect, {ok, #state{num = I, pending_q = p1_queue:new(QueueType, Limit)}}. handle_call(connect, From, #state{connection = undefined, pending_q = Q} = State) -> CurrTime = erlang:monotonic_time(millisecond), Q2 = try p1_queue:in({From, CurrTime}, Q) catch error:full -> Q1 = clean_queue(Q, CurrTime), p1_queue:in({From, CurrTime}, Q1) end, {noreply, State#state{pending_q = Q2}}; handle_call(connect, From, #state{connection = Pid} = State) -> case is_process_alive(Pid) of true -> {reply, ok, State}; false -> self() ! connect, handle_call(connect, From, State#state{connection = undefined}) end; handle_call({subscribe, Caller, Channels}, _From, #state{connection = Pid, subscriptions = Subs} = State) -> Subs1 = lists:foldl( fun(Channel, Acc) -> Callers = maps:get(Channel, Acc, []) -- [Caller], maps:put(Channel, [Caller|Callers], Acc) end, Subs, Channels), eredis_subscribe(Pid, Channels), {reply, ok, State#state{subscriptions = Subs1}}; handle_call(Request, _From, State) -> ?WARNING_MSG("Unexpected call: ~p", [Request]), {noreply, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(connect, #state{connection = undefined} = State) -> NewState = case connect(State) of {ok, Connection} -> Q1 = flush_queue(State#state.pending_q), re_subscribe(Connection, State#state.subscriptions), State#state{connection = Connection, pending_q = Q1}; {error, _} -> State end, {noreply, NewState}; handle_info(connect, State) -> %% Already connected {noreply, State}; handle_info({'EXIT', Pid, _}, State) -> case State#state.connection of Pid -> self() ! connect, {noreply, State#state{connection = undefined}}; _ -> {noreply, State} end; handle_info({subscribed, Channel, Pid}, State) -> case State#state.connection of Pid -> case maps:is_key(Channel, State#state.subscriptions) of true -> eredis_sub:ack_message(Pid); false -> ?WARNING_MSG("Got subscription ack for unknown channel ~ts", [Channel]) end; _ -> ok end, {noreply, State}; handle_info({message, Channel, Data, Pid}, State) -> case State#state.connection of Pid -> lists:foreach( fun(Subscriber) -> erlang:send(Subscriber, {redis_message, Channel, Data}) end, maps:get(Channel, State#state.subscriptions, [])), eredis_sub:ack_message(Pid); _ -> ok end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info = ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec connect(state()) -> {ok, pid()} | {error, any()}. connect(#state{num = Num}) -> Server = ejabberd_option:redis_server(), Port = ejabberd_option:redis_port(), DB = ejabberd_option:redis_db(), Pass = ejabberd_option:redis_password(), ConnTimeout = ejabberd_option:redis_connect_timeout(), try case do_connect(Num, Server, Port, Pass, DB, ConnTimeout) of {ok, Client} -> ?DEBUG("Connection #~p established to Redis at ~ts:~p", [Num, Server, Port]), register(get_connection(Num), Client), {ok, Client}; {error, Why} -> erlang:error(Why) end catch _:Reason -> Timeout = p1_rand:uniform( min(10, ejabberd_redis_sup:get_pool_size())), ?ERROR_MSG("Redis connection #~p at ~ts:~p has failed: ~p; " "reconnecting in ~p seconds", [Num, Server, Port, Reason, Timeout]), erlang:send_after(timer:seconds(Timeout), self(), connect), {error, Reason} end. do_connect(1, Server, Port, Pass, _DB, _ConnTimeout) -> %% First connection in the pool is always a subscriber Res = eredis_sub:start_link(Server, Port, Pass, no_reconnect, infinity, drop), case Res of {ok, Pid} -> eredis_sub:controlling_process(Pid); _ -> ok end, Res; do_connect(_, Server, Port, Pass, DB, ConnTimeout) -> eredis:start_link(Server, Port, DB, Pass, no_reconnect, ConnTimeout). -spec call(pos_integer(), {q, redis_command()}, integer()) -> {ok, redis_reply()} | redis_error(); (pos_integer(), {qp, redis_pipeline()}, integer()) -> [{ok, redis_reply()} | redis_error()] | redis_error(). call(I, {F, Cmd}, Retries) -> ?DEBUG("Redis query: ~p", [Cmd]), Conn = get_connection(I), Res = try eredis:F(Conn, Cmd, ?CALL_TIMEOUT) of {error, Reason} when is_atom(Reason) -> try exit(whereis(Conn), kill) catch _:_ -> ok end, {error, disconnected}; Other -> Other catch exit:{timeout, _} -> {error, timeout}; exit:{_, {gen_server, call, _}} -> {error, disconnected} end, case Res of {error, disconnected} when Retries > 0 -> try gen_server_call(get_proc(I), connect) of ok -> call(I, {F, Cmd}, Retries-1); {error, _} = Err -> Err catch exit:{Why, {?GEN_SERVER, call, _}} -> Reason1 = case Why of timeout -> timeout; _ -> disconnected end, log_error(Cmd, Reason1), {error, Reason1} end; {error, Reason1} -> log_error(Cmd, Reason1), Res; _ -> Res end. gen_server_call(Proc, Msg) -> case ejabberd_redis_sup:start() of ok -> ?GEN_SERVER:call(Proc, Msg, ?CALL_TIMEOUT); {error, _} -> {error, disconnected} end. -spec log_error(redis_command() | redis_pipeline(), atom() | binary()) -> ok. log_error(Cmd, Reason) -> ?ERROR_MSG("Redis request has failed:~n" "** request = ~p~n" "** response = ~ts", [Cmd, format_error(Reason)]). -spec get_rnd_id() -> pos_integer(). get_rnd_id() -> p1_rand:round_robin(ejabberd_redis_sup:get_pool_size() - 1) + 2. -spec get_result([{ok, redis_reply()} | redis_error()]) -> {ok, redis_reply()} | redis_error(). get_result([{error, _} = Err|_]) -> Err; get_result([{ok, _} = OK]) -> OK; get_result([_|T]) -> get_result(T). -spec tr_enq([iodata()], list()) -> queued. tr_enq(Cmd, Stack) -> erlang:put(?TR_STACK, [Cmd|Stack]), queued. -spec decode_pairs([binary()]) -> [{binary(), binary()}]. decode_pairs(Pairs) -> decode_pairs(Pairs, []). -spec decode_pairs([binary()], [{binary(), binary()}]) -> [{binary(), binary()}]. decode_pairs([Field, Val|Pairs], Acc) -> decode_pairs(Pairs, [{Field, Val}|Acc]); decode_pairs([], Acc) -> lists:reverse(Acc). dec_bool(<<$1>>) -> true; dec_bool(<<$0>>) -> false. -spec reply(T) -> {ok, T} | queued. reply(Val) -> case erlang:get(?TR_STACK) of undefined -> {ok, Val}; _ -> queued end. -spec max_fsm_queue() -> pos_integer(). max_fsm_queue() -> proplists:get_value(max_queue, fsm_limit_opts(), ?DEFAULT_MAX_QUEUE). fsm_limit_opts() -> ejabberd_config:fsm_limit_opts([]). get_queue_type() -> ejabberd_option:redis_queue_type(). -spec flush_queue(queue()) -> queue(). flush_queue(Q) -> CurrTime = erlang:monotonic_time(millisecond), p1_queue:dropwhile( fun({From, Time}) -> if (CurrTime - Time) >= ?CALL_TIMEOUT -> ok; true -> ?GEN_SERVER:reply(From, ok) end, true end, Q). -spec clean_queue(queue(), integer()) -> queue(). clean_queue(Q, CurrTime) -> Q1 = p1_queue:dropwhile( fun({_From, Time}) -> (CurrTime - Time) >= ?CALL_TIMEOUT end, Q), Len = p1_queue:len(Q1), Limit = p1_queue:get_limit(Q1), if Len >= Limit -> ?ERROR_MSG("Redis request queue is overloaded", []), p1_queue:dropwhile( fun({From, _Time}) -> ?GEN_SERVER:reply(From, {error, overloaded}), true end, Q1); true -> Q1 end. re_subscribe(Pid, Subs) -> case maps:keys(Subs) of [] -> ok; Channels -> eredis_subscribe(Pid, Channels) end. eredis_subscribe(Pid, Channels) -> ?DEBUG("Redis query: ~p", [[<<"SUBSCRIBE">>|Channels]]), eredis_sub:subscribe(Pid, Channels). ejabberd-21.12/src/ejabberd_captcha.erl0000644000232200023220000004744114154362354020360 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_captcha.erl %%% Author : Evgeniy Khramtsov %%% Purpose : CAPTCHA processing. %%% Created : 26 Apr 2008 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_captcha). -protocol({xep, 158, '1.0'}). -behaviour(gen_server). %% API -export([start_link/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([create_captcha/6, build_captcha_html/2, check_captcha/2, process_reply/1, process/2, is_feature_available/0, create_captcha_x/5, host_up/1, host_down/1, config_reloaded/0, process_iq/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("translate.hrl"). -define(CAPTCHA_LIFETIME, 120000). -define(LIMIT_PERIOD, 60*1000*1000). -type image_error() :: efbig | enodata | limit | malformed_image | timeout. -type priority() :: neg_integer(). -type callback() :: fun((captcha_succeed | captcha_failed) -> any()). -record(state, {limits = treap:empty() :: treap:treap(), enabled = false :: boolean()}). -record(captcha, {id :: binary(), pid :: pid() | undefined, key :: binary(), tref :: reference(), args :: any()}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec captcha_text(binary()) -> binary(). captcha_text(Lang) -> translate:translate(Lang, ?T("Enter the text you see")). -spec mk_ocr_field(binary(), binary(), binary()) -> xdata_field(). mk_ocr_field(Lang, CID, Type) -> URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>}, [_, F] = captcha_form:encode([{ocr, <<>>}], Lang, [ocr]), xmpp:set_els(F, [#media{uri = [URI]}]). -spec create_captcha(binary(), jid(), jid(), binary(), any(), callback() | term()) -> {error, image_error()} | {ok, binary(), [text()], [xmpp_element()]}. create_captcha(SID, From, To, Lang, Limiter, Args) -> case create_image(Limiter) of {ok, Type, Key, Image} -> Id = <<(p1_rand:get_string())/binary>>, JID = jid:encode(From), CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, Fs = captcha_form:encode( [{from, To}, {challenge, Id}, {sid, SID}, mk_ocr_field(Lang, CID, Type)], Lang, [challenge]), X = #xdata{type = form, fields = Fs}, Captcha = #xcaptcha{xdata = X}, BodyString = {?T("Your subscription request and/or messages to ~s have been blocked. " "To unblock your subscription request, visit ~s"), [JID, get_url(Id)]}, Body = xmpp:mk_text(BodyString, Lang), OOB = #oob_x{url = get_url(Id)}, Hint = #hint{type = 'no-store'}, Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), ets:insert(captcha, #captcha{id = Id, pid = self(), key = Key, tref = Tref, args = Args}), {ok, Id, Body, [Hint, OOB, Captcha, Data]}; Err -> Err end. -spec create_captcha_x(binary(), jid(), binary(), any(), xdata()) -> {ok, [xmpp_element()]} | {error, image_error()}. create_captcha_x(SID, To, Lang, Limiter, #xdata{fields = Fs} = X) -> case create_image(Limiter) of {ok, Type, Key, Image} -> Id = <<(p1_rand:get_string())/binary>>, CID = <<"sha1+", (str:sha(Image))/binary, "@bob.xmpp.org">>, Data = #bob_data{cid = CID, 'max-age' = 0, type = Type, data = Image}, HelpTxt = translate:translate( Lang, ?T("If you don't see the CAPTCHA image here, visit the web page.")), Imageurl = get_url(<>), [H|T] = captcha_form:encode( [{'captcha-fallback-text', HelpTxt}, {'captcha-fallback-url', Imageurl}, {from, To}, {challenge, Id}, {sid, SID}, mk_ocr_field(Lang, CID, Type)], Lang, [challenge]), Captcha = X#xdata{type = form, fields = [H|Fs ++ T]}, Tref = erlang:send_after(?CAPTCHA_LIFETIME, ?MODULE, {remove_id, Id}), ets:insert(captcha, #captcha{id = Id, key = Key, tref = Tref}), {ok, [Captcha, Data]}; Err -> Err end. -spec build_captcha_html(binary(), binary()) -> captcha_not_found | {xmlel(), {xmlel(), cdata(), xmlel(), xmlel()}}. build_captcha_html(Id, Lang) -> case lookup_captcha(Id) of {ok, _} -> ImgEl = #xmlel{name = <<"img">>, attrs = [{<<"src">>, get_url(<>)}], children = []}, Text = {xmlcdata, captcha_text(Lang)}, IdEl = #xmlel{name = <<"input">>, attrs = [{<<"type">>, <<"hidden">>}, {<<"name">>, <<"id">>}, {<<"value">>, Id}], children = []}, KeyEl = #xmlel{name = <<"input">>, attrs = [{<<"type">>, <<"text">>}, {<<"name">>, <<"key">>}, {<<"size">>, <<"10">>}], children = []}, FormEl = #xmlel{name = <<"form">>, attrs = [{<<"action">>, get_url(Id)}, {<<"name">>, <<"captcha">>}, {<<"method">>, <<"POST">>}], children = [ImgEl, #xmlel{name = <<"br">>, attrs = [], children = []}, Text, #xmlel{name = <<"br">>, attrs = [], children = []}, IdEl, KeyEl, #xmlel{name = <<"br">>, attrs = [], children = []}, #xmlel{name = <<"input">>, attrs = [{<<"type">>, <<"submit">>}, {<<"name">>, <<"enter">>}, {<<"value">>, ?T("OK")}], children = []}]}, {FormEl, {ImgEl, Text, IdEl, KeyEl}}; _ -> captcha_not_found end. -spec process_reply(xmpp_element()) -> ok | {error, bad_match | not_found | malformed}. process_reply(#xdata{} = X) -> Required = [<<"challenge">>, <<"ocr">>], Fs = lists:filter( fun(#xdata_field{var = Var}) -> lists:member(Var, [<<"FORM_TYPE">>|Required]) end, X#xdata.fields), try captcha_form:decode(Fs, [?NS_CAPTCHA], Required) of Props -> Id = proplists:get_value(challenge, Props), OCR = proplists:get_value(ocr, Props), case check_captcha(Id, OCR) of captcha_valid -> ok; captcha_non_valid -> {error, bad_match}; captcha_not_found -> {error, not_found} end catch _:{captcha_form, Why} -> ?WARNING_MSG("Malformed CAPTCHA form: ~ts", [captcha_form:format_error(Why)]), {error, malformed} end; process_reply(#xcaptcha{xdata = #xdata{} = X}) -> process_reply(X); process_reply(_) -> {error, malformed}. -spec process_iq(iq()) -> iq(). process_iq(#iq{type = set, lang = Lang, sub_els = [#xcaptcha{} = El]} = IQ) -> case process_reply(El) of ok -> xmpp:make_iq_result(IQ); {error, malformed} -> Txt = ?T("Incorrect CAPTCHA submit"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); {error, _} -> Txt = ?T("The CAPTCHA verification has failed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)) end; process_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). process(_Handlers, #request{method = 'GET', lang = Lang, path = [_, Id]}) -> case build_captcha_html(Id, Lang) of {FormEl, _} -> Form = #xmlel{name = <<"div">>, attrs = [{<<"align">>, <<"center">>}], children = [FormEl]}, ejabberd_web:make_xhtml([Form]); captcha_not_found -> ejabberd_web:error(not_found) end; process(_Handlers, #request{method = 'GET', path = [_, Id, <<"image">>], ip = IP}) -> {Addr, _Port} = IP, case lookup_captcha(Id) of {ok, #captcha{key = Key}} -> case create_image(Addr, Key) of {ok, Type, _, Img} -> {200, [{<<"Content-Type">>, Type}, {<<"Cache-Control">>, <<"no-cache">>}, {<<"Last-Modified">>, list_to_binary(httpd_util:rfc1123_date())}], Img}; {error, limit} -> ejabberd_web:error(not_allowed); _ -> ejabberd_web:error(not_found) end; _ -> ejabberd_web:error(not_found) end; process(_Handlers, #request{method = 'POST', q = Q, lang = Lang, path = [_, Id]}) -> ProvidedKey = proplists:get_value(<<"key">>, Q, none), case check_captcha(Id, ProvidedKey) of captcha_valid -> Form = #xmlel{name = <<"p">>, attrs = [], children = [{xmlcdata, translate:translate(Lang, ?T("The CAPTCHA is valid."))}]}, ejabberd_web:make_xhtml([Form]); captcha_non_valid -> ejabberd_web:error(not_allowed); captcha_not_found -> ejabberd_web:error(not_found) end; process(_Handlers, _Request) -> ejabberd_web:error(not_found). host_up(Host) -> gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA, ?MODULE, process_iq). host_down(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_CAPTCHA). config_reloaded() -> gen_server:call(?MODULE, config_reloaded, timer:minutes(1)). init([]) -> _ = mnesia:delete_table(captcha), _ = ets:new(captcha, [named_table, public, {keypos, #captcha.id}]), case check_captcha_setup() of true -> register_handlers(), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), {ok, #state{enabled = true}}; false -> {ok, #state{enabled = false}}; {error, Reason} -> {stop, Reason} end. handle_call({is_limited, Limiter, RateLimit}, _From, State) -> NowPriority = now_priority(), CleanPriority = NowPriority + (?LIMIT_PERIOD), Limits = clean_treap(State#state.limits, CleanPriority), case treap:lookup(Limiter, Limits) of {ok, _, Rate} when Rate >= RateLimit -> {reply, true, State#state{limits = Limits}}; {ok, Priority, Rate} -> NewLimits = treap:insert(Limiter, Priority, Rate + 1, Limits), {reply, false, State#state{limits = NewLimits}}; _ -> NewLimits = treap:insert(Limiter, NowPriority, 1, Limits), {reply, false, State#state{limits = NewLimits}} end; handle_call(config_reloaded, _From, #state{enabled = Enabled} = State) -> State1 = case is_feature_available() of true when not Enabled -> case check_captcha_setup() of true -> register_handlers(), State#state{enabled = true}; _ -> State end; false when Enabled -> unregister_handlers(), State#state{enabled = false}; _ -> State end, {reply, ok, State1}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({remove_id, Id}, State) -> ?DEBUG("CAPTCHA ~p timed out", [Id]), case ets:lookup(captcha, Id) of [#captcha{args = Args, pid = Pid}] -> callback(captcha_failed, Pid, Args), ets:delete(captcha, Id); _ -> ok end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, #state{enabled = Enabled}) -> if Enabled -> unregister_handlers(); true -> ok end, ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). register_handlers() -> ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), ejabberd_hooks:add(host_down, ?MODULE, host_down, 50), lists:foreach(fun host_up/1, ejabberd_option:hosts()). unregister_handlers() -> ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 50), lists:foreach(fun host_down/1, ejabberd_option:hosts()). code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec create_image() -> {ok, binary(), binary(), binary()} | {error, image_error()}. create_image() -> create_image(undefined). -spec create_image(term()) -> {ok, binary(), binary(), binary()} | {error, image_error()}. create_image(Limiter) -> Key = str:substr(p1_rand:get_string(), 1, 6), create_image(Limiter, Key). -spec create_image(term(), binary()) -> {ok, binary(), binary(), binary()} | {error, image_error()}. create_image(Limiter, Key) -> case is_limited(Limiter) of true -> {error, limit}; false -> do_create_image(Key) end. -spec do_create_image(binary()) -> {ok, binary(), binary(), binary()} | {error, image_error()}. do_create_image(Key) -> FileName = get_prog_name(), Cmd = lists:flatten(io_lib:format("~ts ~ts", [FileName, Key])), case cmd(Cmd) of {ok, <<137, $P, $N, $G, $\r, $\n, 26, $\n, _/binary>> = Img} -> {ok, <<"image/png">>, Key, Img}; {ok, <<255, 216, _/binary>> = Img} -> {ok, <<"image/jpeg">>, Key, Img}; {ok, <<$G, $I, $F, $8, X, $a, _/binary>> = Img} when X == $7; X == $9 -> {ok, <<"image/gif">>, Key, Img}; {error, enodata = Reason} -> ?ERROR_MSG("Failed to process output from \"~ts\". " "Maybe ImageMagick's Convert program " "is not installed.", [Cmd]), {error, Reason}; {error, Reason} -> ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", [Cmd, Reason]), {error, Reason}; _ -> Reason = malformed_image, ?ERROR_MSG("Failed to process an output from \"~ts\": ~p", [Cmd, Reason]), {error, Reason} end. get_prog_name() -> case ejabberd_option:captcha_cmd() of undefined -> ?DEBUG("The option captcha_cmd is not configured, " "but some module wants to use the CAPTCHA " "feature.", []), false; FileName -> FileName end. -spec get_url(binary()) -> binary(). get_url(Str) -> case ejabberd_option:captcha_url() of undefined -> URL = parse_captcha_host(), <>; URL -> <> end. -spec parse_captcha_host() -> binary(). parse_captcha_host() -> CaptchaHost = ejabberd_option:captcha_host(), case str:tokens(CaptchaHost, <<":">>) of [Host] -> <<"http://", Host/binary>>; [<<"http", _/binary>> = TransferProt, Host] -> <>; [Host, PortString] -> TransferProt = atom_to_binary(get_transfer_protocol(PortString), latin1), <>; [TransferProt, Host, PortString] -> <>; _ -> <<"http://", (ejabberd_config:get_myname())/binary>> end. get_transfer_protocol(PortString) -> PortNumber = binary_to_integer(PortString), PortListeners = get_port_listeners(PortNumber), get_captcha_transfer_protocol(PortListeners). get_port_listeners(PortNumber) -> AllListeners = ejabberd_option:listen(), lists:filter( fun({{Port, _IP, _Transport}, _Module, _Opts}) -> Port == PortNumber end, AllListeners). get_captcha_transfer_protocol([]) -> throw(<<"The port number mentioned in captcha_host " "is not a ejabberd_http listener with " "'captcha' option. Change the port number " "or specify http:// in that option.">>); get_captcha_transfer_protocol([{_, ejabberd_http, Opts} | Listeners]) -> Handlers = maps:get(request_handlers, Opts, []), case lists:any( fun({_, ?MODULE}) -> true; ({_, _}) -> false end, Handlers) of true -> case maps:get(tls, Opts) of true -> https; false -> http end; false -> get_captcha_transfer_protocol(Listeners) end; get_captcha_transfer_protocol([_ | Listeners]) -> get_captcha_transfer_protocol(Listeners). is_limited(undefined) -> false; is_limited(Limiter) -> case ejabberd_option:captcha_limit() of infinity -> false; Int -> case catch gen_server:call(?MODULE, {is_limited, Limiter, Int}, 5000) of true -> true; false -> false; Err -> ?ERROR_MSG("Call failed: ~p", [Err]), false end end. -define(CMD_TIMEOUT, 5000). -define(MAX_FILE_SIZE, 64 * 1024). -spec cmd(string()) -> {ok, binary()} | {error, image_error()}. cmd(Cmd) -> Port = open_port({spawn, Cmd}, [stream, eof, binary]), TRef = erlang:start_timer(?CMD_TIMEOUT, self(), timeout), recv_data(Port, TRef, <<>>). -spec recv_data(port(), reference(), binary()) -> {ok, binary()} | {error, image_error()}. recv_data(Port, TRef, Buf) -> receive {Port, {data, Bytes}} -> NewBuf = <>, if byte_size(NewBuf) > (?MAX_FILE_SIZE) -> return(Port, TRef, {error, efbig}); true -> recv_data(Port, TRef, NewBuf) end; {Port, {data, _}} -> return(Port, TRef, {error, efbig}); {Port, eof} when Buf /= <<>> -> return(Port, TRef, {ok, Buf}); {Port, eof} -> return(Port, TRef, {error, enodata}); {timeout, TRef, _} -> return(Port, TRef, {error, timeout}) end. -spec return(port(), reference(), {ok, binary()} | {error, image_error()}) -> {ok, binary()} | {error, image_error()}. return(Port, TRef, Result) -> misc:cancel_timer(TRef), catch port_close(Port), Result. is_feature_available() -> case get_prog_name() of Prog when is_binary(Prog) -> true; false -> false end. check_captcha_setup() -> case is_feature_available() of true -> case create_image() of {ok, _, _, _} -> true; Err -> ?CRITICAL_MSG("Captcha is enabled in the option captcha_cmd, " "but it can't generate images.", []), Err end; false -> false end. -spec lookup_captcha(binary()) -> {ok, #captcha{}} | {error, enoent}. lookup_captcha(Id) -> case ets:lookup(captcha, Id) of [C] -> {ok, C}; [] -> {error, enoent} end. -spec check_captcha(binary(), binary()) -> captcha_not_found | captcha_valid | captcha_non_valid. check_captcha(Id, ProvidedKey) -> case lookup_captcha(Id) of {ok, #captcha{pid = Pid, args = Args, key = ValidKey, tref = Tref}} -> ets:delete(captcha, Id), misc:cancel_timer(Tref), if ValidKey == ProvidedKey -> callback(captcha_succeed, Pid, Args), captcha_valid; true -> callback(captcha_failed, Pid, Args), captcha_non_valid end; {error, _} -> captcha_not_found end. -spec clean_treap(treap:treap(), priority()) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; false -> {_Key, Priority, _Value} = treap:get_root(Treap), if Priority > CleanPriority -> clean_treap(treap:delete_root(Treap), CleanPriority); true -> Treap end end. -spec callback(captcha_succeed | captcha_failed, pid() | undefined, callback() | term()) -> any(). callback(Result, _Pid, F) when is_function(F) -> F(Result); callback(Result, Pid, Args) when is_pid(Pid) -> Pid ! {Result, Args}; callback(_, _, _) -> ok. -spec now_priority() -> priority(). now_priority() -> -erlang:system_time(microsecond). ejabberd-21.12/src/ejabberd_system_monitor.erl0000644000232200023220000002332714154362354022045 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_system_monitor.erl %%% Author : Alexey Shchepin %%% Description : ejabberd watchdog %%% Created : 21 Mar 2007 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_system_monitor). -behaviour(gen_event). -author('alexey@process-one.net'). -author('ekhramtsov@process-one.net'). %% API -export([start/0, config_reloaded/0]). %% gen_event callbacks -export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -define(CHECK_INTERVAL, timer:seconds(30)). -record(state, {tref :: undefined | reference(), mref :: undefined | reference()}). -record(proc_stat, {qlen :: non_neg_integer(), memory :: non_neg_integer(), initial_call :: mfa(), current_function :: mfa(), ancestors :: [pid() | atom()], application :: pid() | atom(), name :: pid() | atom()}). -type state() :: #state{}. -type proc_stat() :: #proc_stat{}. -type app_pids() :: #{pid() => atom()}. %%%=================================================================== %%% API %%%=================================================================== -spec start() -> ok. start() -> gen_event:add_handler(alarm_handler, ?MODULE, []), gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {?MODULE, []}), application:load(os_mon), application:set_env(os_mon, start_cpu_sup, false), application:set_env(os_mon, start_os_sup, false), application:set_env(os_mon, start_memsup, true), application:set_env(os_mon, start_disksup, false), ejabberd:start_app(os_mon), set_oom_watermark(). excluded_apps() -> [os_mon, mnesia, sasl, stdlib, kernel]. -spec config_reloaded() -> ok. config_reloaded() -> set_oom_watermark(). %%%=================================================================== %%% gen_event callbacks %%%=================================================================== init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), {ok, #state{}}. handle_event({set_alarm, {system_memory_high_watermark, _}}, State) -> handle_overload(State), {ok, restart_timer(State)}; handle_event({clear_alarm, system_memory_high_watermark}, State) -> misc:cancel_timer(State#state.tref), {ok, State#state{tref = undefined}}; handle_event({set_alarm, {process_memory_high_watermark, Pid}}, State) -> case proc_stat(Pid, get_app_pids()) of #proc_stat{name = Name} = ProcStat -> ?WARNING_MSG( "Process ~p consumes more than 5% of OS memory (~ts)~n", [Name, format_proc(ProcStat)]), handle_overload(State), {ok, State}; _ -> {ok, State} end; handle_event({clear_alarm, process_memory_high_watermark}, State) -> {ok, State}; handle_event(Event, State) -> ?WARNING_MSG("unexpected event: ~p~n", [Event]), {ok, State}. handle_call(_Request, State) -> {ok, {error, badarg}, State}. handle_info({timeout, _TRef, handle_overload}, State) -> handle_overload(State), {ok, restart_timer(State)}; handle_info(Info, State) -> ?WARNING_MSG("unexpected info: ~p~n", [Info]), {ok, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec handle_overload(state()) -> ok. handle_overload(State) -> handle_overload(State, processes()). -spec handle_overload(state(), [pid()]) -> ok. handle_overload(_State, Procs) -> AppPids = get_app_pids(), {TotalMsgs, ProcsNum, Apps, Stats} = overloaded_procs(AppPids, Procs), MaxMsgs = ejabberd_option:oom_queue(), if TotalMsgs >= MaxMsgs -> SortedStats = lists:reverse(lists:keysort(#proc_stat.qlen, Stats)), ?WARNING_MSG( "The system is overloaded with ~b messages " "queued by ~b process(es) (~b%) " "from the following applications: ~ts; " "the top processes are:~n~ts~n", [TotalMsgs, ProcsNum, round(ProcsNum*100/length(Procs)), format_apps(Apps), format_top_procs(SortedStats)]), kill(SortedStats, round(TotalMsgs/ProcsNum)); true -> ok end, lists:foreach(fun erlang:garbage_collect/1, Procs). -spec get_app_pids() -> app_pids(). get_app_pids() -> try application:info() of Info -> case lists:keyfind(running, 1, Info) of {_, Apps} -> lists:foldl( fun({Name, Pid}, M) when is_pid(Pid) -> maps:put(Pid, Name, M); (_, M) -> M end, #{}, Apps); false -> #{} end catch _:_ -> #{} end. -spec overloaded_procs(app_pids(), [pid()]) -> {non_neg_integer(), non_neg_integer(), dict:dict(), [proc_stat()]}. overloaded_procs(AppPids, AllProcs) -> lists:foldl( fun(Pid, {TotalMsgs, ProcsNum, Apps, Stats}) -> case proc_stat(Pid, AppPids) of #proc_stat{qlen = QLen, application = App} = Stat when QLen > 0 -> {TotalMsgs + QLen, ProcsNum + 1, dict:update_counter(App, QLen, Apps), [Stat|Stats]}; _ -> {TotalMsgs, ProcsNum, Apps, Stats} end end, {0, 0, dict:new(), []}, AllProcs). -spec proc_stat(pid(), app_pids()) -> proc_stat() | undefined. proc_stat(Pid, AppPids) -> case process_info(Pid, [message_queue_len, memory, initial_call, current_function, dictionary, group_leader, registered_name]) of [{_, MsgLen}, {_, Mem}, {_, InitCall}, {_, CurrFun}, {_, Dict}, {_, GL}, {_, Name}] -> IntLen = proplists:get_value('$internal_queue_len', Dict, 0), TrueInitCall = proplists:get_value('$initial_call', Dict, InitCall), Ancestors = proplists:get_value('$ancestors', Dict, []), Len = IntLen + MsgLen, App = maps:get(GL, AppPids, kernel), RegName = case Name of [] -> Pid; _ -> Name end, #proc_stat{qlen = Len, memory = Mem, initial_call = TrueInitCall, current_function = CurrFun, ancestors = Ancestors, application = App, name = RegName}; _ -> undefined end. -spec restart_timer(#state{}) -> #state{}. restart_timer(State) -> misc:cancel_timer(State#state.tref), TRef = erlang:start_timer(?CHECK_INTERVAL, self(), handle_overload), State#state{tref = TRef}. -spec format_apps(dict:dict()) -> iodata(). format_apps(Apps) -> AppList = lists:reverse(lists:keysort(2, dict:to_list(Apps))), string:join( [io_lib:format("~p (~b msgs)", [App, Msgs]) || {App, Msgs} <- AppList], ", "). -spec format_top_procs([proc_stat()]) -> iodata(). format_top_procs(Stats) -> Stats1 = lists:sublist(Stats, 5), string:join( lists:map( fun(#proc_stat{name = Name} = Stat) -> [io_lib:format("** ~w: ", [Name]), format_proc(Stat)] end,Stats1), io_lib:nl()). -spec format_proc(proc_stat()) -> iodata(). format_proc(#proc_stat{qlen = Len, memory = Mem, initial_call = InitCall, current_function = CurrFun, ancestors = Ancs, application = App}) -> io_lib:format( "msgs = ~b, memory = ~b, initial_call = ~ts, " "current_function = ~ts, ancestors = ~w, application = ~w", [Len, Mem, format_mfa(InitCall), format_mfa(CurrFun), Ancs, App]). -spec format_mfa(mfa()) -> iodata(). format_mfa({M, F, A}) when is_atom(M), is_atom(F), is_integer(A) -> io_lib:format("~ts:~ts/~b", [M, F, A]); format_mfa(WTF) -> io_lib:format("~w", [WTF]). -spec kill([proc_stat()], non_neg_integer()) -> ok. kill(Stats, Threshold) -> case ejabberd_option:oom_killer() of true -> do_kill(Stats, Threshold); false -> ok end. -spec do_kill([proc_stat()], non_neg_integer()) -> ok. do_kill(Stats, Threshold) -> Killed = lists:filtermap( fun(#proc_stat{qlen = Len, name = Name, application = App}) when Len >= Threshold -> case lists:member(App, excluded_apps()) of true -> ?WARNING_MSG( "Unable to kill process ~p from whitelisted " "application ~p~n", [Name, App]), false; false -> case kill_proc(Name) of false -> false; Pid -> {true, Pid} end end; (_) -> false end, Stats), TotalKilled = length(Killed), if TotalKilled > 0 -> ?ERROR_MSG( "Killed ~b process(es) consuming more than ~b message(s) each~n", [TotalKilled, Threshold]); true -> ok end. -spec kill_proc(pid() | atom()) -> false | pid(). kill_proc(undefined) -> false; kill_proc(Name) when is_atom(Name) -> kill_proc(whereis(Name)); kill_proc(Pid) -> exit(Pid, kill), Pid. -spec set_oom_watermark() -> ok. set_oom_watermark() -> WaterMark = ejabberd_option:oom_watermark(), memsup:set_sysmem_high_watermark(WaterMark/100). ejabberd-21.12/src/ejabberd_sip.erl0000644000232200023220000000552714154362354017547 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_sip.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 30 Apr 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2013-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_sip). -behaviour(ejabberd_listener). -ifndef(SIP). -include("logger.hrl"). -export([accept/1, start/3, start_link/3, listen_options/0]). fail() -> ?CRITICAL_MSG("Listening module ~ts is not available: " "ejabberd is not compiled with SIP support", [?MODULE]), erlang:error(sip_not_compiled). accept(_) -> fail(). listen_options() -> fail(). start(_, _, _) -> fail(). start_link(_, _, _) -> fail(). -else. %% API -export([tcp_init/2, udp_init/2, udp_recv/5, start/3, start_link/3, accept/1]). -export([listen_opt_type/1, listen_options/0]). %%%=================================================================== %%% API %%%=================================================================== tcp_init(Socket, Opts) -> ejabberd:start_app(esip), esip_socket:tcp_init(Socket, set_certfile(Opts)). udp_init(Socket, Opts) -> ejabberd:start_app(esip), esip_socket:udp_init(Socket, Opts). udp_recv(Sock, Addr, Port, Data, Opts) -> esip_socket:udp_recv(Sock, Addr, Port, Data, Opts). start(SockMod, Socket, Opts) -> esip_socket:start({SockMod, Socket}, Opts). start_link(gen_tcp, Sock, Opts) -> esip_socket:start_link(Sock, Opts). accept(_) -> ok. set_certfile(Opts) -> case lists:keymember(certfile, 1, Opts) of true -> Opts; false -> case ejabberd_pkix:get_certfile(ejabberd_config:get_myname()) of {ok, CertFile} -> [{certfile, CertFile}|Opts]; error -> Opts end end. listen_opt_type(certfile) -> econf:pem(). listen_options() -> [{tls, false}, {certfile, undefined}]. %%%=================================================================== %%% Internal functions %%%=================================================================== -endif. ejabberd-21.12/src/mod_proxy65_mnesia.erl0000644000232200023220000001152514154362354020660 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 16 Jan 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_mnesia). -behaviour(gen_server). -behaviour(mod_proxy65). %% API -export([init/0, register_stream/2, unregister_stream/1, activate_stream/4]). -export([start_link/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -record(bytestream, {sha1 = <<"">> :: binary() | '$1', target :: pid() | '_', initiator :: pid() | '_' | undefined, active = false :: boolean() | '_', jid_i :: undefined | binary() | '_'}). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, supervisor:start_child(ejabberd_backend_sup, Spec). register_stream(SHA1, StreamPid) -> F = fun () -> case mnesia:read(bytestream, SHA1, write) of [] -> mnesia:write(#bytestream{sha1 = SHA1, target = StreamPid}); [#bytestream{target = Pid, initiator = undefined} = ByteStream] when is_pid(Pid), Pid /= StreamPid -> mnesia:write(ByteStream#bytestream{ initiator = StreamPid}) end end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, Reason} end. unregister_stream(SHA1) -> F = fun () -> mnesia:delete({bytestream, SHA1}) end, case mnesia:transaction(F) of {atomic, ok} -> ok; {aborted, Reason} -> ?ERROR_MSG("Mnesia transaction failed: ~p", [Reason]), {error, Reason} end. activate_stream(SHA1, Initiator, MaxConnections, _Node) -> case gen_server:call(?MODULE, {activate_stream, SHA1, Initiator, MaxConnections}) of {atomic, {ok, IPid, TPid}} -> {ok, IPid, TPid}; {atomic, {limit, IPid, TPid}} -> {error, {limit, IPid, TPid}}; {atomic, conflict} -> {error, conflict}; {atomic, notfound} -> {error, notfound}; Err -> {error, Err} end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> ejabberd_mnesia:create(?MODULE, bytestream, [{ram_copies, [node()]}, {attributes, record_info(fields, bytestream)}]), {ok, #state{}}. handle_call({activate_stream, SHA1, Initiator, MaxConnections}, _From, State) -> F = fun () -> case mnesia:read(bytestream, SHA1, write) of [#bytestream{target = TPid, initiator = IPid} = ByteStream] when is_pid(TPid), is_pid(IPid) -> ActiveFlag = ByteStream#bytestream.active, if ActiveFlag == false -> ConnsPerJID = mnesia:select( bytestream, [{#bytestream{sha1 = '$1', jid_i = Initiator, _ = '_'}, [], ['$1']}]), if length(ConnsPerJID) < MaxConnections -> mnesia:write( ByteStream#bytestream{active = true, jid_i = Initiator}), {ok, IPid, TPid}; true -> {limit, IPid, TPid} end; true -> conflict end; _ -> notfound end end, Reply = mnesia:transaction(F), {reply, Reply, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/ejabberd_sup.erl0000644000232200023220000000606514154362354017561 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_sup.erl %%% Author : Alexey Shchepin %%% Purpose : Erlang/OTP supervisor %%% Created : 31 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sup). -author('alexey@process-one.net'). -behaviour(supervisor). -export([start_link/0, init/1, stop_child/1]). -define(SHUTDOWN_TIMEOUT, timer:minutes(1)). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> {ok, {{one_for_one, 10, 1}, [worker(ejabberd_systemd), worker(ejabberd_hooks), worker(ejabberd_cluster), worker(translate), worker(ejabberd_access_permissions), worker(ejabberd_ctl), worker(ejabberd_commands), worker(ejabberd_admin), supervisor(ejabberd_listener), worker(ejabberd_pkix), worker(acl), worker(ejabberd_shaper), supervisor(ejabberd_db_sup), supervisor(ejabberd_backend_sup), supervisor(ejabberd_sql_sup), worker(ejabberd_iq), worker(ejabberd_router), worker(ejabberd_router_multicast), worker(ejabberd_local), worker(ejabberd_sm), simple_supervisor(ejabberd_s2s_in), simple_supervisor(ejabberd_s2s_out), worker(ejabberd_s2s), simple_supervisor(ejabberd_service), worker(ejabberd_captcha), worker(ext_mod), supervisor(ejabberd_gen_mod_sup, gen_mod), worker(ejabberd_acme), worker(ejabberd_auth), worker(ejabberd_oauth)]}}. -spec stop_child(atom()) -> ok. stop_child(Name) -> _ = supervisor:terminate_child(?MODULE, Name), _ = supervisor:delete_child(?MODULE, Name), ok. %%%=================================================================== %%% Internal functions %%%=================================================================== worker(Mod) -> {Mod, {Mod, start_link, []}, permanent, ?SHUTDOWN_TIMEOUT, worker, [Mod]}. supervisor(Mod) -> supervisor(Mod, Mod). supervisor(Name, Mod) -> {Name, {Mod, start_link, []}, permanent, infinity, supervisor, [Mod]}. simple_supervisor(Mod) -> Name = list_to_atom(atom_to_list(Mod) ++ "_sup"), {Name, {ejabberd_tmp_sup, start_link, [Name, Mod]}, permanent, infinity, supervisor, [ejabberd_tmp_sup]}. ejabberd-21.12/src/ejabberd_tmp_sup.erl0000644000232200023220000000271614154362354020440 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_tmp_sup.erl %%% Author : Alexey Shchepin %%% Purpose : Supervisor for temporary processes %%% Created : 18 Jul 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_tmp_sup). -author('alexey@process-one.net'). -export([start_link/2, init/1]). start_link(Name, Module) -> supervisor:start_link({local, Name}, ?MODULE, Module). init(Module) -> {ok, {{simple_one_for_one, 10, 1}, [{undefined, {Module, start_link, []}, temporary, 5000, worker, [Module]}]}}. ejabberd-21.12/src/ejabberd_commands_doc.erl0000644000232200023220000007140614154362354021401 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_commands_doc.erl %%% Author : Badlop %%% Purpose : Management of ejabberd commands %%% Created : 20 May 2008 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_commands_doc). -author('pawel@process-one.net'). -export([generate_html_output/3]). -export([generate_md_output/3]). -export([generate_tags_md/1]). -include("ejabberd_commands.hrl"). -define(RAW(V), if HTMLOutput -> fxml:crypt(iolist_to_binary(V)); true -> iolist_to_binary(V) end). -define(TAG(N), if HTMLOutput -> [<<"<", ??N, "/>">>]; true -> md_tag(N, <<"">>) end). -define(TAG(N, V), if HTMLOutput -> [<<"<", ??N, ">">>, V, <<"">>]; true -> md_tag(N, V) end). -define(TAG(N, C, V), if HTMLOutput -> [<<"<", ??N, " class='", C, "'>">>, V, <<"">>]; true -> md_tag(N, V) end). -define(TAG_R(N, V), ?TAG(N, ?RAW(V))). -define(TAG_R(N, C, V), ?TAG(N, C, ?RAW(V))). -define(SPAN(N, V), ?TAG_R(span, ??N, V)). -define(STR(A), ?SPAN(str,[<<"\"">>, A, <<"\"">>])). -define(NUM(A), ?SPAN(num,integer_to_binary(A))). -define(FIELD(A), ?SPAN(field,A)). -define(ID(A), ?SPAN(id,A)). -define(OP(A), ?SPAN(op,A)). -define(ARG(A), ?FIELD(atom_to_list(A))). -define(KW(A), ?SPAN(kw,A)). -define(BR, <<"\n">>). -define(ARG_S(A), ?STR(atom_to_list(A))). -define(RAW_L(A), ?RAW(<>)). -define(STR_L(A), ?STR(<>)). -define(FIELD_L(A), ?FIELD(<>)). -define(ID_L(A), ?ID(<>)). -define(OP_L(A), ?OP(<>)). -define(KW_L(A), ?KW(<>)). -define(STR_A(A), ?STR(atom_to_list(A))). -define(ID_A(A), ?ID(atom_to_list(A))). list_join_with([], _M) -> []; list_join_with([El|Tail], M) -> lists:reverse(lists:foldl(fun(E, Acc) -> [E, M | Acc] end, [El], Tail)). md_tag(dt, V) -> [<<"- ">>, V]; md_tag(dd, V) -> [<<" : ">>, V, <<"\n">>]; md_tag(li, V) -> [<<"- ">>, V, <<"\n">>]; md_tag(pre, V) -> [V, <<"\n">>]; md_tag(p, V) -> [<<"\n\n">>, V, <<"\n">>]; md_tag(h1, V) -> [<<"\n\n## ">>, V, <<"\n">>]; md_tag(h2, V) -> [<<"\n__">>, V, <<"__\n\n">>]; md_tag(strong, V) -> [<<"*">>, V, <<"*">>]; md_tag('div', V) -> [<<"
">>, V, <<"
">>]; md_tag(_, V) -> V. perl_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?NUM(Int)]; perl_gen({Name, string}, Str, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; perl_gen({Name, binary}, Str, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?STR(Str)]; perl_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> [?ARG(Name), ?OP_L(" => "), ?STR_A(Atom)]; perl_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> Res = lists:map(fun({A,B})->perl_gen(A, B, Indent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), [?ARG(Name), ?OP_L(" => {"), list_join_with(Res, [?OP_L(", ")]), ?OP_L("}")]; perl_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Res = lists:map(fun(E) -> [?OP_L("{"), perl_gen(ElDesc, E, Indent, HTMLOutput), ?OP_L("}")] end, List), [?ARG(Name), ?OP_L(" => ["), list_join_with(Res, [?OP_L(", ")]), ?OP_L("]")]. perl_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ perl\n">>} end, [Preamble, Indent, ?ID_L("XMLRPC::Lite"), ?OP_L("->"), ?ID_L("proxy"), ?OP_L("("), ?ID_L("$url"), ?OP_L(")->"), ?ID_L("call"), ?OP_L("("), ?STR_A(Name), ?OP_L(", {"), ?BR, Indent, <<" ">>, list_join_with(lists:map(fun({A,B})->perl_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), ?BR, Indent, ?OP_L("})->"), ?ID_L("results"), ?OP_L("()")]. java_gen_map(Vals, Indent, HTMLOutput) -> {Split, NL} = case Indent of none -> {<<" ">>, <<" ">>}; _ -> {[?BR, <<" ", Indent/binary>>], [?BR, Indent]} end, [?KW_L("new "), ?ID_L("HashMap"), ?OP_L("<"), ?ID_L("String"), ?OP_L(", "), ?ID_L("Object"), ?OP_L(">() {{"), Split, list_join_with(Vals, Split), NL, ?OP_L("}}")]. java_gen({Name, integer}, Int, _Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Integer"), ?OP_L("("), ?NUM(Int), ?OP_L("));")]; java_gen({Name, string}, Str, _Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; java_gen({Name, binary}, Str, _Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR(Str), ?OP_L(");")]; java_gen({Name, atom}, Atom, _Indent, HTMLOutput) -> [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?STR_A(Atom), ?OP_L(");")]; java_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> NewIndent = <<" ", Indent/binary>>, Res = lists:map(fun({A, B}) -> [java_gen(A, B, NewIndent, HTMLOutput)] end, lists:zip(Fields, tuple_to_list(Tuple))), [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), java_gen_map(Res, Indent, HTMLOutput), ?OP_L(")")]; java_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> {NI, NI2, I} = case List of [_] -> {" ", " ", Indent}; _ -> {[?BR, <<" ", Indent/binary>>], [?BR, <<" ", Indent/binary>>], <<" ", Indent/binary>>} end, Res = lists:map(fun(E) -> java_gen_map([java_gen(ElDesc, E, I, HTMLOutput)], none, HTMLOutput) end, List), [?ID_L("put"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), ?KW_L("new "), ?ID_L("Object"), ?OP_L("[] {"), NI, list_join_with(Res, [?OP_L(","), NI]), NI2, ?OP_L("});")]. java_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ java\n">>} end, [Preamble, Indent, ?ID_L("XmlRpcClientConfigImpl config"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClientConfigImpl"), ?OP_L("();"), ?BR, Indent, ?ID_L("config"), ?OP_L("."), ?ID_L("setServerURL"), ?OP_L("("), ?ID_L("url"), ?OP_L(");"), ?BR, Indent, ?BR, Indent, ?ID_L("XmlRpcClient client"), ?OP_L(" = "), ?KW_L("new "), ?ID_L("XmlRpcClient"), ?OP_L("();"), ?BR, Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("setConfig"), ?OP_L("("), ?ID_L("config"), ?OP_L(");"), ?BR, Indent, ?BR, Indent, ?ID_L("client"), ?OP_L("."), ?ID_L("execute"), ?OP_L("("), ?STR_A(Name), ?OP_L(", "), java_gen_map(lists:map(fun({A,B})->java_gen(A, B, Indent, HTMLOutput) end, lists:zip(ArgsDesc, Values)), Indent, HTMLOutput), ?OP_L(");")]. -define(XML_S(N, V), ?OP_L("<"), ?FIELD_L(??N), ?OP_L(">"), V). -define(XML_E(N), ?OP_L("")). -define(XML(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?BR, Indent, ?XML_E(N)). -define(XML(N, Indent, D, V), ?XML(N, [Indent, lists:duplicate(D, <<" ">>)], V)). -define(XML_L(N, Indent, V), ?BR, Indent, ?XML_S(N, V), ?XML_E(N)). -define(XML_L(N, Indent, D, V), ?XML_L(N, [Indent, lists:duplicate(D, <<" ">>)], V)). xml_gen({Name, integer}, Int, Indent, HTMLOutput) -> [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML_L(integer, Indent, 2, ?ID(integer_to_binary(Int)))])])]; xml_gen({Name, string}, Str, Indent, HTMLOutput) -> [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML_L(string, Indent, 2, ?ID(Str))])])]; xml_gen({Name, binary}, Str, Indent, HTMLOutput) -> [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML_L(string, Indent, 2, ?ID(Str))])])]; xml_gen({Name, atom}, Atom, Indent, HTMLOutput) -> [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML_L(string, Indent, 2, ?ID(atom_to_list(Atom)))])])]; xml_gen({Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> NewIndent = <<" ", Indent/binary>>, Res = lists:map(fun({A, B}) -> xml_gen(A, B, NewIndent, HTMLOutput) end, lists:zip(Fields, tuple_to_list(Tuple))), [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML(struct, NewIndent, Res)])])]; xml_gen({Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Ind1 = <<" ", Indent/binary>>, Ind2 = <<" ", Ind1/binary>>, Res = lists:map(fun(E) -> [?XML(value, Ind1, [?XML(struct, Ind1, 1, xml_gen(ElDesc, E, Ind2, HTMLOutput))])] end, List), [?XML(member, Indent, [?XML_L(name, Indent, 1, ?ID_A(Name)), ?XML(value, Indent, 1, [?XML(array, Indent, 2, [?XML(data, Indent, 3, Res)])])])]. xml_call(Name, ArgsDesc, Values, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ xml">>} end, Res = lists:map(fun({A, B}) -> xml_gen(A, B, <>, HTMLOutput) end, lists:zip(ArgsDesc, Values)), [Preamble, ?XML(methodCall, Indent, [?XML_L(methodName, Indent, 1, ?ID_A(Name)), ?XML(params, Indent, 1, [?XML(param, Indent, 2, [?XML(value, Indent, 3, [?XML(struct, Indent, 4, Res)])])])])]. % [?ARG_S(Name), ?OP_L(": "), ?STR(Str)]; json_gen({_Name, integer}, Int, _Indent, HTMLOutput) -> [?NUM(Int)]; json_gen({_Name, string}, Str, _Indent, HTMLOutput) -> [?STR(Str)]; json_gen({_Name, binary}, Str, _Indent, HTMLOutput) -> [?STR(Str)]; json_gen({_Name, atom}, Atom, _Indent, HTMLOutput) -> [?STR_A(Atom)]; json_gen({_Name, rescode}, Val, _Indent, HTMLOutput) -> [?ID_A(Val == ok orelse Val == true)]; json_gen({_Name, restuple}, {Val, Str}, _Indent, HTMLOutput) -> [?OP_L("{"), ?STR_L("res"), ?OP_L(": "), ?ID_A(Val == ok orelse Val == true), ?OP_L(", "), ?STR_L("text"), ?OP_L(": "), ?STR(Str), ?OP_L("}")]; json_gen({_Name, {list, {_, {tuple, [{_, atom}, ValFmt]}}}}, List, Indent, HTMLOutput) -> Indent2 = <<" ", Indent/binary>>, Res = lists:map(fun({N, V})->[?STR_A(N), ?OP_L(": "), json_gen(ValFmt, V, Indent2, HTMLOutput)] end, List), [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; json_gen({_Name, {tuple, Fields}}, Tuple, Indent, HTMLOutput) -> Indent2 = <<" ", Indent/binary>>, Res = lists:map(fun({{N, _} = A, B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, Indent2, HTMLOutput)] end, lists:zip(Fields, tuple_to_list(Tuple))), [?OP_L("{"), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("}")]; json_gen({_Name, {list, ElDesc}}, List, Indent, HTMLOutput) -> Indent2 = <<" ", Indent/binary>>, Res = lists:map(fun(E) -> json_gen(ElDesc, E, Indent2, HTMLOutput) end, List), [?OP_L("["), ?BR, Indent2, list_join_with(Res, [?OP_L(","), ?BR, Indent2]), ?BR, Indent, ?OP_L("]")]. json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput) -> {Indent, Preamble} = if HTMLOutput -> {<<"">>, []}; true -> {<<" ">>, <<"~~~ json\n">>} end, {Code, ResultStr} = case {ResultDesc, Result} of {{_, rescode}, V} when V == true; V == ok -> {200, [?STR_L("")]}; {{_, rescode}, _} -> {500, [?STR_L("")]}; {{_, restuple}, {V1, Text1}} when V1 == true; V1 == ok -> {200, [?STR(Text1)]}; {{_, restuple}, {_, Text2}} -> {500, [?STR(Text2)]}; {{_, {list, _}}, _} -> {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; {{_, {tuple, _}}, _} -> {200, json_gen(ResultDesc, Result, Indent, HTMLOutput)}; {{Name0, _}, _} -> {200, [Indent, ?OP_L("{"), ?STR_A(Name0), ?OP_L(": "), json_gen(ResultDesc, Result, Indent, HTMLOutput), ?OP_L("}")]} end, CodeStr = case Code of 200 -> <<" 200 OK">>; 500 -> <<" 500 Internal Server Error">> end, [Preamble, Indent, ?ID_L("POST /api/"), ?ID_A(Name), ?BR, Indent, ?OP_L("{"), ?BR, Indent, <<" ">>, list_join_with(lists:map(fun({{N,_}=A,B})->[?STR_A(N), ?OP_L(": "), json_gen(A, B, <>, HTMLOutput)] end, lists:zip(ArgsDesc, Values)), [?OP_L(","), ?BR, Indent, <<" ">>]), ?BR, Indent, ?OP_L("}"), ?BR, Indent, ?BR, Indent, ?ID_L("HTTP/1.1"), ?ID(CodeStr), ?BR, Indent, ResultStr ]. generate_example_input({_Name, integer}, {LastStr, LastNum}) -> {LastNum+1, {LastStr, LastNum+1}}; generate_example_input({_Name, string}, {LastStr, LastNum}) -> {string:chars(LastStr+1, 5), {LastStr+1, LastNum}}; generate_example_input({_Name, binary}, {LastStr, LastNum}) -> {iolist_to_binary(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; generate_example_input({_Name, atom}, {LastStr, LastNum}) -> {list_to_atom(string:chars(LastStr+1, 5)), {LastStr+1, LastNum}}; generate_example_input({_Name, rescode}, {LastStr, LastNum}) -> {ok, {LastStr, LastNum}}; generate_example_input({_Name, restuple}, {LastStr, LastNum}) -> {{ok, <<"Success">>}, {LastStr, LastNum}}; generate_example_input({_Name, {tuple, Fields}}, Data) -> {R, D} = lists:foldl(fun(Field, {Res2, Data2}) -> {Res3, Data3} = generate_example_input(Field, Data2), {[Res3 | Res2], Data3} end, {[], Data}, Fields), {list_to_tuple(lists:reverse(R)), D}; generate_example_input({_Name, {list, Desc}}, Data) -> {R1, D1} = generate_example_input(Desc, Data), {R2, D2} = generate_example_input(Desc, D1), {[R1, R2], D2}. gen_calls(#ejabberd_commands{args_example=none, args=ArgsDesc} = C, HTMLOutput, Langs) -> {R, _} = lists:foldl(fun(Arg, {Res, Data}) -> {Res3, Data3} = generate_example_input(Arg, Data), {[Res3 | Res], Data3} end, {[], {$a-1, 0}}, ArgsDesc), gen_calls(C#ejabberd_commands{args_example=lists:reverse(R)}, HTMLOutput, Langs); gen_calls(#ejabberd_commands{result_example=none, result=ResultDesc} = C, HTMLOutput, Langs) -> {R, _} = generate_example_input(ResultDesc, {$a-1, 0}), gen_calls(C#ejabberd_commands{result_example=R}, HTMLOutput, Langs); gen_calls(#ejabberd_commands{args_example=Values, args=ArgsDesc, result_example=Result, result=ResultDesc, name=Name}, HTMLOutput, Langs) -> Perl = perl_call(Name, ArgsDesc, Values, HTMLOutput), Java = java_call(Name, ArgsDesc, Values, HTMLOutput), XML = xml_call(Name, ArgsDesc, Values, HTMLOutput), JSON = json_call(Name, ArgsDesc, Values, ResultDesc, Result, HTMLOutput), if HTMLOutput -> [?TAG(ul, "code-samples-names", [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, <<"Java">>); _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, <<"Perl">>); _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, <<"XML">>); _ -> [] end, case lists:member(<<"json">>, Langs) of true -> ?TAG(li, <<"JSON">>); _ -> [] end]), ?TAG(ul, "code-samples", [case lists:member(<<"java">>, Langs) of true -> ?TAG(li, ?TAG(pre, Java)); _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> ?TAG(li, ?TAG(pre, Perl)); _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> ?TAG(li, ?TAG(pre, XML)); _ -> [] end, case lists:member(<<"json">>, Langs) of true -> ?TAG(li, ?TAG(pre, JSON)); _ -> [] end])]; true -> case Langs of Val when length(Val) == 0 orelse length(Val) == 1 -> [case lists:member(<<"java">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> [<<"\n">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"json">>, Langs) of true -> [<<"\n">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, <<"\n\n">>]; _ -> [<<"\n">>, case lists:member(<<"java">>, Langs) of true -> <<"* Java\n">>; _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> <<"* Perl\n">>; _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> <<"* XmlRPC\n">>; _ -> [] end, case lists:member(<<"json">>, Langs) of true -> <<"* JSON\n">>; _ -> [] end, <<"{: .code-samples-labels}\n">>, case lists:member(<<"java">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Java), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"perl">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, Perl), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"xmlrpc">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, XML), <<"~~~\n">>]; _ -> [] end, case lists:member(<<"json">>, Langs) of true -> [<<"\n* ">>, ?TAG(pre, JSON), <<"~~~\n">>]; _ -> [] end, <<"{: .code-samples-tabs}\n\n">>] end end. format_type({list, {_, {tuple, Els}}}) -> io_lib:format("[~ts]", [format_type({tuple, Els})]); format_type({list, El}) -> io_lib:format("[~ts]", [format_type(El)]); format_type({tuple, Els}) -> Args = [format_type(El) || El <- Els], io_lib:format("{~ts}", [string:join(Args, ", ")]); format_type({Name, Type}) -> io_lib:format("~ts::~ts", [Name, format_type(Type)]); format_type(binary) -> "string"; format_type(atom) -> "string"; format_type(Type) -> io_lib:format("~p", [Type]). gen_param(Name, Type, undefined, HTMLOutput) -> [?TAG(li, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))])]; gen_param(Name, Type, Desc, HTMLOutput) -> [?TAG(dt, [?TAG_R(strong, atom_to_list(Name)), <<" :: ">>, ?RAW(format_type(Type))]), ?TAG(dd, ?RAW(Desc))]. make_tags(HTMLOutput) -> TagsList = ejabberd_commands:get_tags_commands(1000000), lists:map(fun(T) -> gen_tags(T, HTMLOutput) end, TagsList). -dialyzer({no_match, gen_tags/2}). gen_tags({TagName, Commands}, HTMLOutput) -> [?TAG(h1, TagName) | [?TAG(p, ?RAW("* *`"++C++"`*")) || C <- Commands]]. gen_doc(#ejabberd_commands{name=Name, tags=Tags, desc=Desc, longdesc=LongDesc, args=Args, args_desc=ArgsDesc, note=Note, definer=Definer, result=Result, result_desc=ResultDesc}=Cmd, HTMLOutput, Langs) -> try ArgsText = case ArgsDesc of none -> [?TAG(ul, "args-list", [gen_param(AName, Type, undefined, HTMLOutput) || {AName, Type} <- Args])]; _ -> [?TAG(dl, "args-list", [gen_param(AName, Type, ADesc, HTMLOutput) || {{AName, Type}, ADesc} <- lists:zip(Args, ArgsDesc)])] end, ResultText = case Result of {res,rescode} -> [?TAG(dl, [gen_param(res, integer, "Status code (0 on success, 1 otherwise)", HTMLOutput)])]; {res,restuple} -> [?TAG(dl, [gen_param(res, string, "Raw result string", HTMLOutput)])]; {RName, Type} -> case ResultDesc of none -> [?TAG(ul, [gen_param(RName, Type, undefined, HTMLOutput)])]; _ -> [?TAG(dl, [gen_param(RName, Type, ResultDesc, HTMLOutput)])] end end, TagsText = [?RAW("*`"++atom_to_list(Tag)++"`* ") || Tag <- Tags], IsDefinerMod = case Definer of unknown -> true; _ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes))) end, ModuleText = case IsDefinerMod of true -> [?TAG(h2, <<"Module:">>), ?TAG(p, ?RAW("*`"++atom_to_list(Definer)++"`*"))]; false -> [] end, NoteEl = case Note of "" -> []; _ -> ?TAG('div', "note-down", ?RAW(Note)) end, [NoteEl, ?TAG(h1, atom_to_list(Name)), ?TAG(p, ?RAW(Desc)), case LongDesc of "" -> []; _ -> ?TAG(p, ?RAW(LongDesc)) end, ?TAG(h2, <<"Arguments:">>), ArgsText, ?TAG(h2, <<"Result:">>), ResultText, ?TAG(h2, <<"Tags:">>), ?TAG(p, TagsText)] ++ ModuleText ++ [ ?TAG(h2, <<"Examples:">>), gen_calls(Cmd, HTMLOutput, Langs)] catch _:Ex -> throw(iolist_to_binary(io_lib:format( <<"Error when generating documentation for command '~p': ~p">>, [Name, Ex]))) end. find_commands_definitions() -> case code:lib_dir(ejabberd, ebin) of {error, _} -> lists:map(fun({N, _, _}) -> ejabberd_commands:get_command_definition(N) end, ejabberd_commands:list_commands()); Path -> lists:flatmap(fun(P) -> Mod = list_to_atom(filename:rootname(P)), code:ensure_loaded(Mod), Cs = case erlang:function_exported(Mod, get_commands_spec, 0) of true -> apply(Mod, get_commands_spec, []); _ -> [] end, [C#ejabberd_commands{definer = Mod} || C <- Cs] end, filelib:wildcard("*.beam", Path)) end. generate_html_output(File, RegExp, Languages) -> Cmds = find_commands_definitions(), {ok, RE} = re:compile(RegExp), Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse re:run(atom_to_list(Module), RE, [{capture, none}]) == match end, Cmds), Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> N1 =< N2 end, Cmds2), Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], Langs = binary:split(Languages, <<",">>, [global]), Out = lists:map(fun(C) -> gen_doc(C, true, Langs) end, Cmds4), {ok, Fh} = file:open(File, [write]), io:format(Fh, "~ts", [[html_pre(), Out, html_post()]]), file:close(Fh), ok. maybe_add_policy_arguments(#ejabberd_commands{args=Args1, policy=user}=Cmd) -> Args2 = [{user, binary}, {server, binary} | Args1], Cmd#ejabberd_commands{args = Args2}; maybe_add_policy_arguments(Cmd) -> Cmd. generate_md_output(File, RegExp, Languages) -> Cmds = find_commands_definitions(), {ok, RE} = re:compile(RegExp), Cmds2 = lists:filter(fun(#ejabberd_commands{name=Name, module=Module}) -> re:run(atom_to_list(Name), RE, [{capture, none}]) == match orelse re:run(atom_to_list(Module), RE, [{capture, none}]) == match end, Cmds), Cmds3 = lists:sort(fun(#ejabberd_commands{name=N1}, #ejabberd_commands{name=N2}) -> N1 =< N2 end, Cmds2), Cmds4 = [maybe_add_policy_arguments(Cmd) || Cmd <- Cmds3], Langs = binary:split(Languages, <<",">>, [global]), Header = <<"---\ntitle: Administration API reference\ntoc: true\nmenu: API Reference\norder: 1\n" "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_commands'\n---\n\n" "This section describes API of ejabberd.\n">>, Out = lists:map(fun(C) -> gen_doc(C, false, Langs) end, Cmds4), {ok, Fh} = file:open(File, [write]), io:format(Fh, "~ts~ts", [Header, Out]), file:close(Fh), ok. generate_tags_md(File) -> Header = <<"---\ntitle: API Tags\ntoc: true\nmenu: API Tags\norder: 2\n" "// Autogenerated with 'ejabberdctl gen_markdown_doc_for_tags'\n---\n\n" "This section enumerates the tags and their associated API.\n">>, Tags = make_tags(false), {ok, Fh} = file:open(File, [write]), io:format(Fh, "~ts~ts", [Header, Tags]), file:close(Fh), ok. html_pre() -> " ". html_post() -> " ". ejabberd-21.12/src/mod_shared_roster_mnesia.erl0000644000232200023220000001432714154362354022173 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_shared_roster_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_shared_roster_mnesia). -behaviour(mod_shared_roster). %% API -export([init/2, list_groups/1, groups_with_opts/1, create_group/3, delete_group/2, get_group_opts/2, set_group_opts/3, get_user_groups/2, get_group_explicit_users/2, get_user_displayed_groups/3, is_user_in_group/3, add_user_to_group/3, remove_user_from_group/3, import/3]). -export([need_transform/1, transform/1, use_cache/1]). -include("mod_roster.hrl"). -include("mod_shared_roster.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, sr_group, [{disc_copies, [node()]}, {attributes, record_info(fields, sr_group)}]), ejabberd_mnesia:create(?MODULE, sr_user, [{disc_copies, [node()]}, {type, bag}, {attributes, record_info(fields, sr_user)}, {index, [group_host]}]). list_groups(Host) -> mnesia:dirty_select(sr_group, [{#sr_group{group_host = {'$1', '$2'}, _ = '_'}, [{'==', '$2', Host}], ['$1']}]). -spec use_cache(binary()) -> boolean(). use_cache(_Host) -> false. groups_with_opts(Host) -> Gs = mnesia:dirty_select(sr_group, [{#sr_group{group_host = {'$1', Host}, opts = '$2', _ = '_'}, [], [['$1', '$2']]}]), lists:map(fun ([G, O]) -> {G, O} end, Gs). create_group(Host, Group, Opts) -> R = #sr_group{group_host = {Group, Host}, opts = Opts}, F = fun () -> mnesia:write(R) end, mnesia:transaction(F). delete_group(Host, Group) -> GroupHost = {Group, Host}, F = fun () -> mnesia:delete({sr_group, GroupHost}), Users = mnesia:index_read(sr_user, GroupHost, #sr_user.group_host), lists:foreach(fun (UserEntry) -> mnesia:delete_object(UserEntry) end, Users) end, mnesia:transaction(F). get_group_opts(Host, Group) -> case catch mnesia:dirty_read(sr_group, {Group, Host}) of [#sr_group{opts = Opts}] -> {ok, Opts}; _ -> error end. set_group_opts(Host, Group, Opts) -> R = #sr_group{group_host = {Group, Host}, opts = Opts}, F = fun () -> mnesia:write(R) end, mnesia:transaction(F). get_user_groups(US, Host) -> case catch mnesia:dirty_read(sr_user, US) of Rs when is_list(Rs) -> [Group || #sr_user{group_host = {Group, H}} <- Rs, H == Host]; _ -> [] end. get_group_explicit_users(Host, Group) -> Read = (catch mnesia:dirty_index_read(sr_user, {Group, Host}, #sr_user.group_host)), case Read of Rs when is_list(Rs) -> [R#sr_user.us || R <- Rs]; _ -> [] end. get_user_displayed_groups(LUser, LServer, GroupsOpts) -> case catch mnesia:dirty_read(sr_user, {LUser, LServer}) of Rs when is_list(Rs) -> [{Group, proplists:get_value(Group, GroupsOpts, [])} || #sr_user{group_host = {Group, H}} <- Rs, H == LServer]; _ -> [] end. is_user_in_group(US, Group, Host) -> case mnesia:dirty_match_object( #sr_user{us = US, group_host = {Group, Host}}) of [] -> false; _ -> true end. add_user_to_group(Host, US, Group) -> R = #sr_user{us = US, group_host = {Group, Host}}, F = fun () -> mnesia:write(R) end, mnesia:transaction(F). remove_user_from_group(Host, US, Group) -> R = #sr_user{us = US, group_host = {Group, Host}}, F = fun () -> mnesia:delete_object(R) end, mnesia:transaction(F). import(LServer, <<"sr_group">>, [Group, SOpts, _TimeStamp]) -> G = #sr_group{group_host = {Group, LServer}, opts = ejabberd_sql:decode_term(SOpts)}, mnesia:dirty_write(G); import(LServer, <<"sr_user">>, [SJID, Group, _TimeStamp]) -> #jid{luser = U, lserver = S} = jid:decode(SJID), User = #sr_user{us = {U, S}, group_host = {Group, LServer}}, mnesia:dirty_write(User). need_transform({sr_group, {G, H}, _}) when is_list(G) orelse is_list(H) -> ?INFO_MSG("Mnesia table 'sr_group' will be converted to binary", []), true; need_transform({sr_user, {U, S}, {G, H}}) when is_list(U) orelse is_list(S) orelse is_list(G) orelse is_list(H) -> ?INFO_MSG("Mnesia table 'sr_user' will be converted to binary", []), true; need_transform({sr_group, {_, _}, [{name, _} | _]}) -> ?INFO_MSG("Mnesia table 'sr_group' will be converted from option Name to Label", []), true; need_transform(_) -> false. transform(#sr_group{group_host = {G, _H}, opts = Opts} = R) when is_binary(G) -> Opts2 = case proplists:get_value(name, Opts, false) of false -> Opts; Name -> [{label, Name} | proplists:delete(name, Opts)] end, R#sr_group{opts = Opts2}; transform(#sr_group{group_host = {G, H}, opts = Opts} = R) -> R#sr_group{group_host = {iolist_to_binary(G), iolist_to_binary(H)}, opts = mod_shared_roster:opts_to_binary(Opts)}; transform(#sr_user{us = {U, S}, group_host = {G, H}} = R) -> R#sr_user{us = {iolist_to_binary(U), iolist_to_binary(S)}, group_host = {iolist_to_binary(G), iolist_to_binary(H)}}. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_http_upload_quota_opt.erl0000644000232200023220000000201514154362354022400 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_http_upload_quota_opt). -export([access_hard_quota/1]). -export([access_soft_quota/1]). -export([max_days/1]). -spec access_hard_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_hard_quota(Opts) when is_map(Opts) -> gen_mod:get_opt(access_hard_quota, Opts); access_hard_quota(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, access_hard_quota). -spec access_soft_quota(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_soft_quota(Opts) when is_map(Opts) -> gen_mod:get_opt(access_soft_quota, Opts); access_soft_quota(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, access_soft_quota). -spec max_days(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_days(Opts) when is_map(Opts) -> gen_mod:get_opt(max_days, Opts); max_days(Host) -> gen_mod:get_module_opt(Host, mod_http_upload_quota, max_days). ejabberd-21.12/src/mod_vcard_xupdate.erl0000644000232200023220000002301114154362354020612 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_vcard_xupdate.erl %%% Author : Igor Goryachev %%% Purpose : Add avatar hash in presence on behalf of client (XEP-0153) %%% Created : 9 Mar 2007 by Igor Goryachev %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_vcard_xupdate). -behaviour(gen_mod). -protocol({xep, 398, '0.2.0'}). %% gen_mod callbacks -export([start/2, stop/1, reload/3]). -export([update_presence/1, vcard_set/1, remove_user/2, mod_doc/0, user_send_packet/1, mod_opt_type/1, mod_options/1, depends/2]). %% API -export([compute_hash/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(VCARD_XUPDATE_CACHE, vcard_xupdate_cache). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> init_cache(Host, Opts), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, update_presence, 100), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 50), ejabberd_hooks:add(vcard_iq_set, Host, ?MODULE, vcard_set, 90), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). stop(Host) -> ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, update_presence, 100), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 50), ejabberd_hooks:delete(vcard_iq_set, Host, ?MODULE, vcard_set, 90), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). reload(Host, NewOpts, _OldOpts) -> init_cache(Host, NewOpts). depends(_Host, _Opts) -> [{mod_vcard, hard}]. %%==================================================================== %% Hooks %%==================================================================== -spec update_presence({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. update_presence({#presence{type = available} = Pres, #{jid := #jid{luser = LUser, lserver = LServer}} = State}) -> case xmpp:get_subtag(Pres, #vcard_xupdate{}) of #vcard_xupdate{hash = <<>>} -> %% XEP-0398 forbids overwriting vcard:x:update %% tags with empty element {Pres, State}; _ -> Pres1 = case get_xupdate(LUser, LServer) of undefined -> xmpp:remove_subtag(Pres, #vcard_xupdate{}); XUpdate -> xmpp:set_subtag(Pres, XUpdate) end, {Pres1, State} end; update_presence(Acc) -> Acc. -spec user_send_packet({presence(), ejabberd_c2s:state()}) -> {presence(), ejabberd_c2s:state()}. user_send_packet({#presence{type = available, to = #jid{luser = U, lserver = S, lresource = <<"">>}}, #{jid := #jid{luser = U, lserver = S}}} = Acc) -> %% This is processed by update_presence/2 explicitly, we don't %% want to call this multiple times for performance reasons Acc; user_send_packet(Acc) -> update_presence(Acc). -spec vcard_set(iq()) -> iq(). vcard_set(#iq{from = #jid{luser = LUser, lserver = LServer}} = IQ) -> ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}), ejabberd_sm:force_update_presence({LUser, LServer}), IQ; vcard_set(Acc) -> Acc. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), ets_cache:delete(?VCARD_XUPDATE_CACHE, {LUser, LServer}). %%==================================================================== %% Storage %%==================================================================== -spec get_xupdate(binary(), binary()) -> vcard_xupdate() | undefined. get_xupdate(LUser, LServer) -> Result = case use_cache(LServer) of true -> ets_cache:lookup( ?VCARD_XUPDATE_CACHE, {LUser, LServer}, fun() -> db_get_xupdate(LUser, LServer) end); false -> db_get_xupdate(LUser, LServer) end, case Result of {ok, external} -> undefined; {ok, Hash} -> #vcard_xupdate{hash = Hash}; error -> #vcard_xupdate{} end. -spec db_get_xupdate(binary(), binary()) -> {ok, binary() | external} | error. db_get_xupdate(LUser, LServer) -> case mod_vcard:get_vcard(LUser, LServer) of [VCard] -> {ok, compute_hash(VCard)}; _ -> error end. -spec init_cache(binary(), gen_mod:opts()) -> ok. init_cache(Host, Opts) -> case use_cache(Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?VCARD_XUPDATE_CACHE, CacheOpts); false -> ets_cache:delete(?VCARD_XUPDATE_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_vcard_xupdate_opt:cache_size(Opts), CacheMissed = mod_vcard_xupdate_opt:cache_missed(Opts), LifeTime = mod_vcard_xupdate_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(binary()) -> boolean(). use_cache(Host) -> mod_vcard_xupdate_opt:use_cache(Host). -spec compute_hash(xmlel()) -> binary() | external. compute_hash(VCard) -> case fxml:get_subtag(VCard, <<"PHOTO">>) of false -> <<>>; Photo -> try xmpp:decode(Photo, ?NS_VCARD, []) of #vcard_photo{binval = <<_, _/binary>> = BinVal} -> str:sha(BinVal); #vcard_photo{extval = <<_, _/binary>>} -> external; _ -> <<>> catch _:{xmpp_codec, _} -> <<>> end end. %%==================================================================== %% Options %%==================================================================== mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("The user's client can store an avatar in the " "user vCard. The vCard-Based Avatars protocol " "(https://xmpp.org/extensions/xep-0153.html[XEP-0153]) " "provides a method for clients to inform the contacts " "what is the avatar hash value. However, simple or small " "clients may not implement that protocol."), "", ?T("If this module is enabled, all the outgoing client presence " "stanzas get automatically the avatar hash on behalf of the " "client. So, the contacts receive the presence stanzas with " "the 'Update Data' described in " "https://xmpp.org/extensions/xep-0153.html[XEP-0153] as if the " "client would had inserted it itself. If the client had already " "included such element in the presence stanza, it is replaced " "with the element generated by ejabberd."), "", ?T("By enabling this module, each vCard modification produces " "a hash recalculation, and each presence sent by a client " "produces hash retrieval and a presence stanza rewrite. " "For this reason, enabling this module will introduce a " "computational overhead in servers with clients that change " "frequently their presence. However, the overhead is significantly " "reduced by the use of caching, so you probably don't want " "to set 'use_cache' to 'false'."), "", ?T("The module depends on _`mod_vcard`_."), "", ?T("NOTE: Nowadays https://xmpp.org/extensions/xep-0153.html" "[XEP-0153] is used mostly as \"read-only\", i.e. modern " "clients don't publish their avatars inside vCards. Thus " "in the majority of cases the module is only used along " "with _`mod_avatar`_ for providing backward compatibility.")], opts => [{use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. ejabberd-21.12/src/mod_vcard_mnesia_opt.erl0000644000232200023220000000060314154362354021300 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_vcard_mnesia_opt). -export([search_all_hosts/1]). -spec search_all_hosts(gen_mod:opts() | global | binary()) -> boolean(). search_all_hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(search_all_hosts, Opts); search_all_hosts(Host) -> gen_mod:get_module_opt(Host, mod_vcard, search_all_hosts). ejabberd-21.12/src/mod_register_opt.erl0000644000232200023220000000546414154362354020503 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_register_opt). -export([access/1]). -export([access_from/1]). -export([access_remove/1]). -export([allow_modules/1]). -export([captcha_protected/1]). -export([ip_access/1]). -export([password_strength/1]). -export([redirect_url/1]). -export([registration_watchers/1]). -export([welcome_message/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_register, access). -spec access_from(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access_from(Opts) when is_map(Opts) -> gen_mod:get_opt(access_from, Opts); access_from(Host) -> gen_mod:get_module_opt(Host, mod_register, access_from). -spec access_remove(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_remove(Opts) when is_map(Opts) -> gen_mod:get_opt(access_remove, Opts); access_remove(Host) -> gen_mod:get_module_opt(Host, mod_register, access_remove). -spec allow_modules(gen_mod:opts() | global | binary()) -> 'all' | [atom()]. allow_modules(Opts) when is_map(Opts) -> gen_mod:get_opt(allow_modules, Opts); allow_modules(Host) -> gen_mod:get_module_opt(Host, mod_register, allow_modules). -spec captcha_protected(gen_mod:opts() | global | binary()) -> boolean(). captcha_protected(Opts) when is_map(Opts) -> gen_mod:get_opt(captcha_protected, Opts); captcha_protected(Host) -> gen_mod:get_module_opt(Host, mod_register, captcha_protected). -spec ip_access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). ip_access(Opts) when is_map(Opts) -> gen_mod:get_opt(ip_access, Opts); ip_access(Host) -> gen_mod:get_module_opt(Host, mod_register, ip_access). -spec password_strength(gen_mod:opts() | global | binary()) -> number(). password_strength(Opts) when is_map(Opts) -> gen_mod:get_opt(password_strength, Opts); password_strength(Host) -> gen_mod:get_module_opt(Host, mod_register, password_strength). -spec redirect_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). redirect_url(Opts) when is_map(Opts) -> gen_mod:get_opt(redirect_url, Opts); redirect_url(Host) -> gen_mod:get_module_opt(Host, mod_register, redirect_url). -spec registration_watchers(gen_mod:opts() | global | binary()) -> [jid:jid()]. registration_watchers(Opts) when is_map(Opts) -> gen_mod:get_opt(registration_watchers, Opts); registration_watchers(Host) -> gen_mod:get_module_opt(Host, mod_register, registration_watchers). -spec welcome_message(gen_mod:opts() | global | binary()) -> {binary(),binary()}. welcome_message(Opts) when is_map(Opts) -> gen_mod:get_opt(welcome_message, Opts); welcome_message(Host) -> gen_mod:get_module_opt(Host, mod_register, welcome_message). ejabberd-21.12/src/win32_dns.erl0000644000232200023220000001062314154362354016735 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : win32_dns.erl %%% Author : Geoff Cant %%% Purpose : Get name servers in a Windows machine %%% Created : 5 Mar 2009 by Geoff Cant %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(win32_dns). -export([get_nameservers/0]). -include("logger.hrl"). -define(IF_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters\\Interfaces"). -define(TOP_KEY, "\\hklm\\system\\CurrentControlSet\\Services\\TcpIp\\Parameters"). get_nameservers() -> {_, Config} = pick_config(), IPTs = get_value(["NameServer"], Config), lists:filter(fun(IPTuple) -> is_good_ns(IPTuple) end, IPTs). is_good_ns(Addr) -> element(1, inet_res:nnslookup("a.root-servers.net", in, a, [{Addr,53}], timer:seconds(5) ) ) =:= ok. reg() -> {ok, R} = win32reg:open([read]), R. interfaces(R) -> ok = win32reg:change_key(R, ?IF_KEY), {ok, I} = win32reg:sub_keys(R), I. config_keys(R, Key) -> ok = win32reg:change_key(R, Key), [ {K, case win32reg:value(R, K) of {ok, V} -> try_translate(K, V); _ -> undefined end } || K <- ["Domain", "DhcpDomain", "NameServer", "DhcpNameServer", "SearchList"]]. try_translate(K, V) -> try translate(K, V) of Res -> Res catch A:B -> ?ERROR_MSG("Error '~p' translating Win32 registry~n" "K: ~p~nV: ~p~nError: ~p", [A, K, V, B]), undefined end. translate(NS, V) when NS =:= "NameServer"; NS =:= "DhcpNameServer" -> %% The IPs may be separated by commas ',' or by spaces " " %% The parts of an IP are separated by dots '.' IPsStrings = [string:tokens(IP, ".") || IP <- string:tokens(V, " ,")], [ list_to_tuple([list_to_integer(String) || String <- IpStrings]) || IpStrings <- IPsStrings]; translate(_, V) -> V. interface_configs(R) -> [{If, config_keys(R, ?IF_KEY ++ "\\" ++ If)} || If <- interfaces(R)]. sort_configs(Configs) -> lists:sort(fun ({_, A}, {_, B}) -> ANS = proplists:get_value("NameServer", A), BNS = proplists:get_value("NameServer", B), if ANS =/= undefined, BNS =:= undefined -> false; true -> count_undef(A) < count_undef(B) end end, Configs). count_undef(L) when is_list(L) -> lists:foldl(fun ({_K, undefined}, Acc) -> Acc +1; ({_K, []}, Acc) -> Acc +1; (_, Acc) -> Acc end, 0, L). all_configs() -> R = reg(), TopConfig = config_keys(R, ?TOP_KEY), Configs = [{top, TopConfig} | interface_configs(R)], win32reg:close(R), {TopConfig, Configs}. pick_config() -> {TopConfig, Configs} = all_configs(), NSConfigs = [{If, C} || {If, C} <- Configs, get_value(["DhcpNameServer","NameServer"], C) =/= undefined], case get_value(["DhcpNameServer","NameServer"], TopConfig) of %% No top level nameserver to pick interface with undefined -> hd(sort_configs(NSConfigs)); %% Top level has a nameserver - use this to select an interface. NS -> Cs = [ {If, C} || {If, C} <- Configs, lists:member(NS, [get_value(["NameServer"], C), get_value(["DhcpNameServer"], C)])], hd(sort_configs(Cs)) end. get_value([], _Config) -> undefined; get_value([K|Keys], Config) -> case proplists:get_value(K, Config) of undefined -> get_value(Keys, Config); V -> V end. ejabberd-21.12/src/proxy_protocol.erl0000644000232200023220000001266214154362354020236 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_http.erl %%% Author : Paweł Chmielowski %%% Purpose : %%% Created : 27 Nov 2018 by Paweł Chmielowski %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(proxy_protocol). -author("pawel@process-one.net"). %% API -export([decode/3]). decode(SockMod, Socket, Timeout) -> V = SockMod:recv(Socket, 6, Timeout), case V of {ok, <<"PROXY ">>} -> decode_v1(SockMod, Socket, Timeout); {ok, <<16#0d, 16#0a, 16#0d, 16#0a, 16#00, 16#0d>>} -> decode_v2(SockMod, Socket, Timeout); _ -> {error, eproto} end. decode_v1(SockMod, Socket, Timeout) -> case read_until_rn(SockMod, Socket, <<>>, false, Timeout) of {error, _} = Err -> Err; Val -> case binary:split(Val, <<" ">>, [global]) of [<<"TCP4">>, SAddr, DAddr, SPort, DPort] -> try {inet_parse:ipv4strict_address(binary_to_list(SAddr)), inet_parse:ipv4strict_address(binary_to_list(DAddr)), binary_to_integer(SPort), binary_to_integer(DPort)} of {{ok, DA}, {ok, SA}, DP, SP} -> {{SA, SP}, {DA, DP}}; _ -> {error, eproto} catch error:badarg -> {error, eproto} end; [<<"TCP6">>, SAddr, DAddr, SPort, DPort] -> try {inet_parse:ipv6strict_address(binary_to_list(SAddr)), inet_parse:ipv6strict_address(binary_to_list(DAddr)), binary_to_integer(SPort), binary_to_integer(DPort)} of {{ok, DA}, {ok, SA}, DP, SP} -> {{SA, SP}, {DA, DP}}; _ -> {error, eproto} catch error:badarg -> {error, eproto} end; [<<"UNKNOWN">> | _] -> {undefined, undefined} end end. decode_v2(SockMod, Socket, Timeout) -> case SockMod:recv(Socket, 10, Timeout) of {error, _} = Err -> Err; {ok, <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, 2:4, Command:4, Transport:8, AddrLen:16/big-unsigned-integer>>} -> case SockMod:recv(Socket, AddrLen, Timeout) of {error, _} = Err -> Err; {ok, Data} -> case Command of 0 -> case {inet:sockname(Socket), inet:peername(Socket)} of {{ok, SA}, {ok, DA}} -> {SA, DA}; {{error, _} = E, _} -> E; {_, {error, _} = E} -> E end; 1 -> case Transport of % UNSPEC or UNIX V when V == 0; V == 16#31; V == 16#32 -> {{unknown, unknown}, {unknown, unknown}}; % IPV4 over TCP or UDP V when V == 16#11; V == 16#12 -> case Data of <> -> {{{S1, S2, S3, S4}, SP}, {{D1, D2, D3, D4}, DP}}; _ -> {error, eproto} end; % IPV6 over TCP or UDP V when V == 16#21; V == 16#22 -> case Data of <> -> {{{S1, S2, S3, S4, S5, S6, S7, S8}, SP}, {{D1, D2, D3, D4, D5, D6, D7, D8}, DP}}; _ -> {error, eproto} end end; _ -> {error, eproto} end end; <<16#0a, 16#51, 16#55, 16#49, 16#54, 16#0a, _/binary>> -> {error, eproto}; _ -> {error, eproto} end. read_until_rn(_SockMod, _Socket, Data, _, _) when size(Data) > 107 -> {error, eproto}; read_until_rn(SockMod, Socket, Data, true, Timeout) -> case SockMod:recv(Socket, 1, Timeout) of {ok, <<"\n">>} -> Data; {ok, <<"\r">>} -> read_until_rn(SockMod, Socket, <>, true, Timeout); {ok, Other} -> read_until_rn(SockMod, Socket, <>, false, Timeout); {error, _} = Err -> Err end; read_until_rn(SockMod, Socket, Data, false, Timeout) -> case SockMod:recv(Socket, 2, Timeout) of {ok, <<"\r\n">>} -> Data; {ok, <>} -> read_until_rn(SockMod, Socket, <>, true, Timeout); {ok, Other} -> read_until_rn(SockMod, Socket, <>, false, Timeout); {error, _} = Err -> Err end. ejabberd-21.12/src/mod_s2s_dialback_opt.erl0000644000232200023220000000052614154362354021172 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_s2s_dialback_opt). -export([access/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_s2s_dialback, access). ejabberd-21.12/src/gen_mod.erl0000644000232200023220000004650314154362354016545 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : gen_mod.erl %%% Author : Alexey Shchepin %%% Purpose : %%% Created : 24 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(gen_mod). -behaviour(supervisor). -author('alexey@process-one.net'). -export([init/1, start_link/0, start_child/3, start_child/4, stop_child/1, stop_child/2, stop/0, config_reloaded/0]). -export([start_module/2, stop_module/2, stop_module_keep_config/2, get_opt/2, set_opt/3, get_opt_hosts/1, is_equal_opt/3, get_module_opt/3, get_module_opts/2, get_module_opt_hosts/2, loaded_modules/1, loaded_modules_with_opts/1, get_hosts/2, get_module_proc/2, is_loaded/2, is_loaded_elsewhere/2, start_modules/0, start_modules/1, stop_modules/0, stop_modules/1, db_mod/2, ram_db_mod/2]). -export([validate/2]). %% Deprecated functions %% update_module/3 is used by test suite ONLY -export([update_module/3]). -deprecated([{update_module, 3}]). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include("ejabberd_stacktrace.hrl"). -record(ejabberd_module, {module_host = {undefined, <<"">>} :: {atom(), binary()}, opts = [] :: opts() | '_' | '$2', order = 0 :: integer()}). -type opts() :: #{atom() => term()}. -type db_type() :: atom(). -type opt_desc() :: #{desc => binary() | [binary()], value => string() | binary()}. -type opt_doc() :: {atom(), opt_desc()} | {atom(), opt_desc(), [opt_doc()]}. -callback start(binary(), opts()) -> ok | {ok, pid()} | {error, term()}. -callback stop(binary()) -> any(). -callback reload(binary(), opts(), opts()) -> ok | {ok, pid()} | {error, term()}. -callback mod_opt_type(atom()) -> econf:validator(). -callback mod_options(binary()) -> [{atom(), term()} | atom()]. -callback mod_doc() -> #{desc => binary() | [binary()], opts => [opt_doc()], example => [string()] | [{binary(), [string()]}]}. -callback depends(binary(), opts()) -> [{module(), hard | soft}]. -optional_callbacks([mod_opt_type/1, reload/3]). -export_type([opts/0]). -export_type([db_type/0]). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. start_link() -> case supervisor:start_link({local, ejabberd_gen_mod_sup}, ?MODULE, []) of {ok, Pid} -> start_modules(), {ok, Pid}; Err -> Err end. init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 60), ejabberd_hooks:add(host_up, ?MODULE, start_modules, 40), ejabberd_hooks:add(host_down, ?MODULE, stop_modules, 70), ets:new(ejabberd_modules, [named_table, public, {keypos, #ejabberd_module.module_host}, {read_concurrency, true}]), {ok, {{one_for_one, 10, 1}, []}}. -spec stop() -> ok. stop() -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 60), ejabberd_hooks:delete(host_up, ?MODULE, start_modules, 40), ejabberd_hooks:delete(host_down, ?MODULE, stop_modules, 70), stop_modules(), ejabberd_sup:stop_child(ejabberd_gen_mod_sup). -spec start_child(module(), binary(), opts()) -> {ok, pid()} | {error, any()}. start_child(Mod, Host, Opts) -> start_child(Mod, Host, Opts, get_module_proc(Host, Mod)). -spec start_child(module(), binary(), opts(), atom()) -> {ok, pid()} | {error, any()}. start_child(Mod, Host, Opts, Proc) -> Spec = {Proc, {?GEN_SERVER, start_link, [{local, Proc}, Mod, [Host, Opts], ejabberd_config:fsm_limit_opts([])]}, transient, timer:minutes(1), worker, [Mod]}, supervisor:start_child(ejabberd_gen_mod_sup, Spec). -spec stop_child(module(), binary()) -> ok | {error, any()}. stop_child(Mod, Host) -> stop_child(get_module_proc(Host, Mod)). -spec stop_child(atom()) -> ok | {error, any()}. stop_child(Proc) -> supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). -spec start_modules() -> any(). start_modules() -> Hosts = ejabberd_option:hosts(), ?INFO_MSG("Loading modules for ~ts", [misc:format_hosts_list(Hosts)]), lists:foreach(fun start_modules/1, Hosts). -spec start_modules(binary()) -> ok. start_modules(Host) -> Modules = ejabberd_option:modules(Host), lists:foreach( fun({Module, Opts, Order}) -> start_module(Host, Module, Opts, Order) end, Modules). -spec start_module(binary(), atom()) -> ok | {ok, pid()} | {error, not_found_in_config}. start_module(Host, Module) -> Modules = ejabberd_option:modules(Host), case lists:keyfind(Module, 1, Modules) of {_, Opts, Order} -> start_module(Host, Module, Opts, Order); false -> {error, not_found_in_config} end. -spec start_module(binary(), atom(), opts(), integer()) -> ok | {ok, pid()}. start_module(Host, Module, Opts, Order) -> ?DEBUG("Loading ~ts at ~ts", [Module, Host]), store_options(Host, Module, Opts, Order), try case Module:start(Host, Opts) of ok -> ok; {ok, Pid} when is_pid(Pid) -> {ok, Pid}; Err -> ets:delete(ejabberd_modules, {Module, Host}), erlang:error({bad_return, Module, Err}) end catch ?EX_RULE(Class, Reason, Stack) -> StackTrace = ?EX_STACK(Stack), ets:delete(ejabberd_modules, {Module, Host}), ErrorText = format_module_error( Module, start, 2, Opts, Class, Reason, StackTrace), ?CRITICAL_MSG(ErrorText, []), maybe_halt_ejabberd(), erlang:raise(Class, Reason, StackTrace) end. -spec reload_modules(binary()) -> ok. reload_modules(Host) -> NewMods = ejabberd_option:modules(Host), OldMods = lists:reverse(loaded_modules_with_opts(Host)), lists:foreach( fun({Mod, _Opts}) -> case lists:keymember(Mod, 1, NewMods) of false -> stop_module(Host, Mod); true -> ok end end, OldMods), lists:foreach( fun({Mod, Opts, Order}) -> case lists:keymember(Mod, 1, OldMods) of false -> start_module(Host, Mod, Opts, Order); true -> ok end end, NewMods), lists:foreach( fun({Mod, OldOpts}) -> case lists:keyfind(Mod, 1, NewMods) of {_, NewOpts, Order} -> if OldOpts /= NewOpts -> reload_module(Host, Mod, NewOpts, OldOpts, Order); true -> ok end; _ -> ok end end, OldMods). -spec reload_module(binary(), module(), opts(), opts(), integer()) -> ok | {ok, pid()}. reload_module(Host, Module, NewOpts, OldOpts, Order) -> case erlang:function_exported(Module, reload, 3) of true -> ?DEBUG("Reloading ~ts at ~ts", [Module, Host]), store_options(Host, Module, NewOpts, Order), try case Module:reload(Host, NewOpts, OldOpts) of ok -> ok; {ok, Pid} when is_pid(Pid) -> {ok, Pid}; Err -> erlang:error({bad_return, Module, Err}) end catch ?EX_RULE(Class, Reason, Stack) -> StackTrace = ?EX_STACK(Stack), ErrorText = format_module_error( Module, reload, 3, NewOpts, Class, Reason, StackTrace), ?CRITICAL_MSG(ErrorText, []), erlang:raise(Class, Reason, StackTrace) end; false -> ?WARNING_MSG("Module ~ts doesn't support reloading " "and will be restarted", [Module]), stop_module(Host, Module), start_module(Host, Module, NewOpts, Order) end. -spec update_module(binary(), module(), opts()) -> ok | {ok, pid()}. update_module(Host, Module, Opts) -> case ets:lookup(ejabberd_modules, {Module, Host}) of [#ejabberd_module{opts = OldOpts, order = Order}] -> NewOpts = maps:merge(OldOpts, Opts), reload_module(Host, Module, NewOpts, OldOpts, Order); [] -> erlang:error({module_not_loaded, Module, Host}) end. -spec store_options(binary(), module(), opts(), integer()) -> true. store_options(Host, Module, Opts, Order) -> ets:insert(ejabberd_modules, #ejabberd_module{module_host = {Module, Host}, opts = Opts, order = Order}). maybe_halt_ejabberd() -> case is_app_running(ejabberd) of false -> ?CRITICAL_MSG("ejabberd initialization was aborted " "because a module start failed.", []), ejabberd:halt(); true -> ok end. is_app_running(AppName) -> Timeout = 15000, lists:keymember(AppName, 1, application:which_applications(Timeout)). -spec stop_modules() -> ok. stop_modules() -> lists:foreach( fun(Host) -> stop_modules(Host) end, ejabberd_option:hosts()). -spec stop_modules(binary()) -> ok. stop_modules(Host) -> Modules = lists:reverse(loaded_modules_with_opts(Host)), lists:foreach( fun({Module, _Args}) -> stop_module_keep_config(Host, Module) end, Modules). -spec stop_module(binary(), atom()) -> error | {aborted, any()} | {atomic, any()}. stop_module(Host, Module) -> case stop_module_keep_config(Host, Module) of error -> error; ok -> ok end. -spec stop_module_keep_config(binary(), atom()) -> error | ok. stop_module_keep_config(Host, Module) -> ?DEBUG("Stopping ~ts at ~ts", [Module, Host]), try Module:stop(Host) of _ -> ets:delete(ejabberd_modules, {Module, Host}), ok catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to stop module ~ts at ~ts:~n** ~ts", [Module, Host, misc:format_exception(2, Class, Reason, StackTrace)]), error end. -spec get_opt(atom(), opts()) -> any(). get_opt(Opt, Opts) -> maps:get(Opt, Opts). -spec set_opt(atom(), term(), opts()) -> opts(). set_opt(Opt, Val, Opts) -> maps:put(Opt, Val, Opts). -spec get_module_opt(global | binary(), atom(), atom()) -> any(). get_module_opt(global, Module, Opt) -> get_module_opt(ejabberd_config:get_myname(), Module, Opt); get_module_opt(Host, Module, Opt) -> Opts = get_module_opts(Host, Module), get_opt(Opt, Opts). -spec get_module_opt_hosts(binary(), module()) -> [binary()]. get_module_opt_hosts(Host, Module) -> Opts = get_module_opts(Host, Module), get_opt_hosts(Opts). -spec get_opt_hosts(opts()) -> [binary()]. get_opt_hosts(Opts) -> case get_opt(hosts, Opts) of L when L == [] orelse L == undefined -> [get_opt(host, Opts)]; L -> L end. -spec get_module_opts(binary(), module()) -> opts(). get_module_opts(Host, Module) -> try ets:lookup_element(ejabberd_modules, {Module, Host}, 3) catch _:badarg -> erlang:error({module_not_loaded, Module, Host}) end. -spec db_mod(binary() | global | db_type() | opts(), module()) -> module(). db_mod(T, M) -> db_mod(db_type, T, M). -spec ram_db_mod(binary() | global | db_type() | opts(), module()) -> module(). ram_db_mod(T, M) -> db_mod(ram_db_type, T, M). -spec db_mod(db_type | ram_db_type, binary() | global | db_type() | opts(), module()) -> module(). db_mod(Opt, Host, Module) when is_binary(Host) orelse Host == global -> db_mod(Opt, get_module_opt(Host, Module, Opt), Module); db_mod(Opt, Opts, Module) when is_map(Opts) -> db_mod(Opt, get_opt(Opt, Opts), Module); db_mod(_Opt, Type, Module) when is_atom(Type) -> list_to_existing_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(Type)). -spec loaded_modules(binary()) -> [atom()]. loaded_modules(Host) -> Mods = ets:select( ejabberd_modules, ets:fun2ms( fun(#ejabberd_module{module_host = {Mod, H}, order = Order}) when H == Host -> {Mod, Order} end)), [Mod || {Mod, _} <- lists:keysort(2, Mods)]. -spec loaded_modules_with_opts(binary()) -> [{atom(), opts()}]. loaded_modules_with_opts(Host) -> Mods = ets:select( ejabberd_modules, ets:fun2ms( fun(#ejabberd_module{module_host = {Mod, H}, opts = Opts, order = Order}) when H == Host -> {Mod, Opts, Order} end)), [{Mod, Opts} || {Mod, Opts, _} <- lists:keysort(3, Mods)]. -spec get_hosts(opts(), binary()) -> [binary()]. get_hosts(Opts, Prefix) -> case get_opt(hosts, Opts) of undefined -> case get_opt(host, Opts) of undefined -> [<> || Host <- ejabberd_option:hosts()]; Host -> [Host] end; Hosts -> Hosts end. -spec get_module_proc(binary() | global, atom()) -> atom(). get_module_proc(global, Base) -> get_module_proc(<<"global">>, Base); get_module_proc(Host, Base) -> binary_to_atom( <<(erlang:atom_to_binary(Base, latin1))/binary, "_", Host/binary>>, latin1). -spec is_loaded(binary(), atom()) -> boolean(). is_loaded(Host, Module) -> ets:member(ejabberd_modules, {Module, Host}). -spec is_loaded_elsewhere(binary(), atom()) -> boolean(). is_loaded_elsewhere(Host, Module) -> ets:select_count( ejabberd_modules, ets:fun2ms( fun(#ejabberd_module{module_host = {Mod, H}}) -> (Mod == Module) and (H /= Host) end)) /= 0. -spec config_reloaded() -> ok. config_reloaded() -> lists:foreach(fun reload_modules/1, ejabberd_option:hosts()). -spec is_equal_opt(atom(), opts(), opts()) -> true | {false, any(), any()}. is_equal_opt(Opt, NewOpts, OldOpts) -> NewVal = get_opt(Opt, NewOpts), OldVal = get_opt(Opt, OldOpts), if NewVal /= OldVal -> {false, NewVal, OldVal}; true -> true end. %%%=================================================================== %%% Formatters %%%=================================================================== -spec format_module_error(atom(), start | reload, non_neg_integer(), opts(), error | exit | throw, any(), [erlang:stack_item()]) -> iolist(). format_module_error(Module, Fun, Arity, Opts, Class, Reason, St) -> case {Class, Reason} of {error, {bad_return, Module, {error, _} = Err}} -> io_lib:format("Failed to ~ts module ~ts: ~ts", [Fun, Module, misc:format_val(Err)]); {error, {bad_return, Module, Ret}} -> io_lib:format("Module ~ts returned unexpected value from ~ts/~B:~n" "** Error: ~p~n" "** Hint: this is either not an ejabberd module " "or it implements ejabberd API incorrectly", [Module, Fun, Arity, Ret]); _ -> io_lib:format("Internal error of module ~ts has " "occurred during ~ts:~n" "** Options: ~p~n" "** ~ts", [Module, Fun, Opts, misc:format_exception(2, Class, Reason, St)]) end. %%%=================================================================== %%% Validation %%%=================================================================== -spec validator(binary()) -> econf:validator(). validator(Host) -> econf:options( #{modules => econf:and_then( econf:map( econf:beam([{start, 2}, {stop, 1}, {mod_options, 1}, {depends, 2}]), econf:options( #{db_type => econf:atom(), ram_db_type => econf:atom(), '_' => econf:any()})), fun(L) -> Validators = maps:from_list( lists:map( fun({Mod, Opts}) -> {Mod, validator(Host, Mod, Opts)} end, L)), Validator = econf:options(Validators, [unique]), Validator(L) end)}). -spec validator(binary(), module(), [{atom(), term()}]) -> econf:validator(). validator(Host, Module, Opts) -> {Required, {DefaultOpts1, Validators}} = lists:mapfoldl( fun({M, DefOpts}, {DAcc, VAcc}) -> lists:mapfoldl( fun({Opt, Def}, {DAcc1, VAcc1}) -> {[], {DAcc1#{Opt => Def}, VAcc1#{Opt => get_opt_type(Module, M, Opt)}}}; (Opt, {DAcc1, VAcc1}) -> {[Opt], {DAcc1, VAcc1#{Opt => get_opt_type(Module, M, Opt)}}} end, {DAcc, VAcc}, DefOpts) end, {#{}, #{}}, get_defaults(Host, Module, Opts)), econf:and_then( econf:options( Validators, [{required, lists:usort(lists:flatten(Required))}, {return, map}, unique]), fun(Opts1) -> maps:merge(DefaultOpts1, Opts1) end). -spec validate(binary(), [{module(), opts()}]) -> {ok, [{module(), opts(), integer()}]} | econf:error_return(). validate(Host, ModOpts) -> case econf:validate(validator(Host), [{modules, ModOpts}]) of {ok, [{modules, ModOpts1}]} -> try sort_modules(Host, ModOpts1) catch throw:{?MODULE, Reason} -> {error, Reason, [modules]} end; {error, _, _} = Err -> Err end. -spec get_defaults(binary(), module(), [{atom(), term()}]) -> [{module(), [{atom(), term()} | atom()]}]. get_defaults(Host, Module, Opts) -> DefaultOpts = Module:mod_options(Host), [{Module, DefaultOpts}| lists:filtermap( fun({Opt, T1}) when Opt == db_type; Opt == ram_db_type -> T2 = proplists:get_value(Opt, Opts, T1), DBMod = list_to_atom(atom_to_list(Module) ++ "_" ++ atom_to_list(T2)), case code:ensure_loaded(DBMod) of {module, _} -> case erlang:function_exported(DBMod, mod_options, 1) of true -> {true, {DBMod, DBMod:mod_options(Host)}}; false -> false end; _ -> false end; (_) -> false end, DefaultOpts)]. -spec get_opt_type(module(), module(), atom()) -> econf:validator(). get_opt_type(Mod, SubMod, Opt) -> try SubMod:mod_opt_type(Opt) catch _:_ -> Mod:mod_opt_type(Opt) end. -spec sort_modules(binary(), [{module(), opts()}]) -> {ok, [{module(), opts(), integer()}]}. sort_modules(Host, ModOpts) -> G = digraph:new([acyclic]), lists:foreach( fun({Mod, Opts}) -> digraph:add_vertex(G, Mod, Opts), Deps = Mod:depends(Host, Opts), lists:foreach( fun({DepMod, Type}) -> case lists:keyfind(DepMod, 1, ModOpts) of false when Type == hard -> throw({?MODULE, {missing_module_dep, Mod, DepMod}}); false when Type == soft -> warn_soft_dep_fail(DepMod, Mod); {DepMod, DepOpts} -> digraph:add_vertex(G, DepMod, DepOpts), case digraph:add_edge(G, DepMod, Mod) of {error, {bad_edge, Path}} -> warn_cyclic_dep(Path); _ -> ok end end end, Deps) end, ModOpts), {Result, _} = lists:mapfoldl( fun(V, Order) -> {M, O} = digraph:vertex(G, V), {{M, O, Order}, Order+1} end, 1, digraph_utils:topsort(G)), digraph:delete(G), {ok, Result}. -spec warn_soft_dep_fail(module(), module()) -> ok. warn_soft_dep_fail(DepMod, Mod) -> ?WARNING_MSG("Module ~ts is recommended for module " "~ts but is not found in the config", [DepMod, Mod]). -spec warn_cyclic_dep([module()]) -> ok. warn_cyclic_dep(Path) -> ?WARNING_MSG("Cyclic dependency detected between modules ~ts. " "This is either a bug, or the modules are not " "supposed to work together in this configuration. " "The modules will still be loaded though", [misc:format_cycle(Path)]). ejabberd-21.12/src/mod_stats.erl0000644000232200023220000002065314154362354017130 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_stats.erl %%% Author : Alexey Shchepin %%% Purpose : Basic statistics. %%% Created : 11 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_stats). -author('alexey@process-one.net'). -protocol({xep, 39, '0.6.0'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_iq/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_STATS, ?MODULE, process_iq). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_STATS). reload(Host, NewOpts, _OldOpts) -> start(Host, NewOpts). depends(_Host, _Opts) -> []. process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_iq(#iq{type = get, to = To, lang = Lang, sub_els = [#stats{} = Stats]} = IQ) -> Node = str:tokens(Stats#stats.node, <<"/">>), Names = [Name || #stat{name = Name} <- Stats#stats.list], case get_local_stats(To#jid.server, Node, Names, Lang) of {result, List} -> xmpp:make_iq_result(IQ, Stats#stats{list = List}); {error, Error} -> xmpp:make_error(IQ, Error) end. -define(STAT(Name), #stat{name = Name}). get_local_stats(_Server, [], [], _Lang) -> {result, [?STAT(<<"users/online">>), ?STAT(<<"users/total">>), ?STAT(<<"users/all-hosts/online">>), ?STAT(<<"users/all-hosts/total">>)]}; get_local_stats(Server, [], Names, _Lang) -> {result, lists:map(fun (Name) -> get_local_stat(Server, [], Name) end, Names)}; get_local_stats(_Server, [<<"running nodes">>, _], [], _Lang) -> {result, [?STAT(<<"time/uptime">>), ?STAT(<<"time/cputime">>), ?STAT(<<"users/online">>), ?STAT(<<"transactions/committed">>), ?STAT(<<"transactions/aborted">>), ?STAT(<<"transactions/restarted">>), ?STAT(<<"transactions/logged">>)]}; get_local_stats(_Server, [<<"running nodes">>, ENode], Names, Lang) -> case search_running_node(ENode) of false -> Txt = ?T("No running node found"), {error, xmpp:err_item_not_found(Txt, Lang)}; Node -> {result, lists:map(fun (Name) -> get_node_stat(Node, Name) end, Names)} end; get_local_stats(_Server, _, _, Lang) -> Txt = ?T("No statistics found for this item"), {error, xmpp:err_feature_not_implemented(Txt, Lang)}. -define(STATVAL(Val, Unit), #stat{name = Name, units = Unit, value = Val}). -define(STATERR(Code, Desc), #stat{name = Name, error = #stat_error{code = Code, reason = Desc}}). get_local_stat(Server, [], Name) when Name == <<"users/online">> -> case catch ejabberd_sm:get_vh_session_list(Server) of {'EXIT', _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Users -> ?STATVAL((integer_to_binary(length(Users))), <<"users">>) end; get_local_stat(Server, [], Name) when Name == <<"users/total">> -> case catch ejabberd_auth:count_users(Server) of {'EXIT', _Reason} -> ?STATERR(500, <<"Internal Server Error">>); NUsers -> ?STATVAL((integer_to_binary(NUsers)), <<"users">>) end; get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/online">> -> Users = ejabberd_sm:connected_users_number(), ?STATVAL((integer_to_binary(Users)), <<"users">>); get_local_stat(_Server, [], Name) when Name == <<"users/all-hosts/total">> -> NumUsers = lists:foldl(fun (Host, Total) -> ejabberd_auth:count_users(Host) + Total end, 0, ejabberd_option:hosts()), ?STATVAL((integer_to_binary(NumUsers)), <<"users">>); get_local_stat(_Server, _, Name) -> ?STATERR(404, <<"Not Found">>). get_node_stat(Node, Name) when Name == <<"time/uptime">> -> case catch ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); CPUTime -> ?STATVAL(str:format("~.3f", [element(1, CPUTime) / 1000]), <<"seconds">>) end; get_node_stat(Node, Name) when Name == <<"time/cputime">> -> case catch ejabberd_cluster:call(Node, erlang, statistics, [runtime]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); RunTime -> ?STATVAL(str:format("~.3f", [element(1, RunTime) / 1000]), <<"seconds">>) end; get_node_stat(Node, Name) when Name == <<"users/online">> -> case catch ejabberd_cluster:call(Node, ejabberd_sm, dirty_get_my_sessions_list, []) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Users -> ?STATVAL((integer_to_binary(length(Users))), <<"users">>) end; get_node_stat(Node, Name) when Name == <<"transactions/committed">> -> case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Transactions -> ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/aborted">> -> case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Transactions -> ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/restarted">> -> case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Transactions -> ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(Node, Name) when Name == <<"transactions/logged">> -> case catch ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]) of {badrpc, _Reason} -> ?STATERR(500, <<"Internal Server Error">>); Transactions -> ?STATVAL((integer_to_binary(Transactions)), <<"transactions">>) end; get_node_stat(_, Name) -> ?STATERR(404, <<"Not Found">>). search_running_node(SNode) -> search_running_node(SNode, mnesia:system_info(running_db_nodes)). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case iolist_to_binary(atom_to_list(Node)) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. mod_options(_Host) -> []. mod_doc() -> #{desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0039.html" "[XEP-0039: Statistics Gathering]. This protocol " "allows you to retrieve the following statistics " "from your ejabberd server:"), "", ?T("- Total number of registered users on the current " "virtual host (users/total)."), "", ?T("- Total number of registered users on all virtual " "hosts (users/all-hosts/total)."), "", ?T("- Total number of online users on the current " "virtual host (users/online)."), "", ?T("- Total number of online users on all virtual " "hosts (users/all-hosts/online)."), "", ?T("NOTE: The protocol extension is deferred and seems " "like even a few clients that were supporting it " "are now abandoned. So using this module makes " "very little sense.")]}. ejabberd-21.12/src/gen_iq_handler.erl0000644000232200023220000001242714154362354020072 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : gen_iq_handler.erl %%% Author : Alexey Shchepin %%% Purpose : IQ handler support %%% Created : 22 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(gen_iq_handler). -author('alexey@process-one.net'). %% API -export([add_iq_handler/5, remove_iq_handler/3, handle/1, handle/2, start/1, get_features/2]). %% Deprecated functions -export([add_iq_handler/6, handle/5, iqdisc/1]). -deprecated([{add_iq_handler, 6}, {handle, 5}, {iqdisc, 1}]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -type component() :: ejabberd_sm | ejabberd_local. %%==================================================================== %% API %%==================================================================== -spec start(component()) -> ok. start(Component) -> catch ets:new(Component, [named_table, public, ordered_set, {read_concurrency, true}, {heir, erlang:group_leader(), none}]), ok. -spec add_iq_handler(component(), binary(), binary(), module(), atom()) -> ok. add_iq_handler(Component, Host, NS, Module, Function) -> ets:insert(Component, {{Host, NS}, Module, Function}), ok. -spec remove_iq_handler(component(), binary(), binary()) -> ok. remove_iq_handler(Component, Host, NS) -> ets:delete(Component, {Host, NS}), ok. -spec handle(iq()) -> ok. handle(#iq{to = To} = IQ) -> Component = case To#jid.luser of <<"">> -> ejabberd_local; _ -> ejabberd_sm end, handle(Component, IQ). -spec handle(component(), iq()) -> ok. handle(Component, #iq{to = To, type = T, lang = Lang, sub_els = [El]} = Packet) when T == get; T == set -> XMLNS = xmpp:get_ns(El), Host = To#jid.lserver, case ets:lookup(Component, {Host, XMLNS}) of [{_, Module, Function}] -> process_iq(Host, Module, Function, Packet); [] -> Txt = ?T("No module is handling this query"), Err = xmpp:err_service_unavailable(Txt, Lang), ejabberd_router:route_error(Packet, Err) end; handle(_, #iq{type = T, lang = Lang, sub_els = SubEls} = Packet) when T == get; T == set -> Txt = case SubEls of [] -> ?T("No child elements found"); _ -> ?T("Too many child elements") end, Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Packet, Err); handle(_, #iq{type = T}) when T == result; T == error -> ok. -spec get_features(component(), binary()) -> [binary()]. get_features(Component, Host) -> get_features(Component, ets:next(Component, {Host, <<"">>}), Host, []). get_features(Component, {Host, XMLNS}, Host, XMLNSs) -> get_features(Component, ets:next(Component, {Host, XMLNS}), Host, [XMLNS|XMLNSs]); get_features(_, _, _, XMLNSs) -> XMLNSs. -spec process_iq(binary(), atom(), atom(), iq()) -> ok. process_iq(_Host, Module, Function, IQ) -> try process_iq(Module, Function, IQ) of #iq{} = ResIQ -> ejabberd_router:route(ResIQ); ignore -> ok catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to process iq:~n~ts~n** ~ts", [xmpp:pp(IQ), misc:format_exception(2, Class, Reason, StackTrace)]), Txt = ?T("Module failed to handle the query"), Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), ejabberd_router:route_error(IQ, Err) end. -spec process_iq(module(), atom(), iq()) -> ignore | iq(). process_iq(Module, Function, #iq{lang = Lang, sub_els = [El]} = IQ) -> try Pkt = case erlang:function_exported(Module, decode_iq_subel, 1) of true -> Module:decode_iq_subel(El); false -> xmpp:decode(El) end, Module:Function(IQ#iq{sub_els = [Pkt]}) catch error:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end. -spec iqdisc(binary() | global) -> no_queue. iqdisc(_Host) -> no_queue. %%==================================================================== %% Deprecated API %%==================================================================== -spec add_iq_handler(module(), binary(), binary(), module(), atom(), any()) -> ok. add_iq_handler(Component, Host, NS, Module, Function, _Type) -> add_iq_handler(Component, Host, NS, Module, Function). -spec handle(binary(), atom(), atom(), any(), iq()) -> any(). handle(Host, Module, Function, _Opts, IQ) -> process_iq(Host, Module, Function, IQ). ejabberd-21.12/src/ejabberd_redis_sup.erl0000644000232200023220000000675014154362354020750 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 6 Apr 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_redis_sup). -behaviour(supervisor). %% API -export([start/0, stop/0, start_link/0]). -export([get_pool_size/0, config_reloaded/0]). %% Supervisor callbacks -export([init/1]). -include("logger.hrl"). %%%=================================================================== %%% API functions %%%=================================================================== start() -> case is_started() of true -> ok; false -> ejabberd:start_app(eredis), Spec = {?MODULE, {?MODULE, start_link, []}, permanent, infinity, supervisor, [?MODULE]}, case supervisor:start_child(ejabberd_db_sup, Spec) of {ok, _} -> ok; {error, {already_started, Pid}} -> %% Wait for the supervisor to fully start _ = supervisor:count_children(Pid), ok; {error, Why} = Err -> ?ERROR_MSG("Failed to start ~ts: ~p", [?MODULE, Why]), Err end end. stop() -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 20), _ = supervisor:terminate_child(ejabberd_db_sup, ?MODULE), _ = supervisor:delete_child(ejabberd_db_sup, ?MODULE), ok. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). config_reloaded() -> case is_started() of true -> lists:foreach( fun(Spec) -> supervisor:start_child(?MODULE, Spec) end, get_specs()), PoolSize = get_pool_size(), lists:foreach( fun({Id, _, _, _}) when Id > PoolSize -> case supervisor:terminate_child(?MODULE, Id) of ok -> supervisor:delete_child(?MODULE, Id); _ -> ok end; (_) -> ok end, supervisor:which_children(?MODULE)); false -> ok end. %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([]) -> ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 20), {ok, {{one_for_one, 500, 1}, get_specs()}}. %%%=================================================================== %%% Internal functions %%%=================================================================== get_specs() -> lists:map( fun(I) -> {I, {ejabberd_redis, start_link, [I]}, transient, 2000, worker, [?MODULE]} end, lists:seq(1, get_pool_size())). get_pool_size() -> ejabberd_option:redis_pool_size() + 1. is_started() -> whereis(?MODULE) /= undefined. ejabberd-21.12/src/ejabberd_oauth_sql.erl0000644000232200023220000001035514154362354020746 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_oauth_sql.erl %%% Author : Alexey Shchepin %%% Purpose : OAUTH2 SQL backend %%% Created : 27 Jul 2016 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%------------------------------------------------------------------- -module(ejabberd_oauth_sql). -behaviour(ejabberd_oauth). -export([init/0, store/1, lookup/1, clean/1, lookup_client/1, store_client/1, remove_client/1]). -include("ejabberd_oauth.hrl"). -include("ejabberd_sql_pt.hrl"). -include_lib("xmpp/include/jid.hrl"). -include("logger.hrl"). init() -> ok. store(R) -> Token = R#oauth_token.token, {User, Server} = R#oauth_token.us, SJID = jid:encode({User, Server, <<"">>}), Scope = str:join(R#oauth_token.scope, <<" ">>), Expire = R#oauth_token.expire, case ?SQL_UPSERT( ejabberd_config:get_myname(), "oauth_token", ["!token=%(Token)s", "jid=%(SJID)s", "scope=%(Scope)s", "expire=%(Expire)d"]) of ok -> ok; _ -> {error, db_failure} end. lookup(Token) -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("select @(jid)s, @(scope)s, @(expire)d" " from oauth_token where token=%(Token)s")) of {selected, [{SJID, Scope, Expire}]} -> JID = jid:decode(SJID), US = {JID#jid.luser, JID#jid.lserver}, {ok, #oauth_token{token = Token, us = US, scope = str:tokens(Scope, <<" ">>), expire = Expire}}; _ -> error end. clean(TS) -> ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from oauth_token where expire < %(TS)d")). lookup_client(ClientID) -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("select @(client_name)s, @(grant_type)s, @(options)s" " from oauth_client where client_id=%(ClientID)s")) of {selected, [{ClientName, SGrantType, SOptions}]} -> GrantType = case SGrantType of <<"password">> -> password; <<"implicit">> -> implicit end, case misc:base64_to_term(SOptions) of {term, Options} -> {ok, #oauth_client{client_id = ClientID, client_name = ClientName, grant_type = GrantType, options = Options}}; _ -> error end; _ -> error end. store_client(#oauth_client{client_id = ClientID, client_name = ClientName, grant_type = GrantType, options = Options}) -> SGrantType = case GrantType of password -> <<"password">>; implicit -> <<"implicit">> end, SOptions = misc:term_to_base64(Options), case ?SQL_UPSERT( ejabberd_config:get_myname(), "oauth_client", ["!client_id=%(ClientID)s", "client_name=%(ClientName)s", "grant_type=%(SGrantType)s", "options=%(SOptions)s"]) of ok -> ok; _ -> {error, db_failure} end. remove_client(Client) -> ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from oauth_client where client=%(Client)s")). ejabberd-21.12/src/mod_private.erl0000644000232200023220000003450714154362354017447 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_private.erl %%% Author : Alexey Shchepin %%% Purpose : Support for private storage. %%% Created : 16 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_private). -author('alexey@process-one.net'). -protocol({xep, 49, '1.2'}). -protocol({xep, 411, '0.2.0'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_sm_iq/1, import_info/0, remove_user/2, get_data/2, get_data/3, export/1, mod_doc/0, import/5, import_start/2, mod_opt_type/1, set_data/2, mod_options/1, depends/2, get_sm_features/5, pubsub_publish_item/6]). -export([get_commands_spec/0, bookmarks_to_pep/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_private.hrl"). -include("ejabberd_commands.hrl"). -include("translate.hrl"). -define(PRIVATE_CACHE, private_cache). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback set_data(binary(), binary(), [{binary(), xmlel()}]) -> ok | {error, any()}. -callback get_data(binary(), binary(), binary()) -> {ok, xmlel()} | error | {error, any()}. -callback get_all_data(binary(), binary()) -> {ok, [xmlel()]} | error | {error, any()}. -callback del_data(binary(), binary()) -> ok | {error, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:add(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE, ?MODULE, process_sm_iq), ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(Host) -> ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), ejabberd_hooks:delete(pubsub_publish_item, Host, ?MODULE, pubsub_publish_item, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PRIVATE), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). depends(_Host, _Opts) -> [{mod_pubsub, soft}]. mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module adds support for " "https://xmpp.org/extensions/xep-0049.html" "[XEP-0049: Private XML Storage]."), "", ?T("Using this method, XMPP entities can store " "private data on the server, retrieve it " "whenever necessary and share it between multiple " "connected clients of the same user. The data stored " "might be anything, as long as it is a valid XML. " "One typical usage is storing a bookmark of all user's conferences " "(https://xmpp.org/extensions/xep-0048.html" "[XEP-0048: Bookmarks]).")], opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. -spec get_sm_features({error, stanza_error()} | empty | {result, [binary()]}, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | empty | {result, [binary()]}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_features(Acc, _From, To, <<"">>, _Lang) -> case gen_mod:is_loaded(To#jid.lserver, mod_pubsub) of true -> {result, [?NS_BOOKMARKS_CONVERSION_0 | case Acc of {result, Features} -> Features; empty -> [] end]}; false -> Acc end; get_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec process_sm_iq(iq()) -> iq(). process_sm_iq(#iq{type = Type, lang = Lang, from = #jid{luser = LUser, lserver = LServer} = From, to = #jid{luser = LUser, lserver = LServer}, sub_els = [#private{sub_els = Els0}]} = IQ) -> case filter_xmlels(Els0) of [] -> Txt = ?T("No private data found in this query"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); Data when Type == set -> case set_data(From, Data) of ok -> xmpp:make_iq_result(IQ); {error, #stanza_error{} = Err} -> xmpp:make_error(IQ, Err); {error, _} -> Txt = ?T("Database failure"), Err = xmpp:err_internal_server_error(Txt, Lang), xmpp:make_error(IQ, Err) end; Data when Type == get -> case get_data(LUser, LServer, Data) of {error, _} -> Txt = ?T("Database failure"), Err = xmpp:err_internal_server_error(Txt, Lang), xmpp:make_error(IQ, Err); Els -> xmpp:make_iq_result(IQ, #private{sub_els = Els}) end end; process_sm_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). -spec filter_xmlels([xmlel()]) -> [{binary(), xmlel()}]. filter_xmlels(Els) -> lists:flatmap( fun(#xmlel{} = El) -> case fxml:get_tag_attr_s(<<"xmlns">>, El) of <<"">> -> []; NS -> [{NS, El}] end end, Els). -spec set_data(jid(), [{binary(), xmlel()}]) -> ok | {error, _}. set_data(JID, Data) -> set_data(JID, Data, true). -spec set_data(jid(), [{binary(), xmlel()}], boolean()) -> ok | {error, _}. set_data(JID, Data, Publish) -> {LUser, LServer, _} = jid:tolower(JID), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_data(LUser, LServer, Data) of ok -> delete_cache(Mod, LUser, LServer, Data), case Publish of true -> publish_data(JID, Data); false -> ok end; {error, _} = Err -> Err end. -spec get_data(binary(), binary(), [{binary(), xmlel()}]) -> [xmlel()] | {error, _}. get_data(LUser, LServer, Data) -> Mod = gen_mod:db_mod(LServer, ?MODULE), lists:foldr( fun(_, {error, _} = Err) -> Err; ({NS, El}, Els) -> Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PRIVATE_CACHE, {LUser, LServer, NS}, fun() -> Mod:get_data(LUser, LServer, NS) end); false -> Mod:get_data(LUser, LServer, NS) end, case Res of {ok, StorageEl} -> [StorageEl|Els]; error -> [El|Els]; {error, _} = Err -> Err end end, [], Data). -spec get_data(binary(), binary()) -> [xmlel()] | {error, _}. get_data(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:get_all_data(LUser, LServer) of {ok, Els} -> Els; error -> []; {error, _} = Err -> Err end. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(Server, ?MODULE), Data = case use_cache(Mod, LServer) of true -> case Mod:get_all_data(LUser, LServer) of {ok, Els} -> filter_xmlels(Els); _ -> [] end; false -> [] end, Mod:del_data(LUser, LServer), delete_cache(Mod, LUser, LServer, Data). %%%=================================================================== %%% Pubsub %%%=================================================================== -spec publish_data(jid(), [{binary(), xmlel()}]) -> ok | {error, stanza_error()}. publish_data(JID, Data) -> {_, LServer, _} = LBJID = jid:remove_resource(jid:tolower(JID)), case gen_mod:is_loaded(LServer, mod_pubsub) of true -> case lists:keyfind(?NS_STORAGE_BOOKMARKS, 1, Data) of false -> ok; {_, El} -> PubOpts = [{persist_items, true}, {access_model, whitelist}], case mod_pubsub:publish_item( LBJID, LServer, ?NS_STORAGE_BOOKMARKS, JID, <<"current">>, [El], PubOpts, all) of {result, _} -> ok; {error, _} = Err -> Err end end; false -> ok end. -spec pubsub_publish_item(binary(), binary(), jid(), jid(), binary(), [xmlel()]) -> any(). pubsub_publish_item(LServer, ?NS_STORAGE_BOOKMARKS, #jid{luser = LUser, lserver = LServer} = From, #jid{luser = LUser, lserver = LServer}, _ItemId, [Payload|_]) -> set_data(From, [{?NS_STORAGE_BOOKMARKS, Payload}], false); pubsub_publish_item(_, _, _, _, _, _) -> ok. %%%=================================================================== %%% Commands %%%=================================================================== -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> [#ejabberd_commands{name = bookmarks_to_pep, tags = [private], desc = "Export private XML storage bookmarks to PEP", module = ?MODULE, function = bookmarks_to_pep, args = [{user, binary}, {host, binary}], args_rename = [{server, host}], args_desc = ["Username", "Server"], args_example = [<<"bob">>, <<"example.com">>], result = {res, restuple}, result_desc = "Result tuple", result_example = {ok, <<"Bookmarks exported">>}}]. -spec bookmarks_to_pep(binary(), binary()) -> {ok, binary()} | {error, binary()}. bookmarks_to_pep(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PRIVATE_CACHE, {LUser, LServer, ?NS_STORAGE_BOOKMARKS}, fun() -> Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) end); false -> Mod:get_data(LUser, LServer, ?NS_STORAGE_BOOKMARKS) end, case Res of {ok, El} -> Data = [{?NS_STORAGE_BOOKMARKS, El}], case publish_data(jid:make(User, Server), Data) of ok -> {ok, <<"Bookmarks exported to PEP node">>}; {error, Err} -> {error, xmpp:format_stanza_error(Err)} end; _ -> {error, <<"Cannot retrieve bookmarks from private XML storage">>} end. %%%=================================================================== %%% Cache %%%=================================================================== -spec delete_cache(module(), binary(), binary(), [{binary(), xmlel()}]) -> ok. delete_cache(Mod, LUser, LServer, Data) -> case use_cache(Mod, LServer) of true -> Nodes = cache_nodes(Mod, LServer), lists:foreach( fun({NS, _}) -> ets_cache:delete(?PRIVATE_CACHE, {LUser, LServer, NS}, Nodes) end, Data); false -> ok end. -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?PRIVATE_CACHE, CacheOpts); false -> ets_cache:delete(?PRIVATE_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_private_opt:cache_size(Opts), CacheMissed = mod_private_opt:cache_missed(Opts), LifeTime = mod_private_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_private_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. %%%=================================================================== %%% Import/Export %%%=================================================================== import_info() -> [{<<"private_storage">>, 4}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). ejabberd-21.12/src/mod_proxy65_service.erl0000644000232200023220000002573314154362354021052 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_proxy65_service.erl %%% Author : Evgeniy Khramtsov %%% Purpose : SOCKS5 Bytestreams XMPP service. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_service). -author('xram@jabber.ru'). -behaviour(gen_server). %% gen_server callbacks. -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -export([start_link/1, reload/3, add_listener/2, process_disco_info/1, process_disco_items/1, process_vcard/1, process_bytestreams/1, delete_listener/1, route/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -define(PROCNAME, ejabberd_mod_proxy65_service). -record(state, {myhosts = [] :: [binary()]}). %%%------------------------ %%% gen_server callbacks %%%------------------------ start_link(Host) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:start_link({local, Proc}, ?MODULE, [Host], []). reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). init([Host]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, mod_proxy65), MyHosts = gen_mod:get_opt_hosts(Opts), lists:foreach( fun(MyHost) -> gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items), gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_VCARD, ?MODULE, process_vcard), gen_iq_handler:add_iq_handler(ejabberd_local, MyHost, ?NS_BYTESTREAMS, ?MODULE, process_bytestreams), ejabberd_router:register_route( MyHost, Host, {apply, ?MODULE, route}) end, MyHosts), {ok, #state{myhosts = MyHosts}}. terminate(_Reason, #state{myhosts = MyHosts}) -> lists:foreach( fun(MyHost) -> ejabberd_router:unregister_route(MyHost), unregister_handlers(MyHost) end, MyHosts). handle_info({route, Packet}, State) -> try route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> NewHosts = gen_mod:get_opt_hosts(NewOpts), OldHosts = gen_mod:get_opt_hosts(OldOpts), lists:foreach( fun(NewHost) -> ejabberd_router:register_route(NewHost, ServerHost), register_handlers(NewHost) end, NewHosts -- OldHosts), lists:foreach( fun(OldHost) -> ejabberd_router:unregister_route(OldHost), unregister_handlers(OldHost) end, OldHosts -- NewHosts), {noreply, State#state{myhosts = NewHosts}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); route(_) -> ok. %%%------------------------ %%% Listener management %%%------------------------ add_listener(Host, Opts) -> {_, IP, _} = EndPoint = get_endpoint(Host), Opts1 = gen_mod:set_opt(server_host, Host, Opts), Opts2 = gen_mod:set_opt(ip, IP, Opts1), ejabberd_listener:add_listener(EndPoint, mod_proxy65_stream, Opts2). delete_listener(Host) -> ejabberd_listener:delete_listener(get_endpoint(Host), mod_proxy65_stream). %%%------------------------ %%% IQ Processing %%%------------------------ -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_info(#iq{type = get, to = To, lang = Lang} = IQ) -> Host = ejabberd_router:host_of_route(To#jid.lserver), Name = mod_proxy65_opt:name(Host), Info = ejabberd_hooks:run_fold(disco_info, Host, [], [Host, ?MODULE, <<"">>, <<"">>]), xmpp:make_iq_result( IQ, #disco_info{xdata = Info, identities = [#identity{category = <<"proxy">>, type = <<"bytestreams">>, name = translate:translate(Lang, Name)}], features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_VCARD, ?NS_BYTESTREAMS]}). -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_items(#iq{type = get} = IQ) -> xmpp:make_iq_result(IQ, #disco_items{}). -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_vcard(#iq{type = get, to = To, lang = Lang} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_proxy65_opt:vcard(ServerHost) of undefined -> #vcard_temp{fn = <<"ejabberd/mod_proxy65">>, url = ejabberd_config:get_uri(), desc = misc:get_descr( Lang, ?T("ejabberd SOCKS5 Bytestreams module"))}; V -> V end, xmpp:make_iq_result(IQ, VCard). -spec process_bytestreams(iq()) -> iq(). process_bytestreams(#iq{type = get, from = JID, to = To, lang = Lang} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, JID) of allow -> StreamHost = get_streamhost(Host, ServerHost), xmpp:make_iq_result(IQ, #bytestreams{hosts = [StreamHost]}); deny -> xmpp:make_error(IQ, xmpp:err_forbidden(?T("Access denied by service policy"), Lang)) end; process_bytestreams(#iq{type = set, lang = Lang, sub_els = [#bytestreams{sid = SID}]} = IQ) when SID == <<"">> orelse size(SID) > 128 -> Why = {bad_attr_value, <<"sid">>, <<"query">>, ?NS_BYTESTREAMS}, Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); process_bytestreams(#iq{type = set, lang = Lang, sub_els = [#bytestreams{activate = undefined}]} = IQ) -> Why = {missing_cdata, <<"">>, <<"activate">>, ?NS_BYTESTREAMS}, Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)); process_bytestreams(#iq{type = set, lang = Lang, from = InitiatorJID, to = To, sub_els = [#bytestreams{activate = TargetJID, sid = SID}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), ACL = mod_proxy65_opt:access(ServerHost), case acl:match_rule(ServerHost, ACL, InitiatorJID) of allow -> Node = ejabberd_cluster:get_node_by_id(To#jid.lresource), Target = jid:encode(jid:tolower(TargetJID)), Initiator = jid:encode(jid:tolower(InitiatorJID)), SHA1 = str:sha(<>), Mod = gen_mod:ram_db_mod(global, mod_proxy65), MaxConnections = max_connections(ServerHost), case Mod:activate_stream(SHA1, Initiator, MaxConnections, Node) of {ok, InitiatorPid, TargetPid} -> mod_proxy65_stream:activate( {InitiatorPid, InitiatorJID}, {TargetPid, TargetJID}), xmpp:make_iq_result(IQ); {error, notfound} -> Txt = ?T("Failed to activate bytestream"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, {limit, InitiatorPid, TargetPid}} -> mod_proxy65_stream:stop(InitiatorPid), mod_proxy65_stream:stop(TargetPid), Txt = ?T("Too many active bytestreams"), xmpp:make_error(IQ, xmpp:err_resource_constraint(Txt, Lang)); {error, conflict} -> Txt = ?T("Bytestream already activated"), xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); {error, Err} -> ?ERROR_MSG("Failed to activate bytestream from ~ts to ~ts: ~p", [Initiator, Target, Err]), Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; deny -> Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end. %%%------------------------- %%% Auxiliary functions. %%%------------------------- -spec get_streamhost(binary(), binary()) -> streamhost(). get_streamhost(Host, ServerHost) -> {Port, IP, _} = get_endpoint(ServerHost), HostName = case mod_proxy65_opt:hostname(ServerHost) of undefined -> misc:ip_to_list(IP); Val -> Val end, Resource = ejabberd_cluster:node_id(), #streamhost{jid = jid:make(<<"">>, Host, Resource), host = HostName, port = Port}. -spec get_endpoint(binary()) -> {inet:port_number(), inet:ip_address(), tcp}. get_endpoint(Host) -> Port = mod_proxy65_opt:port(Host), IP = case mod_proxy65_opt:ip(Host) of undefined -> misc:get_my_ipv4_address(); Addr -> Addr end, {Port, IP, tcp}. max_connections(ServerHost) -> mod_proxy65_opt:max_connections(ServerHost). register_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_vcard), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS, ?MODULE, process_bytestreams). unregister_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_BYTESTREAMS). ejabberd-21.12/src/eldap.erl0000644000232200023220000012067114154362354016221 0ustar debalancedebalance-module(eldap). %%% -------------------------------------------------------------------- %%% Created: 12 Oct 2000 by Tobbe %%% Function: Erlang client LDAP implementation according RFC 2251. %%% The interface is based on RFC 1823, and %%% draft-ietf-asid-ldap-c-api-00.txt %%% %%% Copyright (C) 2000 Torbjorn Tornkvist, tnt@home.se %%% %%% %%% 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. %%% Modified by Sean Hinde 7th Dec 2000 %%% Turned into gen_fsm, made non-blocking, added timers etc to support this. %%% Now has the concept of a name (string() or atom()) per instance which allows %%% multiple users to call by name if so desired. %%% %%% Can be configured with start_link parameters or use a config file to get %%% host to connect to, dn, password, log function etc. %%% Modified by Alexey Shchepin %%% Modified by Evgeniy Khramtsov %%% Implemented queue for bind() requests to prevent pending binds. %%% Implemented extensibleMatch/2 function. %%% Implemented LDAP Extended Operations (currently only Password Modify %%% is supported - RFC 3062). %%% Modified by Christophe Romain %%% Improve error case handling %%% Modified by Mickael Remond %%% Now use ejabberd log mechanism %%% Modified by: %%% Thomas Baden 2008 April 6th %%% Andy Harb 2008 April 28th %%% Anton Podavalov 2009 February 22th %%% Added LDAPS support, modeled off jungerl eldap.erl version. %%% NOTICE: STARTTLS is not supported. %%% -------------------------------------------------------------------- -vc('$Id$ '). %%%---------------------------------------------------------------------- %%% LDAP Client state machine. %%% Possible states are: %%% connecting - actually disconnected, but retrying periodically %%% wait_bind_response - connected and sent bind request %%% active - bound to LDAP Server and ready to handle commands %%% active_bind - sent bind() request and waiting for response %%%---------------------------------------------------------------------- -behaviour(p1_fsm). -include("logger.hrl"). %% External exports -export([start_link/1, start_link/6]). -export([baseObject/0, singleLevel/0, wholeSubtree/0, close/1, equalityMatch/2, greaterOrEqual/2, lessOrEqual/2, approxMatch/2, search/2, substrings/2, present/1, extensibleMatch/2, 'and'/1, 'or'/1, 'not'/1, modify/3, mod_add/2, mod_delete/2, mod_replace/2, add/3, delete/2, modify_dn/5, modify_passwd/3, bind/3]). -export([get_status/1]). -export([init/1, connecting/2, connecting/3, wait_bind_response/3, active/3, active_bind/3, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -export_type([filter/0]). -include("ELDAPv3.hrl"). -include_lib("kernel/include/inet.hrl"). -include("eldap.hrl"). -define(LDAP_VERSION, 3). -define(RETRY_TIMEOUT, 500). -define(BIND_TIMEOUT, 10000). -define(CMD_TIMEOUT, 100000). %% Used in gen_fsm sync calls. %% Used as a timeout for gen_tcp:send/2 -define(CALL_TIMEOUT, (?CMD_TIMEOUT) + (?BIND_TIMEOUT) + (?RETRY_TIMEOUT)). -define(SEND_TIMEOUT, 30000). -define(MAX_TRANSACTION_ID, 65535). -define(MIN_TRANSACTION_ID, 0). %% Grace period after "soft" LDAP bind errors: -define(GRACEFUL_RETRY_TIMEOUT, 5000). -define(SUPPORTEDEXTENSION, <<"1.3.6.1.4.1.1466.101.120.7">>). -define(SUPPORTEDEXTENSIONSYNTAX, <<"1.3.6.1.4.1.1466.115.121.1.38">>). -define(STARTTLS, <<"1.3.6.1.4.1.1466.20037">>). -type handle() :: pid() | atom() | binary(). -record(eldap, {version = ?LDAP_VERSION :: non_neg_integer(), hosts = [] :: [binary()], host = undefined :: binary() | undefined, port = 389 :: inet:port_number(), sockmod = gen_tcp :: ssl | gen_tcp, tls = none :: none | tls, tls_options = [] :: [{certfile, string()} | {cacertfile, string()} | {depth, non_neg_integer()} | {verify, non_neg_integer()} | {fail_if_no_peer_cert, boolean()}], fd :: gen_tcp:socket() | undefined, rootdn = <<"">> :: binary(), passwd = <<"">> :: binary(), id = 0 :: non_neg_integer(), bind_timer = make_ref() :: reference(), dict = dict:new() :: dict:dict(), req_q = queue:new() :: queue:queue()}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link(Name) -> Reg_name = misc:binary_to_atom(<<"eldap_", Name/binary>>), p1_fsm:start_link({local, Reg_name}, ?MODULE, [], []). -spec start_link(binary(), [binary()], inet:port_number(), binary(), binary(), tlsopts()) -> any(). start_link(Name, Hosts, Port, Rootdn, Passwd, Opts) -> Reg_name = misc:binary_to_atom(<<"eldap_", Name/binary>>), p1_fsm:start_link({local, Reg_name}, ?MODULE, [Hosts, Port, Rootdn, Passwd, Opts], []). -spec get_status(handle()) -> any(). %%% -------------------------------------------------------------------- %%% Get status of connection. %%% -------------------------------------------------------------------- get_status(Handle) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_all_state_event(Handle1, get_status). %%% -------------------------------------------------------------------- %%% Shutdown connection (and process) asynchronous. %%% -------------------------------------------------------------------- -spec close(handle()) -> any(). close(Handle) -> Handle1 = get_handle(Handle), p1_fsm:send_all_state_event(Handle1, close). %%% -------------------------------------------------------------------- %%% Add an entry. The entry field MUST NOT exist for the AddRequest %%% to succeed. The parent of the entry MUST exist. %%% Example: %%% %%% add(Handle, %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", %%% [{"objectclass", ["person"]}, %%% {"cn", ["Bill Valentine"]}, %%% {"sn", ["Valentine"]}, %%% {"telephoneNumber", ["545 555 00"]}] %%% ) %%% -------------------------------------------------------------------- add(Handle, Entry, Attributes) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {add, Entry, add_attrs(Attributes)}, ?CALL_TIMEOUT). %%% Do sanity check ! add_attrs(Attrs) -> F = fun ({Type, Vals}) -> {'AddRequest_attributes', Type, Vals} end, case catch lists:map(F, Attrs) of {'EXIT', _} -> throw({error, attribute_values}); Else -> Else end. %%% -------------------------------------------------------------------- %%% Delete an entry. The entry consists of the DN of %%% the entry to be deleted. %%% Example: %%% %%% delete(Handle, %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com" %%% ) %%% -------------------------------------------------------------------- delete(Handle, Entry) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {delete, Entry}, ?CALL_TIMEOUT). %%% -------------------------------------------------------------------- %%% Modify an entry. Given an entry a number of modification %%% operations can be performed as one atomic operation. %%% Example: %%% %%% modify(Handle, %%% "cn=Torbjorn Tornkvist, ou=people, o=Bluetail AB, dc=bluetail, dc=com", %%% [replace("telephoneNumber", ["555 555 00"]), %%% add("description", ["LDAP hacker"])] %%% ) %%% -------------------------------------------------------------------- -spec modify(handle(), any(), [add | delete | replace]) -> any(). modify(Handle, Object, Mods) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {modify, Object, Mods}, ?CALL_TIMEOUT). %%% %%% Modification operations. %%% Example: %%% replace("telephoneNumber", ["555 555 00"]) %%% mod_add(Type, Values) -> m(add, Type, Values). mod_delete(Type, Values) -> m(delete, Type, Values). %%% -------------------------------------------------------------------- %%% Modify an entry. Given an entry a number of modification %%% operations can be performed as one atomic operation. %%% Example: %%% %%% modify_dn(Handle, %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", %%% "cn=Ben Emerson", %%% true, %%% "" %%% ) %%% -------------------------------------------------------------------- mod_replace(Type, Values) -> m(replace, Type, Values). m(Operation, Type, Values) -> #'ModifyRequest_modification_SEQOF'{operation = Operation, modification = #'AttributeTypeAndValues'{type = Type, vals = Values}}. modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {modify_dn, Entry, NewRDN, bool_p(DelOldRDN), optional(NewSup)}, ?CALL_TIMEOUT). -spec modify_passwd(handle(), binary(), binary()) -> any(). modify_passwd(Handle, DN, Passwd) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {modify_passwd, DN, Passwd}, ?CALL_TIMEOUT). %%% -------------------------------------------------------------------- %%% Bind. %%% Example: %%% %%% bind(Handle, %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", %%% "secret") %%% -------------------------------------------------------------------- -spec bind(handle(), binary(), binary()) -> any(). bind(Handle, RootDN, Passwd) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {bind, RootDN, Passwd}, ?CALL_TIMEOUT). %%% Sanity checks ! bool_p(Bool) when Bool == true; Bool == false -> Bool. optional([]) -> asn1_NOVALUE; optional(Value) -> Value. %%% -------------------------------------------------------------------- %%% Synchronous search of the Directory returning a %%% requested set of attributes. %%% %%% Example: %%% %%% Filter = eldap:substrings("sn", [{any,"o"}]), %%% eldap:search(S, [{base, "dc=bluetail, dc=com"}, %%% {filter, Filter}, %%% {attributes,["cn"]}])), %%% %%% Returned result: {ok, #eldap_search_result{}} %%% %%% Example: %%% %%% {ok,{eldap_search_result, %%% [{eldap_entry, %%% "cn=Magnus Froberg, dc=bluetail, dc=com", %%% [{"cn",["Magnus Froberg"]}]}, %%% {eldap_entry, %%% "cn=Torbjorn Tornkvist, dc=bluetail, dc=com", %%% [{"cn",["Torbjorn Tornkvist"]}]}], %%% []}} %%% %%% -------------------------------------------------------------------- -type search_args() :: [{base, binary()} | {filter, filter()} | {scope, scope()} | {attributes, [binary()]} | {types_only, boolean()} | {timeout, non_neg_integer()} | {limit, non_neg_integer()} | {deref_aliases, never | searching | finding | always}]. -spec search(handle(), eldap_search() | search_args()) -> any(). search(Handle, A) when is_record(A, eldap_search) -> call_search(Handle, A); search(Handle, L) when is_list(L) -> case catch parse_search_args(L) of {error, Emsg} -> {error, Emsg}; {'EXIT', Emsg} -> {error, Emsg}; A when is_record(A, eldap_search) -> call_search(Handle, A) end. call_search(Handle, A) -> Handle1 = get_handle(Handle), p1_fsm:sync_send_event(Handle1, {search, A}, ?CALL_TIMEOUT). -spec parse_search_args(search_args()) -> eldap_search(). parse_search_args(Args) -> parse_search_args(Args, #eldap_search{scope = wholeSubtree}). parse_search_args([{base, Base} | T], A) -> parse_search_args(T, A#eldap_search{base = Base}); parse_search_args([{filter, Filter} | T], A) -> parse_search_args(T, A#eldap_search{filter = Filter}); parse_search_args([{scope, Scope} | T], A) -> parse_search_args(T, A#eldap_search{scope = Scope}); parse_search_args([{attributes, Attrs} | T], A) -> parse_search_args(T, A#eldap_search{attributes = Attrs}); parse_search_args([{types_only, TypesOnly} | T], A) -> parse_search_args(T, A#eldap_search{types_only = TypesOnly}); parse_search_args([{timeout, Timeout} | T], A) when is_integer(Timeout) -> parse_search_args(T, A#eldap_search{timeout = Timeout}); parse_search_args([{limit, Limit} | T], A) when is_integer(Limit) -> parse_search_args(T, A#eldap_search{limit = Limit}); parse_search_args([{deref_aliases, never} | T], A) -> parse_search_args(T, A#eldap_search{deref_aliases = neverDerefAliases}); parse_search_args([{deref_aliases, searching} | T], A) -> parse_search_args(T, A#eldap_search{deref_aliases = derefInSearching}); parse_search_args([{deref_aliases, finding} | T], A) -> parse_search_args(T, A#eldap_search{deref_aliases = derefFindingBaseObj}); parse_search_args([{deref_aliases, always} | T], A) -> parse_search_args(T, A#eldap_search{deref_aliases = derefAlways}); parse_search_args([H | _], _) -> throw({error, {unknown_arg, H}}); parse_search_args([], A) -> A. baseObject() -> baseObject. singleLevel() -> singleLevel. %%% %%% The Scope parameter %%% wholeSubtree() -> wholeSubtree. %%% %%% Boolean filter operations %%% -type filter() :: 'and'() | 'or'() | 'not'() | equalityMatch() | greaterOrEqual() | lessOrEqual() | approxMatch() | present() | substrings() | extensibleMatch(). %%% %%% The following Filter parameters consist of an attribute %%% and an attribute value. Example: F("uid","tobbe") %%% -type 'and'() :: {'and', [filter()]}. -spec 'and'([filter()]) -> 'and'(). 'and'(ListOfFilters) when is_list(ListOfFilters) -> {'and', ListOfFilters}. -type 'or'() :: {'or', [filter()]}. -spec 'or'([filter()]) -> 'or'(). 'or'(ListOfFilters) when is_list(ListOfFilters) -> {'or', ListOfFilters}. -type 'not'() :: {'not', filter()}. -spec 'not'(filter()) -> 'not'(). 'not'(Filter) when is_tuple(Filter) -> {'not', Filter}. -type equalityMatch() :: {equalityMatch, 'AttributeValueAssertion'()}. -spec equalityMatch(binary(), binary()) -> equalityMatch(). equalityMatch(Desc, Value) -> {equalityMatch, av_assert(Desc, Value)}. -type greaterOrEqual() :: {greaterOrEqual, 'AttributeValueAssertion'()}. -spec greaterOrEqual(binary(), binary()) -> greaterOrEqual(). greaterOrEqual(Desc, Value) -> {greaterOrEqual, av_assert(Desc, Value)}. -type lessOrEqual() :: {lessOrEqual, 'AttributeValueAssertion'()}. -spec lessOrEqual(binary(), binary()) -> lessOrEqual(). lessOrEqual(Desc, Value) -> {lessOrEqual, av_assert(Desc, Value)}. -type approxMatch() :: {approxMatch, 'AttributeValueAssertion'()}. -spec approxMatch(binary(), binary()) -> approxMatch(). approxMatch(Desc, Value) -> {approxMatch, av_assert(Desc, Value)}. -type 'AttributeValueAssertion'() :: #'AttributeValueAssertion'{attributeDesc :: binary(), assertionValue :: binary()}. -spec av_assert(binary(), binary()) -> 'AttributeValueAssertion'(). av_assert(Desc, Value) -> #'AttributeValueAssertion'{attributeDesc = Desc, assertionValue = Value}. %%% %%% Filter to check for the presence of an attribute %%% -type present() :: {present, binary()}. -spec present(binary()) -> present(). %%% %%% A substring filter seem to be based on a pattern: %%% %%% InitValue*AnyValue*FinalValue %%% %%% where all three parts seem to be optional (at least when %%% talking with an OpenLDAP server). Thus, the arguments %%% to substrings/2 looks like this: %%% %%% Type ::= string( ) %%% SubStr ::= listof( {initial,Value} | {any,Value}, {final,Value}) %%% %%% Example: substrings("sn",[{initial,"To"},{any,"kv"},{final,"st"}]) %%% will match entries containing: 'sn: Tornkvist' %%% present(Attribute) -> {present, Attribute}. %%% %%% extensibleMatch filter. %%% FIXME: Describe the purpose of this filter. %%% %%% Value ::= string( ) %%% Opts ::= listof( {matchingRule, Str} | {type, Str} | {dnAttributes, true} ) %%% %%% Example: extensibleMatch("Fred", [{matchingRule, "1.2.3.4.5"}, {type, "cn"}]). %%% -type substr() :: [{initial | any | final, binary()}]. -type 'SubstringFilter'() :: #'SubstringFilter'{type :: binary(), substrings :: substr()}. -type substrings() :: {substrings, 'SubstringFilter'()}. -spec substrings(binary(), substr()) -> substrings(). substrings(Type, SubStr) -> {substrings, #'SubstringFilter'{type = Type, substrings = SubStr}}. -type match_opts() :: [{matchingRule | type, binary()} | {dnAttributes, boolean()}]. -type 'MatchingRuleAssertion'() :: #'MatchingRuleAssertion'{matchValue :: binary(), type :: asn1_NOVALUE | binary(), matchingRule :: asn1_NOVALUE | binary(), dnAttributes :: asn1_DEFAULT | true}. -type extensibleMatch() :: {extensibleMatch, 'MatchingRuleAssertion'()}. -spec extensibleMatch(binary(), match_opts()) -> extensibleMatch(). extensibleMatch(Value, Opts) -> MRA = #'MatchingRuleAssertion'{matchValue = Value}, {extensibleMatch, extensibleMatch_opts(Opts, MRA)}. extensibleMatch_opts([{matchingRule, Rule} | Opts], MRA) -> extensibleMatch_opts(Opts, MRA#'MatchingRuleAssertion'{matchingRule = Rule}); extensibleMatch_opts([{type, Desc} | Opts], MRA) -> extensibleMatch_opts(Opts, MRA#'MatchingRuleAssertion'{type = Desc}); extensibleMatch_opts([{dnAttributes, true} | Opts], MRA) -> extensibleMatch_opts(Opts, MRA#'MatchingRuleAssertion'{dnAttributes = true}); extensibleMatch_opts([_ | Opts], MRA) -> extensibleMatch_opts(Opts, MRA); extensibleMatch_opts([], MRA) -> MRA. get_handle(Pid) when is_pid(Pid) -> Pid; get_handle(Atom) when is_atom(Atom) -> Atom; get_handle(Name) when is_binary(Name) -> misc:binary_to_atom(<<"eldap_", Name/binary>>). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | %% {stop, StopReason} %% I use the trick of setting a timeout of 0 to pass control into the %% process. %%---------------------------------------------------------------------- init([Hosts, Port, Rootdn, Passwd, Opts]) -> Encrypt = case proplists:get_value(encrypt, Opts) of tls -> tls; _ -> none end, PortTemp = case Port of undefined -> case Encrypt of tls -> ?LDAPS_PORT; _ -> ?LDAP_PORT end; PT -> PT end, CertOpts = case proplists:get_value(tls_certfile, Opts) of undefined -> []; Path1 -> [{certfile, Path1}] end, CacertOpts = case proplists:get_value(tls_cacertfile, Opts) of undefined -> []; Path2 -> [{cacertfile, Path2}] end, DepthOpts = case proplists:get_value(tls_depth, Opts) of undefined -> []; Depth -> [{depth, Depth}] end, Verify = proplists:get_value(tls_verify, Opts, false), TLSOpts = if (Verify == hard orelse Verify == soft) andalso CacertOpts == [] -> ?WARNING_MSG("TLS verification is enabled but no CA " "certfiles configured, so verification " "is disabled.", []), CertOpts; Verify == soft -> [{verify, verify_peer}, {fail_if_no_peer_cert, false}] ++ CertOpts ++ CacertOpts ++ DepthOpts; Verify == hard -> [{verify, verify_peer}, {fail_if_no_peer_cert, true}] ++ CertOpts ++ CacertOpts ++ DepthOpts; true -> [] end, {ok, connecting, #eldap{hosts = Hosts, port = PortTemp, rootdn = Rootdn, passwd = Passwd, tls = Encrypt, tls_options = TLSOpts, id = 0, dict = dict:new(), req_q = queue:new()}, 0}. connecting(timeout, S) -> {ok, NextState, NewS} = connect_bind(S), {next_state, NextState, NewS}. connecting(Event, From, S) -> Q = queue:in({Event, From}, S#eldap.req_q), {next_state, connecting, S#eldap{req_q = Q}}. wait_bind_response(Event, From, S) -> Q = queue:in({Event, From}, S#eldap.req_q), {next_state, wait_bind_response, S#eldap{req_q = Q}}. active_bind(Event, From, S) -> Q = queue:in({Event, From}, S#eldap.req_q), {next_state, active_bind, S#eldap{req_q = Q}}. active(Event, From, S) -> process_command(S, Event, From). %%---------------------------------------------------------------------- %% Func: handle_event/3 %% Called when p1_fsm:send_all_state_event/2 is invoked. %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(close, _StateName, S) -> catch (S#eldap.sockmod):close(S#eldap.fd), {stop, normal, S}; handle_event(_Event, StateName, S) -> {next_state, StateName, S}. handle_sync_event(_Event, _From, StateName, S) -> {reply, {StateName, S}, StateName, S}. %% %% Packets arriving in various states %% handle_info({Tag, _Socket, Data}, connecting, S) when Tag == tcp; Tag == ssl -> ?DEBUG("TCP packet received when disconnected!~n~p", [Data]), {next_state, connecting, S}; handle_info({Tag, _Socket, Data}, wait_bind_response, S) when Tag == tcp; Tag == ssl -> misc:cancel_timer(S#eldap.bind_timer), case catch recvd_wait_bind_response(Data, S) of bound -> dequeue_commands(S); {fail_bind, Reason} -> report_bind_failure(S#eldap.host, S#eldap.port, Reason), {next_state, connecting, close_and_retry(S, ?GRACEFUL_RETRY_TIMEOUT)}; {'EXIT', Reason} -> report_bind_failure(S#eldap.host, S#eldap.port, Reason), {next_state, connecting, close_and_retry(S)}; {error, Reason} -> report_bind_failure(S#eldap.host, S#eldap.port, Reason), {next_state, connecting, close_and_retry(S)} end; handle_info({Tag, _Socket, Data}, StateName, S) when (StateName == active orelse StateName == active_bind) andalso (Tag == tcp orelse Tag == ssl) -> case catch recvd_packet(Data, S) of {response, Response, RequestType} -> NewS = case Response of {reply, Reply, To, S1} -> p1_fsm:reply(To, Reply), S1; {ok, S1} -> S1 end, if StateName == active_bind andalso RequestType == bindRequest orelse StateName == active -> dequeue_commands(NewS); true -> {next_state, StateName, NewS} end; _ -> {next_state, StateName, S} end; handle_info({Tag, _Socket}, Fsm_state, S) when Tag == tcp_closed; Tag == ssl_closed -> ?WARNING_MSG("LDAP server closed the connection: ~ts:~p~nIn " "State: ~p", [S#eldap.host, S#eldap.port, Fsm_state]), {next_state, connecting, close_and_retry(S)}; handle_info({Tag, _Socket, Reason}, Fsm_state, S) when Tag == tcp_error; Tag == ssl_error -> ?DEBUG("eldap received tcp_error: ~p~nIn State: ~p", [Reason, Fsm_state]), {next_state, connecting, close_and_retry(S)}; %% %% Timers %% handle_info({timeout, Timer, {cmd_timeout, Id}}, StateName, S) -> case cmd_timeout(Timer, Id, S) of {reply, To, Reason, NewS} -> p1_fsm:reply(To, Reason), {next_state, StateName, NewS}; {error, _Reason} -> {next_state, StateName, S} end; handle_info({timeout, retry_connect}, connecting, S) -> {ok, NextState, NewS} = connect_bind(S), {next_state, NextState, NewS}; handle_info({timeout, _Timer, bind_timeout}, wait_bind_response, S) -> {next_state, connecting, close_and_retry(S)}; %% %% Make sure we don't fill the message queue with rubbish %% handle_info(Info, StateName, S) -> ?DEBUG("Unexpected Info: ~p~nIn state: " "~p~n when StateData is: ~p", [Info, StateName, S]), {next_state, StateName, S}. %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- terminate(_Reason, _StateName, _StatData) -> ok. %%---------------------------------------------------------------------- %% Func: code_change/4 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%---------------------------------------------------------------------- code_change(_OldVsn, StateName, S, _Extra) -> {ok, StateName, S}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- dequeue_commands(S) -> case queue:out(S#eldap.req_q) of {{value, {Event, From}}, Q} -> case process_command(S#eldap{req_q = Q}, Event, From) of {_, active, NewS} -> dequeue_commands(NewS); Res -> Res end; {empty, _} -> {next_state, active, S} end. process_command(S, Event, From) -> case send_command(Event, From, S) of {ok, NewS} -> case Event of {bind, _, _} -> {next_state, active_bind, NewS}; _ -> {next_state, active, NewS} end; {error, _Reason} -> Q = queue:in_r({Event, From}, S#eldap.req_q), NewS = close_and_retry(S#eldap{req_q = Q}), {next_state, connecting, NewS} end. send_command(Command, From, S) -> Id = bump_id(S), {Name, Request} = gen_req(Command), Message = #'LDAPMessage'{messageID = Id, protocolOp = {Name, Request}}, ?DEBUG("~p~n", [{Name, ejabberd_config:may_hide_data(Request)}]), {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), case (S#eldap.sockmod):send(S#eldap.fd, Bytes) of ok -> Timer = erlang:start_timer(?CMD_TIMEOUT, self(), {cmd_timeout, Id}), New_dict = dict:store(Id, [{Timer, Command, From, Name}], S#eldap.dict), {ok, S#eldap{id = Id, dict = New_dict}}; Error -> Error end. gen_req({search, A}) -> {searchRequest, #'SearchRequest'{baseObject = A#eldap_search.base, scope = A#eldap_search.scope, derefAliases = A#eldap_search.deref_aliases, sizeLimit = A#eldap_search.limit, timeLimit = A#eldap_search.timeout, typesOnly = A#eldap_search.types_only, filter = A#eldap_search.filter, attributes = A#eldap_search.attributes}}; gen_req({add, Entry, Attrs}) -> {addRequest, #'AddRequest'{entry = Entry, attributes = Attrs}}; gen_req({delete, Entry}) -> {delRequest, Entry}; gen_req({modify, Obj, Mod}) -> {modifyRequest, #'ModifyRequest'{object = Obj, modification = Mod}}; gen_req({modify_dn, Entry, NewRDN, DelOldRDN, NewSup}) -> {modDNRequest, #'ModifyDNRequest'{entry = Entry, newrdn = NewRDN, deleteoldrdn = DelOldRDN, newSuperior = NewSup}}; gen_req({modify_passwd, DN, Passwd}) -> {ok, ReqVal} = 'ELDAPv3':encode('PasswdModifyRequestValue', #'PasswdModifyRequestValue'{userIdentity = DN, newPasswd = Passwd}), {extendedReq, #'ExtendedRequest'{requestName = ?passwdModifyOID, requestValue = iolist_to_binary(ReqVal)}}; gen_req({bind, RootDN, Passwd}) -> {bindRequest, #'BindRequest'{version = ?LDAP_VERSION, name = RootDN, authentication = {simple, Passwd}}}. %%----------------------------------------------------------------------- %% recvd_packet %% Deals with incoming packets in the active state %% Will return one of: %% {ok, NewS} - Don't reply to client yet as this is part of a search %% result and we haven't got all the answers yet. %% {reply, Result, From, NewS} - Reply with result to client From %% {error, Reason} %% {'EXIT', Reason} - Broke %%----------------------------------------------------------------------- recvd_packet(Pkt, S) -> case 'ELDAPv3':decode('LDAPMessage', Pkt) of {ok, Msg} -> Op = Msg#'LDAPMessage'.protocolOp, ?DEBUG("~p", [Op]), Dict = S#eldap.dict, Id = Msg#'LDAPMessage'.messageID, {Timer, From, Name, Result_so_far} = get_op_rec(Id, Dict), Answer = case {Name, Op} of {searchRequest, {searchResEntry, R}} when is_record(R, 'SearchResultEntry') -> New_dict = dict:append(Id, R, Dict), {ok, S#eldap{dict = New_dict}}; {searchRequest, {searchResDone, Result}} -> Reason = Result#'LDAPResult'.resultCode, if Reason == success; Reason == sizeLimitExceeded -> {Res, Ref} = polish(Result_so_far), New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), {reply, #eldap_search_result{entries = Res, referrals = Ref}, From, S#eldap{dict = New_dict}}; true -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), {reply, {error, Reason}, From, S#eldap{dict = New_dict}} end; {searchRequest, {searchResRef, R}} -> New_dict = dict:append(Id, R, Dict), {ok, S#eldap{dict = New_dict}}; {addRequest, {addResponse, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {delRequest, {delResponse, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {modifyRequest, {modifyResponse, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {modDNRequest, {modDNResponse, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {bindRequest, {bindResponse, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_bind_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {extendedReq, {extendedResp, Result}} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), Reply = check_extended_reply(Result, From), {reply, Reply, From, S#eldap{dict = New_dict}}; {OtherName, OtherResult} -> New_dict = dict:erase(Id, Dict), misc:cancel_timer(Timer), {reply, {error, {invalid_result, OtherName, OtherResult}}, From, S#eldap{dict = New_dict}} end, {response, Answer, Name}; Error -> Error end. check_reply(#'LDAPResult'{resultCode = success}, _From) -> ok; check_reply(#'LDAPResult'{resultCode = Reason}, _From) -> {error, Reason}; check_reply(Other, _From) -> {error, Other}. check_bind_reply(#'BindResponse'{resultCode = success}, _From) -> ok; check_bind_reply(#'BindResponse'{resultCode = Reason}, _From) -> {error, Reason}; check_bind_reply(Other, _From) -> {error, Other}. %% TODO: process reply depending on requestName: %% this requires BER-decoding of #'ExtendedResponse'.response check_extended_reply(#'ExtendedResponse'{resultCode = success}, _From) -> ok; check_extended_reply(#'ExtendedResponse'{resultCode = Reason}, _From) -> {error, Reason}; check_extended_reply(Other, _From) -> {error, Other}. get_op_rec(Id, Dict) -> case dict:find(Id, Dict) of {ok, [{Timer, _Command, From, Name} | Res]} -> {Timer, From, Name, Res}; error -> throw({error, unkown_id}) end. %%----------------------------------------------------------------------- %% recvd_wait_bind_response packet %% Deals with incoming packets in the wait_bind_response state %% Will return one of: %% bound - Success - move to active state %% {fail_bind, Reason} - Failed %% {error, Reason} %% {'EXIT', Reason} - Broken packet %%----------------------------------------------------------------------- recvd_wait_bind_response(Pkt, S) -> case 'ELDAPv3':decode('LDAPMessage', Pkt) of {ok, Msg} -> ?DEBUG("~p", [Msg]), check_id(S#eldap.id, Msg#'LDAPMessage'.messageID), case Msg#'LDAPMessage'.protocolOp of {bindResponse, Result} -> case Result#'BindResponse'.resultCode of success -> bound; Error -> {fail_bind, Error} end end; Else -> {fail_bind, Else} end. check_id(Id, Id) -> ok; check_id(_, _) -> throw({error, wrong_bind_id}). %%----------------------------------------------------------------------- %% General Helpers %%----------------------------------------------------------------------- close_and_retry(S, Timeout) -> catch (S#eldap.sockmod):close(S#eldap.fd), Queue = dict:fold(fun (_Id, [{Timer, Command, From, _Name} | _], Q) -> misc:cancel_timer(Timer), queue:in_r({Command, From}, Q); (_, _, Q) -> Q end, S#eldap.req_q, S#eldap.dict), erlang:send_after(Timeout, self(), {timeout, retry_connect}), S#eldap{fd = undefined, req_q = Queue, dict = dict:new()}. close_and_retry(S) -> close_and_retry(S, ?RETRY_TIMEOUT). report_bind_failure(Host, Port, Reason) -> ?WARNING_MSG("LDAP bind failed on ~ts:~p~nReason: ~p", [Host, Port, Reason]). %%----------------------------------------------------------------------- %% Sort out timed out commands %%----------------------------------------------------------------------- cmd_timeout(Timer, Id, S) -> Dict = S#eldap.dict, case dict:find(Id, Dict) of {ok, [{Timer, _Command, From, Name} | Res]} -> case Name of searchRequest -> {Res1, Ref1} = polish(Res), New_dict = dict:erase(Id, Dict), {reply, From, {timeout, #eldap_search_result{entries = Res1, referrals = Ref1}}, S#eldap{dict = New_dict}}; _ -> New_dict = dict:erase(Id, Dict), {reply, From, {error, timeout}, S#eldap{dict = New_dict}} end; error -> {error, timed_out_cmd_not_in_dict} end. %%----------------------------------------------------------------------- %% Common stuff for results %%----------------------------------------------------------------------- %%% %%% Polish the returned search result %%% polish(Entries) -> polish(Entries, [], []). polish([H | T], Res, Ref) when is_record(H, 'SearchResultEntry') -> ObjectName = H#'SearchResultEntry'.objectName, F = fun ({_, A, V}) -> {A, V} end, Attrs = lists:map(F, H#'SearchResultEntry'.attributes), polish(T, [#eldap_entry{object_name = ObjectName, attributes = Attrs} | Res], Ref); polish([H | T], Res, Ref) -> % No special treatment of referrals at the moment. polish(T, Res, [H | Ref]); polish([], Res, Ref) -> {Res, Ref}. -ifdef(NO_CUSTOMIZE_HOSTNAME_CHECK). check_hostname_opt(TLSOpts) -> TLSOpts. -else. check_hostname_opt(TLSOpts) -> MatchFun = public_key:pkix_verify_hostname_match_fun(https), [{customize_hostname_check, [{match_fun, MatchFun}]} | TLSOpts]. -endif. host_tls_options(Host, TLSOpts) -> case proplists:get_value(verify, TLSOpts) of verify_peer -> check_hostname_opt([{server_name_indication, Host} | TLSOpts]); _ -> TLSOpts end. %%----------------------------------------------------------------------- %% Connect to next server in list and attempt to bind to it. %%----------------------------------------------------------------------- connect_bind(S) -> Host = next_host(S#eldap.host, S#eldap.hosts), HostS = binary_to_list(Host), Opts = if S#eldap.tls == tls -> [{packet, asn1}, {active, true}, {keepalive, true}, binary | host_tls_options(HostS, S#eldap.tls_options)]; true -> [{packet, asn1}, {active, true}, {keepalive, true}, {send_timeout, ?SEND_TIMEOUT}, binary] end, ?DEBUG("Connecting to LDAP server at ~ts:~p with options ~p", [Host, S#eldap.port, Opts]), SockMod = case S#eldap.tls of tls -> ssl; _ -> gen_tcp end, case connect(HostS, S#eldap.port, SockMod, Opts) of {ok, Socket} -> case bind_request(Socket, S#eldap{sockmod = SockMod}) of {ok, NewS} -> Timer = erlang:start_timer(?BIND_TIMEOUT, self(), {timeout, bind_timeout}), {ok, wait_bind_response, NewS#eldap{fd = Socket, sockmod = SockMod, host = Host, bind_timer = Timer}}; {error, Reason} -> report_bind_failure(Host, S#eldap.port, Reason), NewS = close_and_retry(S), {ok, connecting, NewS#eldap{host = Host}} end; {error, Reason} -> ?ERROR_MSG("LDAP connection to ~ts:~b failed: ~ts", [Host, S#eldap.port, format_error(SockMod, Reason)]), NewS = close_and_retry(S), {ok, connecting, NewS#eldap{host = Host}} end. bind_request(Socket, S) -> Id = bump_id(S), Req = #'BindRequest'{version = S#eldap.version, name = S#eldap.rootdn, authentication = {simple, S#eldap.passwd}}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {bindRequest, Req}}, ?DEBUG("Bind Request Message:~p~n", [ejabberd_config:may_hide_data(Message)]), {ok, Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), case (S#eldap.sockmod):send(Socket, Bytes) of ok -> {ok, S#eldap{id = Id}}; Error -> Error end. %% Given last tried Server, find next one to try next_host(undefined, [H | _]) -> H; % First time, take first next_host(Host, Hosts) -> % Find next in turn next_host(Host, Hosts, Hosts). next_host(Host, [Host], Hosts) -> hd(Hosts); % Wrap back to first next_host(Host, [Host | Tail], _Hosts) -> hd(Tail); % Take next next_host(_Host, [], Hosts) -> hd(Hosts); % Never connected before? (shouldn't happen) next_host(Host, [_ | T], Hosts) -> next_host(Host, T, Hosts). bump_id(#eldap{id = Id}) when Id > (?MAX_TRANSACTION_ID) -> ?MIN_TRANSACTION_ID; bump_id(#eldap{id = Id}) -> Id + 1. format_error(SockMod, Reason) -> Txt = case SockMod of ssl -> ssl:format_error(Reason); gen_tcp -> inet:format_error(Reason) end, case Txt of "unknown POSIX error" -> lists:flatten(io_lib:format("~p", [Reason])); _ -> Txt end. %%-------------------------------------------------------------------- %% Connecting stuff %%-------------------------------------------------------------------- -define(CONNECT_TIMEOUT, timer:seconds(15)). -define(DNS_TIMEOUT, timer:seconds(5)). connect(Host, Port, Mod, Opts) -> case lookup(Host) of {ok, AddrsFamilies} -> do_connect(AddrsFamilies, Port, Mod, Opts, {error, nxdomain}); {error, _} = Err -> Err end. do_connect([{IP, Family}|AddrsFamilies], Port, Mod, Opts, _Err) -> case Mod:connect(IP, Port, [Family|Opts], ?CONNECT_TIMEOUT) of {ok, Sock} -> {ok, Sock}; {error, _} = Err -> do_connect(AddrsFamilies, Port, Mod, Opts, Err) end; do_connect([], _Port, _Mod, _Opts, Err) -> Err. lookup(Host) -> case inet:parse_address(Host) of {ok, IP} -> {ok, [{IP, get_addr_type(IP)}]}; {error, _} -> do_lookup([{Host, Family} || Family <- [inet6, inet]], [], {error, nxdomain}) end. do_lookup([{Host, Family}|HostFamilies], AddrFamilies, Err) -> case inet:gethostbyname(Host, Family, ?DNS_TIMEOUT) of {ok, HostEntry} -> Addrs = host_entry_to_addrs(HostEntry), AddrFamilies1 = [{Addr, Family} || Addr <- Addrs], do_lookup(HostFamilies, AddrFamilies ++ AddrFamilies1, Err); {error, _} = Err1 -> do_lookup(HostFamilies, AddrFamilies, Err1) end; do_lookup([], [], Err) -> Err; do_lookup([], AddrFamilies, _Err) -> {ok, AddrFamilies}. host_entry_to_addrs(#hostent{h_addr_list = AddrList}) -> lists:filter( fun(Addr) -> try get_addr_type(Addr) of _ -> true catch _:badarg -> false end end, AddrList). get_addr_type({_, _, _, _}) -> inet; get_addr_type({_, _, _, _, _, _, _, _}) -> inet6; get_addr_type(_) -> erlang:error(badarg). ejabberd-21.12/src/ejabberd_regexp.erl0000644000232200023220000000723214154362354020241 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_regexp.erl %%% Author : Badlop %%% Purpose : Frontend to Re OTP module %%% Created : 8 Dec 2011 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_regexp). -export([run/2, split/2, replace/3, greplace/3, sh_to_awk/1]). -spec run(binary(), binary()) -> match | nomatch | {error, any()}. run(String, Regexp) -> re:run(String, Regexp, [{capture, none}, unicode]). -spec split(binary(), binary()) -> [binary()]. split(String, Regexp) -> re:split(String, Regexp, [{return, binary}]). -spec replace(binary(), binary(), binary()) -> binary(). replace(String, Regexp, New) -> re:replace(String, Regexp, New, [{return, binary}]). -spec greplace(binary(), binary(), binary()) -> binary(). greplace(String, Regexp, New) -> re:replace(String, Regexp, New, [global, {return, binary}]). %% This code was copied and adapted from xmerl_regexp.erl -spec sh_to_awk(binary()) -> binary(). sh_to_awk(Sh) -> iolist_to_binary([<<"^(">>, sh_to_awk_1(Sh)]). %Fix the beginning sh_to_awk_1(<<"*", Sh/binary>>) -> %This matches any string [<<".*">>, sh_to_awk_1(Sh)]; sh_to_awk_1(<<"?", Sh/binary>>) -> %This matches any character [$., sh_to_awk_1(Sh)]; sh_to_awk_1(<<"[^]", Sh/binary>>) -> %This takes careful handling [<<"\\^">>, sh_to_awk_1(Sh)]; %% Must move '^' to end. sh_to_awk_1(<<"[^", Sh/binary>>) -> [$[, sh_to_awk_2(Sh, true)]; sh_to_awk_1(<<"[!", Sh/binary>>) -> [<<"[^">>, sh_to_awk_2(Sh, false)]; sh_to_awk_1(<<"[", Sh/binary>>) -> [$[, sh_to_awk_2(Sh, false)]; sh_to_awk_1(<>) -> %% Unspecialise everything else which is not an escape character. case sh_special_char(C) of true -> [$\\,C|sh_to_awk_1(Sh)]; false -> [C|sh_to_awk_1(Sh)] end; sh_to_awk_1(<<>>) -> <<")$">>. %Fix the end sh_to_awk_2(<<"]", Sh/binary>>, UpArrow) -> [$]|sh_to_awk_3(Sh, UpArrow)]; sh_to_awk_2(Sh, UpArrow) -> sh_to_awk_3(Sh, UpArrow). sh_to_awk_3(<<"]", Sh/binary>>, true) -> [<<"^]">>, sh_to_awk_1(Sh)]; sh_to_awk_3(<<"]", Sh/binary>>, false) -> [$]|sh_to_awk_1(Sh)]; sh_to_awk_3(<>, UpArrow) -> [C|sh_to_awk_3(Sh, UpArrow)]; sh_to_awk_3(<<>>, true) -> [$^|sh_to_awk_1(<<>>)]; sh_to_awk_3(<<>>, false) -> sh_to_awk_1(<<>>). %% Test if a character is a special character. -spec sh_special_char(char()) -> boolean(). sh_special_char($|) -> true; sh_special_char($*) -> true; sh_special_char($+) -> true; sh_special_char($?) -> true; sh_special_char($() -> true; sh_special_char($)) -> true; sh_special_char($\\) -> true; sh_special_char($^) -> true; sh_special_char($$) -> true; sh_special_char($.) -> true; sh_special_char($[) -> true; sh_special_char($]) -> true; sh_special_char($") -> true; sh_special_char(_C) -> false. ejabberd-21.12/src/ejabberd_service.erl0000644000232200023220000002416514154362354020413 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 11 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_service). -behaviour(xmpp_stream_in). -behaviour(ejabberd_listener). -protocol({xep, 114, '1.6'}). %% ejabberd_listener callbacks -export([start/3, start_link/3, stop/0, accept/1]). -export([listen_opt_type/1, listen_options/0]). %% xmpp_stream_in callbacks -export([init/1, handle_info/2, terminate/2, code_change/3]). -export([handle_stream_start/2, handle_auth_success/4, handle_auth_failure/4, handle_authenticated_packet/2, get_password_fun/1, tls_options/1]). %% API -export([send/2, close/1, close/2, stop_async/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -type state() :: xmpp_stream_in:state(). -export_type([state/0]). %%%=================================================================== %%% API %%%=================================================================== start(SockMod, Socket, Opts) -> xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). start_link(SockMod, Socket, Opts) -> xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). -spec stop() -> ok. stop() -> Err = xmpp:serr_system_shutdown(), lists:foreach( fun({_Id, Pid, _Type, _Module}) -> send(Pid, Err), stop_async(Pid), supervisor:terminate_child(ejabberd_service_sup, Pid) end, supervisor:which_children(ejabberd_service_sup)), _ = supervisor:terminate_child(ejabberd_sup, ejabberd_service_sup), _ = supervisor:delete_child(ejabberd_sup, ejabberd_service_sup), ok. accept(Ref) -> xmpp_stream_in:accept(Ref). -spec send(pid(), xmpp_element()) -> ok; (state(), xmpp_element()) -> state(). send(Stream, Pkt) -> xmpp_stream_in:send(Stream, Pkt). -spec close(pid()) -> ok; (state()) -> state(). close(Ref) -> xmpp_stream_in:close(Ref). -spec close(pid(), atom()) -> ok. close(Ref, Reason) -> xmpp_stream_in:close(Ref, Reason). -spec stop_async(pid()) -> ok. stop_async(Pid) -> xmpp_stream_in:stop_async(Pid). %%%=================================================================== %%% xmpp_stream_in callbacks %%%=================================================================== tls_options(#{tls_options := TLSOptions}) -> TLSOptions. init([State, Opts]) -> Access = proplists:get_value(access, Opts, all), Shaper = proplists:get_value(shaper, Opts, proplists:get_value(shaper_rule, Opts, none)), GlobalPassword = proplists:get_value(password, Opts, random_password()), HostOpts = proplists:get_value(hosts, Opts, [{global, GlobalPassword}]), HostOpts1 = lists:map( fun({Host, undefined}) -> {Host, GlobalPassword}; ({Host, Password}) -> {Host, Password} end, HostOpts), CheckFrom = proplists:get_value(check_from, Opts, true), TLSOpts1 = lists:filter( fun({certfile, _}) -> true; ({ciphers, _}) -> true; ({dhfile, _}) -> true; ({cafile, _}) -> true; ({protocol_options, _}) -> true; (_) -> false end, Opts), TLSOpts = case proplists:get_bool(tls_compression, Opts) of false -> [compression_none | TLSOpts1]; true -> TLSOpts1 end, GlobalRoutes = proplists:get_value(global_routes, Opts, true), Timeout = ejabberd_option:negotiation_timeout(), State1 = xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)), State2 = xmpp_stream_in:set_timeout(State1, Timeout), State3 = State2#{access => Access, xmlns => ?NS_COMPONENT, lang => ejabberd_option:language(), server => ejabberd_config:get_myname(), host_opts => maps:from_list(HostOpts1), stream_version => undefined, tls_options => TLSOpts, global_routes => GlobalRoutes, check_from => CheckFrom}, ejabberd_hooks:run_fold(component_init, {ok, State3}, [Opts]). handle_stream_start(_StreamStart, #{remote_server := RemoteServer, lang := Lang, host_opts := HostOpts} = State) -> case ejabberd_router:is_my_host(RemoteServer) of true -> Txt = ?T("Unable to register route on existing local domain"), xmpp_stream_in:send(State, xmpp:serr_conflict(Txt, Lang)); false -> NewHostOpts = case maps:is_key(RemoteServer, HostOpts) of true -> HostOpts; false -> case maps:find(global, HostOpts) of {ok, GlobalPass} -> maps:from_list([{RemoteServer, GlobalPass}]); error -> HostOpts end end, CodecOpts = ejabberd_config:codec_options(), State#{host_opts => NewHostOpts, codec_options => CodecOpts} end. get_password_fun(#{remote_server := RemoteServer, socket := Socket, ip := IP, host_opts := HostOpts}) -> fun(_) -> case maps:find(RemoteServer, HostOpts) of {ok, Password} -> {Password, undefined}; error -> ?WARNING_MSG("(~ts) Domain ~ts is unconfigured for " "external component from ~ts", [xmpp_socket:pp(Socket), RemoteServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), {false, undefined} end end. handle_auth_success(_, Mech, _, #{remote_server := RemoteServer, host_opts := HostOpts, socket := Socket, ip := IP, global_routes := GlobalRoutes} = State) -> ?INFO_MSG("(~ts) Accepted external component ~ts authentication " "for ~ts from ~ts", [xmpp_socket:pp(Socket), Mech, RemoteServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), Routes = if GlobalRoutes -> maps:keys(HostOpts); true -> [RemoteServer] end, lists:foreach( fun(H) -> ejabberd_router:register_route(H, ejabberd_config:get_myname()), ejabberd_hooks:run(component_connected, [H]) end, Routes), State#{routes => Routes}. handle_auth_failure(_, Mech, Reason, #{remote_server := RemoteServer, socket := Socket, ip := IP} = State) -> ?WARNING_MSG("(~ts) Failed external component ~ts authentication " "for ~ts from ~ts: ~ts", [xmpp_socket:pp(Socket), Mech, RemoteServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]), State. handle_authenticated_packet(Pkt0, #{ip := {IP, _}, lang := Lang} = State) when ?is_stanza(Pkt0) -> Pkt = xmpp:put_meta(Pkt0, ip, IP), From = xmpp:get_from(Pkt), case check_from(From, State) of true -> {Pkt2, State2} = ejabberd_hooks:run_fold(component_send_packet, {Pkt, State}, []), case Pkt2 of drop -> ok; _ -> ejabberd_router:route(Pkt2) end, State2; false -> Txt = ?T("Improper domain part of 'from' attribute"), Err = xmpp:serr_invalid_from(Txt, Lang), xmpp_stream_in:send(State, Err) end; handle_authenticated_packet(_Pkt, State) -> State. handle_info({route, Packet}, #{access := Access} = State) -> case acl:match_rule(global, Access, xmpp:get_from(Packet)) of allow -> xmpp_stream_in:send(State, Packet); deny -> Lang = xmpp:get_lang(Packet), Err = xmpp:err_not_allowed(?T("Access denied by service policy"), Lang), ejabberd_router:route_error(Packet, Err), State end; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), State. terminate(Reason, #{routes := Routes}) -> lists:foreach( fun(H) -> ejabberd_router:unregister_route(H), ejabberd_hooks:run(component_disconnected, [H, Reason]) end, Routes); terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec check_from(jid(), state()) -> boolean(). check_from(_From, #{check_from := false}) -> %% If the admin does not want to check the from field %% when accept packets from any address. %% In this case, the component can send packet of %% behalf of the server users. true; check_from(From, #{host_opts := HostOpts}) -> %% The default is the standard behaviour in XEP-0114 Server = From#jid.lserver, maps:is_key(Server, HostOpts). random_password() -> str:sha(p1_rand:bytes(20)). listen_opt_type(shaper_rule) -> econf:and_then( econf:shaper(), fun(S) -> ?WARNING_MSG("Listening option 'shaper_rule' of module ~ts " "is renamed to 'shaper'. Please adjust your " "configuration", [?MODULE]), S end); listen_opt_type(check_from) -> econf:bool(); listen_opt_type(password) -> econf:binary(); listen_opt_type(hosts) -> econf:map( econf:domain(), econf:and_then( econf:options( #{password => econf:binary()}), fun(Opts) -> proplists:get_value(password, Opts) end)); listen_opt_type(global_routes) -> econf:bool(). listen_options() -> [{access, all}, {shaper, none}, {shaper_rule, none}, {certfile, undefined}, {ciphers, undefined}, {dhfile, undefined}, {cafile, undefined}, {protocol_options, undefined}, {tls, false}, {tls_compression, false}, {max_stanza_size, infinity}, {max_fsm_queue, 10000}, {password, undefined}, {hosts, []}, {check_from, true}, {global_routes, true}]. ejabberd-21.12/src/mod_conversejs_opt.erl0000644000232200023220000000277114154362354021036 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_conversejs_opt). -export([bosh_service_url/1]). -export([conversejs_css/1]). -export([conversejs_script/1]). -export([default_domain/1]). -export([websocket_url/1]). -spec bosh_service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). bosh_service_url(Opts) when is_map(Opts) -> gen_mod:get_opt(bosh_service_url, Opts); bosh_service_url(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, bosh_service_url). -spec conversejs_css(gen_mod:opts() | global | binary()) -> binary(). conversejs_css(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_css, Opts); conversejs_css(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_css). -spec conversejs_script(gen_mod:opts() | global | binary()) -> binary(). conversejs_script(Opts) when is_map(Opts) -> gen_mod:get_opt(conversejs_script, Opts); conversejs_script(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, conversejs_script). -spec default_domain(gen_mod:opts() | global | binary()) -> binary(). default_domain(Opts) when is_map(Opts) -> gen_mod:get_opt(default_domain, Opts); default_domain(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, default_domain). -spec websocket_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). websocket_url(Opts) when is_map(Opts) -> gen_mod:get_opt(websocket_url, Opts); websocket_url(Host) -> gen_mod:get_module_opt(Host, mod_conversejs, websocket_url). ejabberd-21.12/src/mod_jidprep.erl0000644000232200023220000001376314154362354017433 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_jidprep.erl %%% Author : Holger Weiss %%% Purpose : JID Prep (XEP-0328) %%% Created : 11 Sep 2019 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2019-2021 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(mod_jidprep). -author('holger@zedat.fu-berlin.de'). -protocol({xep, 328, '0.1'}). -behaviour(gen_mod). %% gen_mod callbacks. -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. -export([disco_local_features/5]). %% gen_iq_handler callback. -export([process_iq/1]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> ok. start(Host, _Opts) -> register_iq_handlers(Host), register_hooks(Host). -spec stop(binary()) -> ok. stop(Host) -> unregister_hooks(Host), unregister_iq_handlers(Host). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(_Host, _NewOpts, _OldOpts) -> ok. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(access) -> econf:acl(). -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(_Host) -> [{access, local}]. mod_doc() -> #{desc => ?T("This module allows XMPP clients to ask the " "server to normalize a JID as per the rules specified " "in https://tools.ietf.org/html/rfc6122" "[RFC 6122: XMPP Address Format]. This might be useful " "for clients in certain constrained environments, " "or for testing purposes."), opts => [{access, #{value => ?T("AccessName"), desc => ?T("This option defines which access rule will " "be used to control who is allowed to use this " "service. The default value is 'local'.")}}]}. %%-------------------------------------------------------------------- %% Register/unregister hooks. %%-------------------------------------------------------------------- -spec register_hooks(binary()) -> ok. register_hooks(Host) -> ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_local_features, 50). -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_local_features, 50). %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- -spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). disco_local_features(empty, From, To, Node, Lang) -> disco_local_features({result, []}, From, To, Node, Lang); disco_local_features({result, OtherFeatures} = Acc, From, #jid{lserver = LServer}, <<"">>, _Lang) -> Access = mod_jidprep_opt:access(LServer), case acl:match_rule(LServer, Access, From) of allow -> {result, [?NS_JIDPREP_0 | OtherFeatures]}; deny -> Acc end; disco_local_features(Acc, _From, _To, _Node, _Lang) -> Acc. %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_JIDPREP_0, ?MODULE, process_iq). -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_JIDPREP_0). -spec process_iq(iq()) -> iq(). process_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_iq(#iq{from = From, to = #jid{lserver = LServer}, lang = Lang, sub_els = [#jidprep{jid = #jid{luser = U, lserver = S, lresource = R} = JID}]} = IQ) -> Access = mod_jidprep_opt:access(LServer), case acl:match_rule(LServer, Access, From) of allow -> case jid:make(U, S, R) of #jid{} = Normalized -> ?DEBUG("Normalized JID for ~ts: ~ts", [jid:encode(From), jid:encode(JID)]), xmpp:make_iq_result(IQ, #jidprep{jid = Normalized}); error -> % Cannot happen. ?DEBUG("Normalizing JID failed for ~ts: ~ts", [jid:encode(From), jid:encode(JID)]), Txt = ?T("JID normalization failed"), xmpp:make_error(IQ, xmpp:err_jid_malformed(Txt, Lang)) end; deny -> ?DEBUG("Won't return normalized JID to ~ts: ~ts", [jid:encode(From), jid:encode(JID)]), Txt = ?T("JID normalization denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). ejabberd-21.12/src/mod_avatar_opt.erl0000644000232200023220000000112614154362354020124 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_avatar_opt). -export([convert/1]). -export([rate_limit/1]). -spec convert(gen_mod:opts() | global | binary()) -> [mod_avatar:convert_rule()]. convert(Opts) when is_map(Opts) -> gen_mod:get_opt(convert, Opts); convert(Host) -> gen_mod:get_module_opt(Host, mod_avatar, convert). -spec rate_limit(gen_mod:opts() | global | binary()) -> pos_integer(). rate_limit(Opts) when is_map(Opts) -> gen_mod:get_opt(rate_limit, Opts); rate_limit(Host) -> gen_mod:get_module_opt(Host, mod_avatar, rate_limit). ejabberd-21.12/src/mod_http_upload_opt.erl0000644000232200023220000001105114154362354021167 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_http_upload_opt). -export([access/1]). -export([custom_headers/1]). -export([dir_mode/1]). -export([docroot/1]). -export([external_secret/1]). -export([file_mode/1]). -export([get_url/1]). -export([host/1]). -export([hosts/1]). -export([jid_in_url/1]). -export([max_size/1]). -export([name/1]). -export([put_url/1]). -export([rm_on_unregister/1]). -export([secret_length/1]). -export([service_url/1]). -export([thumbnail/1]). -export([vcard/1]). -spec access(gen_mod:opts() | global | binary()) -> 'local' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, access). -spec custom_headers(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. custom_headers(Opts) when is_map(Opts) -> gen_mod:get_opt(custom_headers, Opts); custom_headers(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, custom_headers). -spec dir_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). dir_mode(Opts) when is_map(Opts) -> gen_mod:get_opt(dir_mode, Opts); dir_mode(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, dir_mode). -spec docroot(gen_mod:opts() | global | binary()) -> binary(). docroot(Opts) when is_map(Opts) -> gen_mod:get_opt(docroot, Opts); docroot(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, docroot). -spec external_secret(gen_mod:opts() | global | binary()) -> binary(). external_secret(Opts) when is_map(Opts) -> gen_mod:get_opt(external_secret, Opts); external_secret(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, external_secret). -spec file_mode(gen_mod:opts() | global | binary()) -> 'undefined' | non_neg_integer(). file_mode(Opts) when is_map(Opts) -> gen_mod:get_opt(file_mode, Opts); file_mode(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, file_mode). -spec get_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). get_url(Opts) when is_map(Opts) -> gen_mod:get_opt(get_url, Opts); get_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, get_url). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, hosts). -spec jid_in_url(gen_mod:opts() | global | binary()) -> 'node' | 'sha1'. jid_in_url(Opts) when is_map(Opts) -> gen_mod:get_opt(jid_in_url, Opts); jid_in_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, jid_in_url). -spec max_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_size(Opts) when is_map(Opts) -> gen_mod:get_opt(max_size, Opts); max_size(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, max_size). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, name). -spec put_url(gen_mod:opts() | global | binary()) -> binary(). put_url(Opts) when is_map(Opts) -> gen_mod:get_opt(put_url, Opts); put_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, put_url). -spec rm_on_unregister(gen_mod:opts() | global | binary()) -> boolean(). rm_on_unregister(Opts) when is_map(Opts) -> gen_mod:get_opt(rm_on_unregister, Opts); rm_on_unregister(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, rm_on_unregister). -spec secret_length(gen_mod:opts() | global | binary()) -> 1..1114111. secret_length(Opts) when is_map(Opts) -> gen_mod:get_opt(secret_length, Opts); secret_length(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, secret_length). -spec service_url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). service_url(Opts) when is_map(Opts) -> gen_mod:get_opt(service_url, Opts); service_url(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, service_url). -spec thumbnail(gen_mod:opts() | global | binary()) -> boolean(). thumbnail(Opts) when is_map(Opts) -> gen_mod:get_opt(thumbnail, Opts); thumbnail(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, thumbnail). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_http_upload, vcard). ejabberd-21.12/src/mod_proxy65_lib.erl0000644000232200023220000000501114154362354020143 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_proxy65_lib.erl %%% Author : Evgeniy Khramtsov %%% Purpose : SOCKS5 parsing library. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_lib). -author('xram@jabber.ru'). -include("mod_proxy65.hrl"). -export([unpack_init_message/1, unpack_auth_request/1, unpack_request/1, make_init_reply/1, make_auth_reply/1, make_reply/1, make_error_reply/1, make_error_reply/2]). unpack_init_message(<<(?VERSION_5), N, AuthMethodList:N/binary>>) when N > 0, N < 256 -> {ok, binary_to_list(AuthMethodList)}; unpack_init_message(_) -> error. unpack_auth_request(<<1, ULen, User:ULen/binary, PLen, Pass:PLen/binary>>) when ULen < 256, PLen < 256 -> {(User), (Pass)}; unpack_auth_request(_) -> error. unpack_request(<<(?VERSION_5), CMD, RSV, (?ATYP_DOMAINNAME), 40, SHA1:40/binary, 0, 0>>) when CMD == (?CMD_CONNECT); CMD == (?CMD_UDP) -> Command = if CMD == (?CMD_CONNECT) -> connect; CMD == (?CMD_UDP) -> udp end, #s5_request{cmd = Command, rsv = RSV, sha1 = (SHA1)}; unpack_request(_) -> error. make_init_reply(Method) -> [?VERSION_5, Method]. make_auth_reply(true) -> [1, ?SUCCESS]; make_auth_reply(false) -> [1, ?ERR_NOT_ALLOWED]. make_reply(#s5_request{rsv = RSV, sha1 = SHA1}) -> [?VERSION_5, ?SUCCESS, RSV, ?ATYP_DOMAINNAME, byte_size(SHA1), SHA1, 0, 0]. make_error_reply(Request) -> make_error_reply(Request, ?ERR_NOT_ALLOWED). make_error_reply(#s5_request{rsv = RSV, sha1 = SHA1}, Reason) -> [?VERSION_5, Reason, RSV, ?ATYP_DOMAINNAME, byte_size(SHA1), SHA1, 0, 0]. ejabberd-21.12/src/eldap_filter_yecc.yrl0000644000232200023220000000541414154362354020612 0ustar debalancedebalanceNonterminals filter filtercomp filterlist item simple present substring extensible initial any final matchingrule xattr attr value. Terminals str '(' ')' '&' '|' '!' '=' '~=' '>=' '<=' '=*' '*' ':dn' ':' ':='. Rootsymbol filter. filter -> '(' filtercomp ')': '$2'. filtercomp -> '&' filterlist: 'and'('$2'). filtercomp -> '|' filterlist: 'or'('$2'). filtercomp -> '!' filter: 'not'('$2'). filtercomp -> item: '$1'. filterlist -> filter: '$1'. filterlist -> filter filterlist: flatten(['$1', '$2']). item -> simple: '$1'. item -> present: '$1'. item -> substring: '$1'. item -> extensible: '$1'. simple -> attr '=' value: equal('$1', '$3'). simple -> attr '~=' value: approx('$1', '$3'). simple -> attr '>=' value: greater('$1', '$3'). simple -> attr '<=' value: less('$1', '$3'). present -> attr '=*': present('$1'). substring -> attr '=' initial '*' any: substrings('$1', ['$3', '$5']). substring -> attr '=' '*' any final: substrings('$1', ['$4', '$5']). substring -> attr '=' initial '*' any final: substrings('$1', ['$3', '$5', '$6']). substring -> attr '=' '*' any: substrings('$1', ['$4']). any -> any value '*': 'any'('$1', '$2'). any -> '$empty': []. initial -> value: initial('$1'). final -> value: final('$1'). extensible -> xattr ':dn' ':' matchingrule ':=' value: extensible('$6', ['$1', '$4', {dnAttributes, true}]). extensible -> xattr ':' matchingrule ':=' value: extensible('$5', ['$1', '$3']). extensible -> xattr ':dn' ':=' value: extensible('$4', ['$1', {dnAttributes, true}]). extensible -> xattr ':=' value: extensible('$3', ['$1']). extensible -> ':dn' ':' matchingrule ':=' value: extensible('$5', ['$3']). extensible -> ':' matchingrule ':=' value: extensible('$4', ['$2']). xattr -> value: xattr('$1'). matchingrule -> value: matchingrule('$1'). attr -> str: value_of('$1'). value -> str: value_of('$1'). Erlang code. 'and'(Value) -> eldap:'and'(Value). 'or'(Value) -> eldap:'or'(Value). 'not'(Value) -> eldap:'not'(Value). equal(Desc, Value) -> eldap:equalityMatch(Desc, Value). approx(Desc, Value) -> eldap:approxMatch(Desc, Value). greater(Desc, Value) -> eldap:greaterOrEqual(Desc, Value). less(Desc, Value) -> eldap:lessOrEqual(Desc, Value). present(Value) -> eldap:present(Value). extensible(Value, Opts) -> eldap:extensibleMatch(Value, Opts). substrings(Desc, ValueList) -> eldap:substrings(Desc, flatten(ValueList)). initial(Value) -> {initial, Value}. final(Value) -> {final, Value}. 'any'(Token, Value) -> [Token, {any, Value}]. xattr(Value) -> {type, Value}. matchingrule(Value) -> {matchingRule, Value}. value_of(Token) -> iolist_to_binary(element(3, Token)). flatten(List) -> lists:flatten(List). ejabberd-21.12/src/mod_version.erl0000644000232200023220000000612614154362354017456 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_version.erl %%% Author : Alexey Shchepin %%% Purpose : XEP-0092: Software Version %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_version). -author('alexey@process-one.net'). -protocol({xep, 92, '1.1'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq/1, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VERSION, ?MODULE, process_local_iq). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VERSION). reload(_Host, _NewOpts, _OldOpts) -> ok. process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq(#iq{type = get, to = To} = IQ) -> Host = To#jid.lserver, OS = case mod_version_opt:show_os(Host) of true -> get_os(); false -> undefined end, xmpp:make_iq_result(IQ, #version{name = <<"ejabberd">>, ver = ejabberd_option:version(), os = OS}). get_os() -> {Osfamily, Osname} = os:type(), OSType = list_to_binary([atom_to_list(Osfamily), $/, atom_to_list(Osname)]), OSVersion = case os:version() of {Major, Minor, Release} -> (str:format("~w.~w.~w", [Major, Minor, Release])); VersionString -> VersionString end, <>. depends(_Host, _Opts) -> []. mod_opt_type(show_os) -> econf:bool(). mod_options(_Host) -> [{show_os, true}]. mod_doc() -> #{desc => ?T("This module implements " "https://xmpp.org/extensions/xep-0092.html" "[XEP-0092: Software Version]. Consequently, " "it answers ejabberd's version when queried."), opts => [{show_os, #{value => "true | false", desc => ?T("Should the operating system be revealed or not. " "The default value is 'true'.")}}]}. ejabberd-21.12/src/mod_vcard_ldap_opt.erl0000644000232200023220000001132014154362354020742 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_vcard_ldap_opt). -export([ldap_backups/1]). -export([ldap_base/1]). -export([ldap_deref_aliases/1]). -export([ldap_encrypt/1]). -export([ldap_filter/1]). -export([ldap_password/1]). -export([ldap_port/1]). -export([ldap_rootdn/1]). -export([ldap_search_fields/1]). -export([ldap_search_reported/1]). -export([ldap_servers/1]). -export([ldap_tls_cacertfile/1]). -export([ldap_tls_certfile/1]). -export([ldap_tls_depth/1]). -export([ldap_tls_verify/1]). -export([ldap_uids/1]). -export([ldap_vcard_map/1]). -spec ldap_backups(gen_mod:opts() | global | binary()) -> [binary()]. ldap_backups(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_backups, Opts); ldap_backups(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_backups). -spec ldap_base(gen_mod:opts() | global | binary()) -> binary(). ldap_base(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_base, Opts); ldap_base(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_base). -spec ldap_deref_aliases(gen_mod:opts() | global | binary()) -> 'always' | 'finding' | 'never' | 'searching'. ldap_deref_aliases(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_deref_aliases, Opts); ldap_deref_aliases(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_deref_aliases). -spec ldap_encrypt(gen_mod:opts() | global | binary()) -> 'none' | 'starttls' | 'tls'. ldap_encrypt(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_encrypt, Opts); ldap_encrypt(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_encrypt). -spec ldap_filter(gen_mod:opts() | global | binary()) -> binary(). ldap_filter(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_filter, Opts); ldap_filter(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_filter). -spec ldap_password(gen_mod:opts() | global | binary()) -> binary(). ldap_password(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_password, Opts); ldap_password(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_password). -spec ldap_port(gen_mod:opts() | global | binary()) -> 1..1114111. ldap_port(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_port, Opts); ldap_port(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_port). -spec ldap_rootdn(gen_mod:opts() | global | binary()) -> binary(). ldap_rootdn(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_rootdn, Opts); ldap_rootdn(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_rootdn). -spec ldap_search_fields(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. ldap_search_fields(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_search_fields, Opts); ldap_search_fields(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_search_fields). -spec ldap_search_reported(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. ldap_search_reported(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_search_reported, Opts); ldap_search_reported(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_search_reported). -spec ldap_servers(gen_mod:opts() | global | binary()) -> [binary()]. ldap_servers(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_servers, Opts); ldap_servers(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_servers). -spec ldap_tls_cacertfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_cacertfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_cacertfile, Opts); ldap_tls_cacertfile(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_cacertfile). -spec ldap_tls_certfile(gen_mod:opts() | global | binary()) -> binary(). ldap_tls_certfile(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_certfile, Opts); ldap_tls_certfile(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_certfile). -spec ldap_tls_depth(gen_mod:opts() | global | binary()) -> non_neg_integer(). ldap_tls_depth(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_depth, Opts); ldap_tls_depth(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_depth). -spec ldap_tls_verify(gen_mod:opts() | global | binary()) -> 'false' | 'hard' | 'soft'. ldap_tls_verify(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_tls_verify, Opts); ldap_tls_verify(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_tls_verify). -spec ldap_uids(gen_mod:opts() | global | binary()) -> [{binary(),binary()}]. ldap_uids(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_uids, Opts); ldap_uids(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_uids). -spec ldap_vcard_map(gen_mod:opts() | global | binary()) -> [{binary(),[{binary(),[binary()]}]}]. ldap_vcard_map(Opts) when is_map(Opts) -> gen_mod:get_opt(ldap_vcard_map, Opts); ldap_vcard_map(Host) -> gen_mod:get_module_opt(Host, mod_vcard, ldap_vcard_map). ejabberd-21.12/src/mod_muc.erl0000644000232200023220000021244414154362354016557 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_muc.erl %%% Author : Alexey Shchepin %%% Purpose : MUC support (XEP-0045) %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc). -author('alexey@process-one.net'). -protocol({xep, 45, '1.25'}). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). -behaviour(gen_mod). %% API -export([start/2, stop/1, start_link/2, reload/3, mod_doc/0, room_destroyed/4, store_room/4, store_room/5, store_changes/4, restore_room/3, forget_room/3, create_room/3, create_room/5, shutdown_rooms/1, process_disco_info/1, process_disco_items/1, process_vcard/1, process_register/1, process_muc_unique/1, process_mucsub/1, broadcast_service_message/3, export/1, import_info/0, import/5, import_start/2, opts_to_binary/1, find_online_room/2, register_online_room/3, get_online_rooms/1, count_online_rooms/1, register_online_user/4, unregister_online_user/4, iq_set_register_info/5, count_online_rooms_by_user/3, get_online_rooms_by_user/3, can_use_nick/4, get_subscribed_rooms/2, procname/2, route/1, unhibernate_room/3]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, mod_opt_type/1, mod_options/1, depends/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_muc.hrl"). -include("mod_muc_room.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -type state() :: #{hosts := [binary()], server_host := binary(), worker := pos_integer()}. -type access() :: {acl:acl(), acl:acl(), acl:acl(), acl:acl(), acl:acl()}. -type muc_room_opts() :: [{atom(), any()}]. -export_type([access/0]). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), [binary()]) -> ok. -callback store_room(binary(), binary(), binary(), list(), list()|undefined) -> {atomic, any()}. -callback store_changes(binary(), binary(), binary(), list()) -> {atomic, any()}. -callback restore_room(binary(), binary(), binary()) -> muc_room_opts() | error. -callback forget_room(binary(), binary(), binary()) -> {atomic, any()}. -callback can_use_nick(binary(), binary(), jid(), binary()) -> boolean(). -callback get_rooms(binary(), binary()) -> [#muc_room{}]. -callback get_nick(binary(), binary(), jid()) -> binary() | error. -callback set_nick(binary(), binary(), jid(), binary()) -> {atomic, ok | false}. -callback register_online_room(binary(), binary(), binary(), pid()) -> any(). -callback unregister_online_room(binary(), binary(), binary(), pid()) -> any(). -callback find_online_room(binary(), binary(), binary()) -> {ok, pid()} | error. -callback find_online_room_by_pid(binary(), pid()) -> {ok, binary(), binary()} | error. -callback get_online_rooms(binary(), binary(), undefined | rsm_set()) -> [{binary(), binary(), pid()}]. -callback count_online_rooms(binary(), binary()) -> non_neg_integer(). -callback rsm_supported() -> boolean(). -callback register_online_user(binary(), ljid(), binary(), binary()) -> any(). -callback unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). -callback count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). -callback get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. -callback get_subscribed_rooms(binary(), binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, db_failure}. -optional_callbacks([get_subscribed_rooms/3, store_changes/4]). %%==================================================================== %% API %%==================================================================== start(Host, Opts) -> case mod_muc_sup:start(Host) of {ok, _} -> MyHosts = gen_mod:get_opt_hosts(Opts), Mod = gen_mod:db_mod(Opts, ?MODULE), RMod = gen_mod:ram_db_mod(Opts, ?MODULE), Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), RMod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)), load_permanent_rooms(MyHosts, Host, Opts); Err -> Err end. stop(Host) -> Proc = mod_muc_sup:procname(Host), supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), supervisor:delete_child(ejabberd_gen_mod_sup, Proc). -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(ServerHost, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), NewRMod = gen_mod:ram_db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), OldRMod = gen_mod:ram_db_mod(OldOpts, ?MODULE), NewHosts = gen_mod:get_opt_hosts(NewOpts), OldHosts = gen_mod:get_opt_hosts(OldOpts), AddHosts = NewHosts -- OldHosts, DelHosts = OldHosts -- NewHosts, if NewMod /= OldMod -> NewMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); true -> ok end, if NewRMod /= OldRMod -> NewRMod:init(ServerHost, gen_mod:set_opt(hosts, NewHosts, NewOpts)); true -> ok end, lists:foreach( fun(I) -> ?GEN_SERVER:cast(procname(ServerHost, I), {reload, AddHosts, DelHosts, NewHosts}) end, lists:seq(1, erlang:system_info(logical_processors))), load_permanent_rooms(AddHosts, ServerHost, NewOpts), shutdown_rooms(ServerHost, DelHosts, OldRMod), lists:foreach( fun(Host) -> lists:foreach( fun({_, _, Pid}) when node(Pid) == node() -> mod_muc_room:config_reloaded(Pid); (_) -> ok end, get_online_rooms(ServerHost, Host)) end, misc:intersection(NewHosts, OldHosts)). depends(_Host, _Opts) -> [{mod_mam, soft}]. start_link(Host, I) -> Proc = procname(Host, I), ?GEN_SERVER:start_link({local, Proc}, ?MODULE, [Host, I], ejabberd_config:fsm_limit_opts([])). -spec procname(binary(), pos_integer() | {binary(), binary()}) -> atom(). procname(Host, I) when is_integer(I) -> binary_to_atom( <<(atom_to_binary(?MODULE, latin1))/binary, "_", Host/binary, "_", (integer_to_binary(I))/binary>>, utf8); procname(Host, RoomHost) -> Cores = erlang:system_info(logical_processors), I = erlang:phash2(RoomHost, Cores) + 1, procname(Host, I). -spec route(stanza()) -> ok. route(Pkt) -> To = xmpp:get_to(Pkt), ServerHost = ejabberd_router:host_of_route(To#jid.lserver), route(Pkt, ServerHost). -spec route(stanza(), binary()) -> ok. route(Pkt, ServerHost) -> From = xmpp:get_from(Pkt), To = xmpp:get_to(Pkt), Host = To#jid.lserver, Access = mod_muc_opt:access(ServerHost), case acl:match_rule(ServerHost, Access, From) of allow -> route(Pkt, Host, ServerHost); deny -> Lang = xmpp:get_lang(Pkt), ErrText = ?T("Access denied by service policy"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Pkt, Err) end. -spec route(stanza(), binary(), binary()) -> ok. route(#iq{to = #jid{luser = <<"">>, lresource = <<"">>}} = IQ, _, _) -> ejabberd_router:process_iq(IQ); route(#message{lang = Lang, body = Body, type = Type, from = From, to = #jid{luser = <<"">>, lresource = <<"">>}} = Pkt, Host, ServerHost) -> if Type == error -> ok; true -> AccessAdmin = mod_muc_opt:access_admin(ServerHost), case acl:match_rule(ServerHost, AccessAdmin, From) of allow -> Msg = xmpp:get_text(Body), broadcast_service_message(ServerHost, Host, Msg); deny -> ErrText = ?T("Only service administrators are allowed " "to send service messages"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Pkt, Err) end end; route(Pkt, Host, ServerHost) -> {Room, _, _} = jid:tolower(xmpp:get_to(Pkt)), case Room of <<"">> -> Txt = ?T("No module is handling this query"), Err = xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)), ejabberd_router:route_error(Pkt, Err); _ -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room(ServerHost, Room, Host) of error -> Proc = procname(ServerHost, {Room, Host}), case whereis(Proc) of Pid when Pid == self() -> route_to_room(Pkt, ServerHost); Pid when is_pid(Pid) -> ?DEBUG("Routing to MUC worker ~p:~n~ts", [Proc, xmpp:pp(Pkt)]), ?GEN_SERVER:cast(Pid, {route_to_room, Pkt}); undefined -> ?DEBUG("MUC worker ~p is dead", [Proc]), Err = xmpp:err_internal_server_error(), ejabberd_router:route_error(Pkt, Err) end; {ok, Pid} -> mod_muc_room:route(Pid, Pkt) end end. -spec shutdown_rooms(binary()) -> [pid()]. shutdown_rooms(ServerHost) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), Hosts = gen_mod:get_module_opt_hosts(ServerHost, mod_muc), shutdown_rooms(ServerHost, Hosts, RMod). -spec shutdown_rooms(binary(), [binary()], module()) -> [pid()]. shutdown_rooms(ServerHost, Hosts, RMod) -> Rooms = [RMod:get_online_rooms(ServerHost, Host, undefined) || Host <- Hosts], lists:flatmap( fun({_, _, Pid}) when node(Pid) == node() -> mod_muc_room:shutdown(Pid), [Pid]; (_) -> [] end, lists:flatten(Rooms)). %% This function is called by a room in three situations: %% A) The owner of the room destroyed it %% B) The only participant of a temporary room leaves it %% C) mod_muc:stop was called, and each room is being terminated %% In this case, the mod_muc process died before the room processes %% So the message sending must be caught -spec room_destroyed(binary(), binary(), pid(), binary()) -> ok. room_destroyed(Host, Room, Pid, ServerHost) -> Proc = procname(ServerHost, {Room, Host}), ?GEN_SERVER:cast(Proc, {room_destroyed, {Room, Host}, Pid}). %% @doc Create a room. %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. create_room(Host, Name, From, Nick, Opts) -> ServerHost = ejabberd_router:host_of_route(Host), Proc = procname(ServerHost, {Name, Host}), ?GEN_SERVER:call(Proc, {create, Name, Host, From, Nick, Opts}). %% @doc Create a room. %% If Opts = default, the default room options are used. %% Else use the passed options as defined in mod_muc_room. create_room(Host, Name, Opts) -> ServerHost = ejabberd_router:host_of_route(Host), Proc = procname(ServerHost, {Name, Host}), ?GEN_SERVER:call(Proc, {create, Name, Host, Opts}). store_room(ServerHost, Host, Name, Opts) -> store_room(ServerHost, Host, Name, Opts, undefined). store_room(ServerHost, Host, Name, Opts, ChangesHints) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store_room(LServer, Host, Name, Opts, ChangesHints). store_changes(ServerHost, Host, Name, ChangesHints) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store_changes(LServer, Host, Name, ChangesHints). restore_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:restore_room(LServer, Host, Name). forget_room(ServerHost, Host, Name) -> LServer = jid:nameprep(ServerHost), ejabberd_hooks:run(remove_room, LServer, [LServer, Name, Host]), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:forget_room(LServer, Host, Name). can_use_nick(_ServerHost, _Host, _JID, <<"">>) -> false; can_use_nick(ServerHost, Host, JID, Nick) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:can_use_nick(LServer, Host, JID, Nick). -spec find_online_room(binary(), binary()) -> {ok, pid()} | error. find_online_room(Room, Host) -> ServerHost = ejabberd_router:host_of_route(Host), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:find_online_room(ServerHost, Room, Host). -spec register_online_room(binary(), binary(), pid()) -> any(). register_online_room(Room, Host, Pid) -> ServerHost = ejabberd_router:host_of_route(Host), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_room(ServerHost, Room, Host, Pid). -spec get_online_rooms(binary()) -> [{binary(), binary(), pid()}]. get_online_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), get_online_rooms(ServerHost, Host). -spec count_online_rooms(binary()) -> non_neg_integer(). count_online_rooms(Host) -> ServerHost = ejabberd_router:host_of_route(Host), count_online_rooms(ServerHost, Host). -spec register_online_user(binary(), ljid(), binary(), binary()) -> any(). register_online_user(ServerHost, LJID, Name, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:register_online_user(ServerHost, LJID, Name, Host). -spec unregister_online_user(binary(), ljid(), binary(), binary()) -> any(). unregister_online_user(ServerHost, LJID, Name, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:unregister_online_user(ServerHost, LJID, Name, Host). -spec count_online_rooms_by_user(binary(), binary(), binary()) -> non_neg_integer(). count_online_rooms_by_user(ServerHost, LUser, LServer) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:count_online_rooms_by_user(ServerHost, LUser, LServer). -spec get_online_rooms_by_user(binary(), binary(), binary()) -> [{binary(), binary()}]. get_online_rooms_by_user(ServerHost, LUser, LServer) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:get_online_rooms_by_user(ServerHost, LUser, LServer). %%==================================================================== %% gen_server callbacks %%==================================================================== -spec init(list()) -> {ok, state()}. init([Host, Worker]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), MyHosts = gen_mod:get_opt_hosts(Opts), register_routes(Host, MyHosts, Worker), register_iq_handlers(MyHosts, Worker), {ok, #{server_host => Host, hosts => MyHosts, worker => Worker}}. -spec handle_call(term(), {pid(), term()}, state()) -> {reply, ok | {ok, pid()} | {error, any()}, state()} | {stop, normal, ok, state()}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call({unhibernate, Room, Host}, _From, #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), {reply, load_room(RMod, Host, ServerHost, Room), State}; handle_call({create, Room, Host, Opts}, _From, #{server_host := ServerHost} = State) -> ?DEBUG("MUC: create new room '~ts'~n", [Room]), NewOpts = case Opts of default -> mod_muc_opt:default_room_options(ServerHost); _ -> Opts end, RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case start_room(RMod, Host, ServerHost, Room, NewOpts) of {ok, _} -> ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), {reply, ok, State}; Err -> {reply, Err, State} end; handle_call({create, Room, Host, From, Nick, Opts}, _From, #{server_host := ServerHost} = State) -> ?DEBUG("MUC: create new room '~ts'~n", [Room]), NewOpts = case Opts of default -> mod_muc_opt:default_room_options(ServerHost); _ -> Opts end, RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case start_room(RMod, Host, ServerHost, Room, NewOpts, From, Nick) of {ok, _} -> ejabberd_hooks:run(create_room, ServerHost, [ServerHost, Room, Host]), {reply, ok, State}; Err -> {reply, Err, State} end. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast({route_to_room, Packet}, #{server_host := ServerHost} = State) -> try route_to_room(Packet, ServerHost) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_cast({room_destroyed, {Room, Host}, Pid}, #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:unregister_online_room(ServerHost, Room, Host, Pid), {noreply, State}; handle_cast({reload, AddHosts, DelHosts, NewHosts}, #{server_host := ServerHost, worker := Worker} = State) -> register_routes(ServerHost, AddHosts, Worker), register_iq_handlers(AddHosts, Worker), unregister_routes(DelHosts, Worker), unregister_iq_handlers(DelHosts, Worker), {noreply, State#{hosts => NewHosts}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info({route, Packet}, #{server_host := ServerHost} = State) -> %% We can only receive the packet here from other nodes %% where mod_muc is not loaded. Such configuration %% is *highly* discouraged try route(Packet, ServerHost) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info({room_destroyed, {Room, Host}, Pid}, State) -> %% For backward compat handle_cast({room_destroyed, {Room, Host}, Pid}, State); handle_info({'DOWN', _Ref, process, Pid, _Reason}, #{server_host := ServerHost} = State) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room_by_pid(ServerHost, Pid) of {ok, Room, Host} -> handle_cast({room_destroyed, {Room, Host}, Pid}, State); _ -> {noreply, State} end; handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(term(), state()) -> any(). terminate(_Reason, #{hosts := Hosts, worker := Worker}) -> unregister_routes(Hosts, Worker), unregister_iq_handlers(Hosts, Worker). -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec register_iq_handlers([binary()], pos_integer()) -> ok. register_iq_handlers(Hosts, 1) -> %% Only register handlers on first worker lists:foreach( fun(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER, ?MODULE, process_register), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_vcard), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUCSUB, ?MODULE, process_mucsub), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE, ?MODULE, process_muc_unique), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items) end, Hosts); register_iq_handlers(_, _) -> ok. -spec unregister_iq_handlers([binary()], pos_integer()) -> ok. unregister_iq_handlers(Hosts, 1) -> %% Only unregister handlers on first worker lists:foreach( fun(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUCSUB), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MUC_UNIQUE), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS) end, Hosts); unregister_iq_handlers(_, _) -> ok. -spec register_routes(binary(), [binary()], pos_integer()) -> ok. register_routes(ServerHost, Hosts, 1) -> %% Only register routes on first worker lists:foreach( fun(Host) -> ejabberd_router:register_route( Host, ServerHost, {apply, ?MODULE, route}) end, Hosts); register_routes(_, _, _) -> ok. -spec unregister_routes([binary()], pos_integer()) -> ok. unregister_routes(Hosts, 1) -> %% Only unregister routes on first worker lists:foreach( fun(Host) -> ejabberd_router:unregister_route(Host) end, Hosts); unregister_routes(_, _) -> ok. %% Function copied from mod_muc_room.erl -spec extract_password(presence() | iq()) -> binary() | false. extract_password(#presence{} = Pres) -> case xmpp:get_subtag(Pres, #muc{}) of #muc{password = Password} when is_binary(Password) -> Password; _ -> false end; extract_password(#iq{} = IQ) -> case xmpp:get_subtag(IQ, #muc_subscribe{}) of #muc_subscribe{password = Password} when Password /= <<"">> -> Password; _ -> false end. -spec unhibernate_room(binary(), binary(), binary()) -> {ok, pid()} | error. unhibernate_room(ServerHost, Host, Room) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room(ServerHost, Room, Host) of error -> Proc = procname(ServerHost, {Room, Host}), case ?GEN_SERVER:call(Proc, {unhibernate, Room, Host}, 20000) of {ok, _} = R -> R; _ -> error end; {ok, _} = R2 -> R2 end. -spec route_to_room(stanza(), binary()) -> ok. route_to_room(Packet, ServerHost) -> From = xmpp:get_from(Packet), To = xmpp:get_to(Packet), {Room, Host, Nick} = jid:tolower(To), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), case RMod:find_online_room(ServerHost, Room, Host) of error -> case should_start_room(Packet) of false -> Lang = xmpp:get_lang(Packet), ErrText = ?T("Conference room does not exist"), Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); StartType -> case load_room(RMod, Host, ServerHost, Room) of {error, notfound} when StartType == start -> case check_create_room(ServerHost, Host, Room, From) of true -> Pass = extract_password(Packet), case start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) of {ok, Pid} -> mod_muc_room:route(Pid, Packet); _Err -> Err = xmpp:err_internal_server_error(), ejabberd_router:route_error(Packet, Err) end; false -> Lang = xmpp:get_lang(Packet), ErrText = ?T("Room creation is denied by service policy"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end; {error, notfound} -> Lang = xmpp:get_lang(Packet), ErrText = ?T("Conference room does not exist"), Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); {error, _} -> Err = xmpp:err_internal_server_error(), ejabberd_router:route_error(Packet, Err); {ok, Pid2} -> mod_muc_room:route(Pid2, Packet) end end; {ok, Pid} -> mod_muc_room:route(Pid, Packet) end. -spec process_vcard(iq()) -> iq(). process_vcard(#iq{type = get, to = To, lang = Lang, sub_els = [#vcard_temp{}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), VCard = case mod_muc_opt:vcard(ServerHost) of undefined -> #vcard_temp{fn = <<"ejabberd/mod_muc">>, url = ejabberd_config:get_uri(), desc = misc:get_descr(Lang, ?T("ejabberd MUC module"))}; V -> V end, xmpp:make_iq_result(IQ, VCard); process_vcard(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_vcard(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_register(iq()) -> iq(). process_register(#iq{type = Type, from = From, to = To, lang = Lang, sub_els = [El = #register{}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), AccessRegister = mod_muc_opt:access_register(ServerHost), case acl:match_rule(ServerHost, AccessRegister, From) of allow -> case Type of get -> xmpp:make_iq_result( IQ, iq_get_register_info(ServerHost, Host, From, Lang)); set -> case process_iq_register_set(ServerHost, Host, From, El, Lang) of {result, Result} -> xmpp:make_iq_result(IQ, Result); {error, Err} -> xmpp:make_error(IQ, Err) end end; deny -> ErrText = ?T("Access denied by service policy"), Err = xmpp:err_forbidden(ErrText, Lang), xmpp:make_error(IQ, Err) end. -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_info(#iq{type = get, from = From, to = To, lang = Lang, sub_els = [#disco_info{node = <<"">>}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), AccessRegister = mod_muc_opt:access_register(ServerHost), X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<"">>, Lang]), MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]; false -> [] end, RSMFeatures = case RMod:rsm_supported() of true -> [?NS_RSM]; false -> [] end, RegisterFeatures = case acl:match_rule(ServerHost, AccessRegister, From) of allow -> [?NS_REGISTER]; deny -> [] end, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE | RegisterFeatures ++ RSMFeatures ++ MAMFeatures], Name = mod_muc_opt:name(ServerHost), Identity = #identity{category = <<"conference">>, type = <<"text">>, name = translate:translate(Lang, Name)}, xmpp:make_iq_result( IQ, #disco_info{features = Features, identities = [Identity], xdata = X}); process_disco_info(#iq{type = get, lang = Lang, sub_els = [#disco_info{}]} = IQ) -> xmpp:make_error(IQ, xmpp:err_item_not_found(?T("Node not found"), Lang)); process_disco_info(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_items(#iq{type = get, from = From, to = To, lang = Lang, sub_els = [#disco_items{node = Node, rsm = RSM}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), MaxRoomsDiscoItems = mod_muc_opt:max_rooms_discoitems(ServerHost), case iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM) of {error, Err} -> xmpp:make_error(IQ, Err); {result, Result} -> xmpp:make_iq_result(IQ, Result) end; process_disco_items(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_muc_unique(iq()) -> iq(). process_muc_unique(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_muc_unique(#iq{from = From, type = get, sub_els = [#muc_unique{}]} = IQ) -> Name = str:sha(term_to_binary([From, erlang:timestamp(), p1_rand:get_string()])), xmpp:make_iq_result(IQ, #muc_unique{name = Name}). -spec process_mucsub(iq()) -> iq(). process_mucsub(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_mucsub(#iq{type = get, from = From, to = To, lang = Lang, sub_els = [#muc_subscriptions{}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), case get_subscribed_rooms(ServerHost, Host, From) of {ok, Subs} -> List = [#muc_subscription{jid = JID, nick = Nick, events = Nodes} || {JID, Nick, Nodes} <- Subs], xmpp:make_iq_result(IQ, #muc_subscriptions{list = List}); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; process_mucsub(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec should_start_room(stanza()) -> start | load | false. should_start_room(#presence{type = available}) -> start; should_start_room(#iq{type = T} = IQ) when T == get; T == set -> case xmpp:has_subtag(IQ, #muc_subscribe{}) orelse xmpp:has_subtag(IQ, #muc_owner{}) of true -> start; _ -> load end; should_start_room(#message{type = T, to = #jid{lresource = <<>>}}) when T == groupchat; T == normal-> load; should_start_room(#message{type = T, to = #jid{lresource = Res}}) when Res /= <<>> andalso T /= groupchat andalso T /= error -> load; should_start_room(_) -> false. -spec check_create_room(binary(), binary(), binary(), jid()) -> boolean(). check_create_room(ServerHost, Host, Room, From) -> AccessCreate = mod_muc_opt:access_create(ServerHost), case acl:match_rule(ServerHost, AccessCreate, From) of allow -> case mod_muc_opt:max_room_id(ServerHost) of Max when byte_size(Room) =< Max -> Regexp = mod_muc_opt:regexp_room_id(ServerHost), case re:run(Room, Regexp, [{capture, none}]) of match -> AccessAdmin = mod_muc_opt:access_admin(ServerHost), case acl:match_rule(ServerHost, AccessAdmin, From) of allow -> true; _ -> ejabberd_hooks:run_fold( check_create_room, ServerHost, true, [ServerHost, Room, Host]) end; _ -> false end; _ -> false end; _ -> false end. -spec get_access(binary() | gen_mod:opts()) -> access(). get_access(ServerHost) -> Access = mod_muc_opt:access(ServerHost), AccessCreate = mod_muc_opt:access_create(ServerHost), AccessAdmin = mod_muc_opt:access_admin(ServerHost), AccessPersistent = mod_muc_opt:access_persistent(ServerHost), AccessMam = mod_muc_opt:access_mam(ServerHost), {Access, AccessCreate, AccessAdmin, AccessPersistent, AccessMam}. -spec get_rooms(binary(), binary()) -> [#muc_room{}]. get_rooms(ServerHost, Host) -> Mod = gen_mod:db_mod(ServerHost, ?MODULE), Mod:get_rooms(ServerHost, Host). -spec load_permanent_rooms([binary()], binary(), gen_mod:opts()) -> ok. load_permanent_rooms(Hosts, ServerHost, Opts) -> case mod_muc_opt:preload_rooms(Opts) of true -> lists:foreach( fun(Host) -> ?DEBUG("Loading rooms at ~ts", [Host]), lists:foreach( fun(R) -> {Room, _} = R#muc_room.name_host, unhibernate_room(ServerHost, Host, Room) end, get_rooms(ServerHost, Host)) end, Hosts); false -> ok end. -spec load_room(module(), binary(), binary(), binary()) -> {ok, pid()} | {error, notfound | term()}. load_room(RMod, Host, ServerHost, Room) -> case restore_room(ServerHost, Host, Room) of error -> {error, notfound}; Opts0 -> case proplists:get_bool(persistent, Opts0) of true -> ?DEBUG("Restore room: ~ts", [Room]), start_room(RMod, Host, ServerHost, Room, Opts0); _ -> ?DEBUG("Restore hibernated non-persistent room: ~ts", [Room]), Res = start_room(RMod, Host, ServerHost, Room, Opts0), Mod = gen_mod:db_mod(ServerHost, mod_muc), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of true -> ok; _ -> forget_room(ServerHost, Host, Room) end, Res end end. start_new_room(RMod, Host, ServerHost, Room, Pass, From, Nick) -> ?DEBUG("Open new room: ~ts", [Room]), DefRoomOpts = mod_muc_opt:default_room_options(ServerHost), DefRoomOpts2 = add_password_options(Pass, DefRoomOpts), start_room(RMod, Host, ServerHost, Room, DefRoomOpts2, From, Nick). add_password_options(false, DefRoomOpts) -> DefRoomOpts; add_password_options(<<>>, DefRoomOpts) -> DefRoomOpts; add_password_options(Pass, DefRoomOpts) when is_binary(Pass) -> O2 = lists:keystore(password, 1, DefRoomOpts, {password, Pass}), lists:keystore(password_protected, 1, O2, {password_protected, true}). start_room(Mod, Host, ServerHost, Room, DefOpts) -> Access = get_access(ServerHost), HistorySize = mod_muc_opt:history_size(ServerHost), QueueType = mod_muc_opt:queue_type(ServerHost), RoomShaper = mod_muc_opt:room_shaper(ServerHost), start_room(Mod, Host, ServerHost, Access, Room, HistorySize, RoomShaper, DefOpts, QueueType). start_room(Mod, Host, ServerHost, Room, DefOpts, Creator, Nick) -> Access = get_access(ServerHost), HistorySize = mod_muc_opt:history_size(ServerHost), QueueType = mod_muc_opt:queue_type(ServerHost), RoomShaper = mod_muc_opt:room_shaper(ServerHost), start_room(Mod, Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefOpts, QueueType). start_room(Mod, Host, ServerHost, Access, Room, HistorySize, RoomShaper, DefOpts, QueueType) -> case mod_muc_room:start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, DefOpts, QueueType) of {ok, Pid} -> erlang:monitor(process, Pid), Mod:register_online_room(ServerHost, Room, Host, Pid), {ok, Pid}; Err -> Err end. start_room(Mod, Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefOpts, QueueType) -> case mod_muc_room:start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefOpts, QueueType) of {ok, Pid} -> erlang:monitor(process, Pid), Mod:register_online_room(ServerHost, Room, Host, Pid), {ok, Pid}; Err -> Err end. -spec iq_disco_items(binary(), binary(), jid(), binary(), integer(), binary(), rsm_set() | undefined) -> {result, disco_items()} | {error, stanza_error()}. iq_disco_items(ServerHost, Host, From, Lang, MaxRoomsDiscoItems, Node, RSM) when Node == <<"">>; Node == <<"nonemptyrooms">>; Node == <<"emptyrooms">> -> Count = count_online_rooms(ServerHost, Host), Query = if Node == <<"">>, RSM == undefined, Count > MaxRoomsDiscoItems -> {only_non_empty, From, Lang}; Node == <<"nonemptyrooms">> -> {only_non_empty, From, Lang}; Node == <<"emptyrooms">> -> {0, From, Lang}; true -> {all, From, Lang} end, MaxItems = case RSM of undefined -> MaxRoomsDiscoItems; #rsm_set{max = undefined} -> MaxRoomsDiscoItems; #rsm_set{max = Max} when Max > MaxRoomsDiscoItems -> MaxRoomsDiscoItems; #rsm_set{max = Max} -> Max end, {Items, HitMax} = lists:foldr( fun(_, {Acc, _}) when length(Acc) >= MaxItems -> {Acc, true}; (R, {Acc, _}) -> case get_room_disco_item(R, Query) of {ok, Item} -> {[Item | Acc], false}; {error, _} -> {Acc, false} end end, {[], false}, get_online_rooms(ServerHost, Host, RSM)), ResRSM = case Items of [_|_] when RSM /= undefined; HitMax -> #disco_item{jid = #jid{luser = First}} = hd(Items), #disco_item{jid = #jid{luser = Last}} = lists:last(Items), #rsm_set{first = #rsm_first{data = First}, last = Last, count = Count}; [] when RSM /= undefined -> #rsm_set{count = Count}; _ -> undefined end, {result, #disco_items{node = Node, items = Items, rsm = ResRSM}}; iq_disco_items(_ServerHost, _Host, _From, Lang, _MaxRoomsDiscoItems, _Node, _RSM) -> {error, xmpp:err_item_not_found(?T("Node not found"), Lang)}. -spec get_room_disco_item({binary(), binary(), pid()}, {mod_muc_room:disco_item_filter(), jid(), binary()}) -> {ok, disco_item()} | {error, timeout | notfound}. get_room_disco_item({Name, Host, Pid}, {Filter, JID, Lang}) -> case mod_muc_room:get_disco_item(Pid, Filter, JID, Lang) of {ok, Desc} -> RoomJID = jid:make(Name, Host), {ok, #disco_item{jid = RoomJID, name = Desc}}; {error, _} = Err -> Err end. -spec get_subscribed_rooms(binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. get_subscribed_rooms(Host, User) -> ServerHost = ejabberd_router:host_of_route(Host), get_subscribed_rooms(ServerHost, Host, User). -spec get_subscribed_rooms(binary(), binary(), jid()) -> {ok, [{jid(), binary(), [binary()]}]} | {error, any()}. get_subscribed_rooms(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), BareFrom = jid:remove_resource(From), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of false -> Rooms = get_online_rooms(ServerHost, Host), {ok, lists:flatmap( fun({Name, _, Pid}) when Pid == self() -> USR = jid:split(BareFrom), case erlang:get(muc_subscribers) of #{USR := #subscriber{nodes = Nodes, nick = Nick}} -> [{jid:make(Name, Host), Nick, Nodes}]; _ -> [] end; ({Name, _, Pid}) -> case mod_muc_room:is_subscribed(Pid, BareFrom) of {true, Nick, Nodes} -> [{jid:make(Name, Host), Nick, Nodes}]; false -> [] end; (_) -> [] end, Rooms)}; true -> Mod:get_subscribed_rooms(LServer, Host, BareFrom) end. get_nick(ServerHost, Host, From) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:get_nick(LServer, Host, From). iq_get_register_info(ServerHost, Host, From, Lang) -> {Nick, Registered} = case get_nick(ServerHost, Host, From) of error -> {<<"">>, false}; N -> {N, true} end, Title = <<(translate:translate( Lang, ?T("Nickname Registration at ")))/binary, Host/binary>>, Inst = translate:translate(Lang, ?T("Enter nickname you want to register")), Fields = muc_register:encode([{roomnick, Nick}], Lang), X = #xdata{type = form, title = Title, instructions = [Inst], fields = Fields}, #register{nick = Nick, registered = Registered, instructions = translate:translate( Lang, ?T("You need a client that supports x:data " "to register the nickname")), xdata = X}. set_nick(ServerHost, Host, From, Nick) -> LServer = jid:nameprep(ServerHost), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:set_nick(LServer, Host, From, Nick). iq_set_register_info(ServerHost, Host, From, Nick, Lang) -> case set_nick(ServerHost, Host, From, Nick) of {atomic, ok} -> {result, undefined}; {atomic, false} -> ErrText = ?T("That nickname is registered by another person"), {error, xmpp:err_conflict(ErrText, Lang)}; _ -> Txt = ?T("Database failure"), {error, xmpp:err_internal_server_error(Txt, Lang)} end. process_iq_register_set(ServerHost, Host, From, #register{remove = true}, Lang) -> iq_set_register_info(ServerHost, Host, From, <<"">>, Lang); process_iq_register_set(_ServerHost, _Host, _From, #register{xdata = #xdata{type = cancel}}, _Lang) -> {result, undefined}; process_iq_register_set(ServerHost, Host, From, #register{nick = Nick, xdata = XData}, Lang) -> case XData of #xdata{type = submit, fields = Fs} -> try Options = muc_register:decode(Fs), N = proplists:get_value(roomnick, Options), iq_set_register_info(ServerHost, Host, From, N, Lang) catch _:{muc_register, Why} -> ErrText = muc_register:format_error(Why), {error, xmpp:err_bad_request(ErrText, Lang)} end; #xdata{} -> Txt = ?T("Incorrect data form"), {error, xmpp:err_bad_request(Txt, Lang)}; _ when is_binary(Nick), Nick /= <<"">> -> iq_set_register_info(ServerHost, Host, From, Nick, Lang); _ -> ErrText = ?T("You must fill in field \"Nickname\" in the form"), {error, xmpp:err_not_acceptable(ErrText, Lang)} end. -spec broadcast_service_message(binary(), binary(), binary()) -> ok. broadcast_service_message(ServerHost, Host, Msg) -> lists:foreach( fun({_, _, Pid}) -> mod_muc_room:service_message(Pid, Msg) end, get_online_rooms(ServerHost, Host)). -spec get_online_rooms(binary(), binary()) -> [{binary(), binary(), pid()}]. get_online_rooms(ServerHost, Host) -> get_online_rooms(ServerHost, Host, undefined). -spec get_online_rooms(binary(), binary(), undefined | rsm_set()) -> [{binary(), binary(), pid()}]. get_online_rooms(ServerHost, Host, RSM) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:get_online_rooms(ServerHost, Host, RSM). -spec count_online_rooms(binary(), binary()) -> non_neg_integer(). count_online_rooms(ServerHost, Host) -> RMod = gen_mod:ram_db_mod(ServerHost, ?MODULE), RMod:count_online_rooms(ServerHost, Host). opts_to_binary(Opts) -> lists:map( fun({title, Title}) -> {title, iolist_to_binary(Title)}; ({description, Desc}) -> {description, iolist_to_binary(Desc)}; ({password, Pass}) -> {password, iolist_to_binary(Pass)}; ({subject, [C|_] = Subj}) when is_integer(C), C >= 0, C =< 255 -> {subject, iolist_to_binary(Subj)}; ({subject_author, Author}) -> {subject_author, iolist_to_binary(Author)}; ({affiliations, Affs}) -> {affiliations, lists:map( fun({{U, S, R}, Aff}) -> NewAff = case Aff of {A, Reason} -> {A, iolist_to_binary(Reason)}; _ -> Aff end, {{iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)}, NewAff} end, Affs)}; ({captcha_whitelist, CWList}) -> {captcha_whitelist, lists:map( fun({U, S, R}) -> {iolist_to_binary(U), iolist_to_binary(S), iolist_to_binary(R)} end, CWList)}; (Opt) -> Opt end, Opts). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"muc_room">>, 4}, {<<"muc_registered">>, 4}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, DBType, Tab, L) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, Tab, L). mod_opt_type(access) -> econf:acl(); mod_opt_type(access_admin) -> econf:acl(); mod_opt_type(access_create) -> econf:acl(); mod_opt_type(access_persistent) -> econf:acl(); mod_opt_type(access_mam) -> econf:acl(); mod_opt_type(access_register) -> econf:acl(); mod_opt_type(history_size) -> econf:non_neg_int(); mod_opt_type(name) -> econf:binary(); mod_opt_type(max_room_desc) -> econf:pos_int(infinity); mod_opt_type(max_room_id) -> econf:pos_int(infinity); mod_opt_type(max_rooms_discoitems) -> econf:non_neg_int(); mod_opt_type(regexp_room_id) -> econf:re([unicode]); mod_opt_type(max_room_name) -> econf:pos_int(infinity); mod_opt_type(max_password) -> econf:pos_int(infinity); mod_opt_type(max_captcha_whitelist) -> econf:pos_int(infinity); mod_opt_type(max_user_conferences) -> econf:pos_int(); mod_opt_type(max_users) -> econf:pos_int(); mod_opt_type(max_users_admin_threshold) -> econf:pos_int(); mod_opt_type(max_users_presence) -> econf:int(); mod_opt_type(min_message_interval) -> econf:number(0); mod_opt_type(min_presence_interval) -> econf:number(0); mod_opt_type(preload_rooms) -> econf:bool(); mod_opt_type(room_shaper) -> econf:atom(); mod_opt_type(user_message_shaper) -> econf:atom(); mod_opt_type(user_presence_shaper) -> econf:atom(); mod_opt_type(default_room_options) -> econf:options( #{allow_change_subj => econf:bool(), allow_private_messages => econf:bool(), allow_private_messages_from_visitors => econf:enum([anyone, moderators, nobody]), allow_query_users => econf:bool(), allow_subscription => econf:bool(), allow_user_invites => econf:bool(), allow_visitor_nickchange => econf:bool(), allow_visitor_status => econf:bool(), anonymous => econf:bool(), captcha_protected => econf:bool(), lang => econf:lang(), logging => econf:bool(), mam => econf:bool(), max_users => econf:pos_int(), members_by_default => econf:bool(), members_only => econf:bool(), moderated => econf:bool(), password => econf:binary(), password_protected => econf:bool(), persistent => econf:bool(), presence_broadcast => econf:list( econf:enum([moderator, participant, visitor])), public => econf:bool(), public_list => econf:bool(), title => econf:binary()}); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(ram_db_type) -> econf:db_type(?MODULE); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(queue_type) -> econf:queue_type(); mod_opt_type(hibernation_timeout) -> econf:timeout(second, infinity); mod_opt_type(vcard) -> econf:vcard_temp(). mod_options(Host) -> [{access, all}, {access_admin, none}, {access_create, all}, {access_persistent, all}, {access_mam, all}, {access_register, all}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, {history_size, 20}, {host, <<"conference.", Host/binary>>}, {hosts, []}, {name, ?T("Chatrooms")}, {max_room_desc, infinity}, {max_room_id, infinity}, {max_room_name, infinity}, {max_password, infinity}, {max_captcha_whitelist, infinity}, {max_rooms_discoitems, 100}, {max_user_conferences, 100}, {max_users, 200}, {max_users_admin_threshold, 5}, {max_users_presence, 1000}, {min_message_interval, 0}, {min_presence_interval, 0}, {queue_type, ejabberd_option:queue_type(Host)}, {regexp_room_id, <<"">>}, {room_shaper, none}, {user_message_shaper, none}, {user_presence_shaper, none}, {preload_rooms, true}, {hibernation_timeout, infinity}, {vcard, undefined}, {default_room_options, [{allow_change_subj,true}, {allow_private_messages,true}, {allow_query_users,true}, {allow_user_invites,false}, {allow_visitor_nickchange,true}, {allow_visitor_status,true}, {anonymous,true}, {captcha_protected,false}, {lang,<<>>}, {logging,false}, {members_by_default,true}, {members_only,false}, {moderated,true}, {password_protected,false}, {persistent,false}, {public,true}, {public_list,true}, {mam,false}, {allow_subscription,false}, {password,<<>>}, {title,<<>>}, {allow_private_messages_from_visitors,anyone}, {max_users,200}, {presence_broadcast,[moderator,participant,visitor]}]}]. mod_doc() -> #{desc => [?T("This module provides support for https://xmpp.org/extensions/xep-0045.html" "[XEP-0045: Multi-User Chat]. Users can discover existing rooms, " "join or create them. Occupants of a room can chat in public or have private chats."), "", ?T("The MUC service allows any Jabber ID to register a nickname, so " "nobody else can use that nickname in any room in the MUC " "service. To register a nickname, open the Service Discovery in " "your XMPP client and register in the MUC service."), "", ?T("This module supports clustering and load balancing. One module " "can be started per cluster node. Rooms are distributed at " "creation time on all available MUC module instances. The " "multi-user chat module is clustered but the rooms themselves " "are not clustered nor fault-tolerant: if the node managing a " "set of rooms goes down, the rooms disappear and they will be " "recreated on an available node on first connection attempt.")], opts => [{access, #{value => ?T("AccessName"), desc => ?T("You can specify who is allowed to use the Multi-User Chat service. " "By default everyone is allowed to use it.")}}, {access_admin, #{value => ?T("AccessName"), desc => ?T("This option specifies who is allowed to administrate " "the Multi-User Chat service. The default value is 'none', " "which means that only the room creator can administer " "their room. The administrators can send a normal message " "to the service JID, and it will be shown in all active " "rooms as a service message. The administrators can send a " "groupchat message to the JID of an active room, and the " "message will be shown in the room as a service message.")}}, {access_create, #{value => ?T("AccessName"), desc => ?T("To configure who is allowed to create new rooms at the " "Multi-User Chat service, this option can be used. " "The default value is 'all', which means everyone is " "allowed to create rooms.")}}, {access_persistent, #{value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'persistent' room option. " "The default value is 'all', which means everyone is allowed to " "modify that option.")}}, {access_mam, #{value => ?T("AccessName"), desc => ?T("To configure who is allowed to modify the 'mam' room option. " "The default value is 'all', which means everyone is allowed to " "modify that option.")}}, {access_register, #{value => ?T("AccessName"), desc => ?T("This option specifies who is allowed to register nickname " "within the Multi-User Chat service. The default is 'all' for " "backward compatibility, which means that any user is allowed " "to register any free nick.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Define the type of persistent storage where the module will " "store room information. The default is the storage defined " "by the global option 'default_db', or 'mnesia' if omitted.")}}, {ram_db_type, #{value => "mnesia | sql", desc => ?T("Define the type of volatile (in-memory) storage where the module " "will store room information ('muc_online_room' and 'muc_online_users').")}}, {hibernation_timeout, #{value => "infinity | Seconds", desc => ?T("Timeout before hibernating the room process, expressed " "in seconds. The default value is 'infinity'.")}}, {history_size, #{value => ?T("Size"), desc => ?T("A small history of the current discussion is sent to users " "when they enter the room. With this option you can define the " "number of history messages to keep and send to users joining the room. " "The value is a non-negative integer. Setting the value to 0 disables " "the history feature and, as a result, nothing is kept in memory. " "The default value is 20. This value affects all rooms on the service. " "NOTE: modern XMPP clients rely on Message Archives (XEP-0313), so feel " "free to disable the history feature if you're only using modern clients " "and have 'mod_mam' module loaded.")}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"conference.\". " "The keyword '@HOST@' is replaced with the real virtual host name.")}}, {name, #{value => "string()", desc => ?T("The value of the service name. This name is only visible in some " "clients that support https://xmpp.org/extensions/xep-0030.html" "[XEP-0030: Service Discovery]. The default is 'Chatrooms'.")}}, {max_room_desc, #{value => ?T("Number"), desc => ?T("This option defines the maximum number of characters that " "Room Description can have when configuring the room. " "The default value is 'infinity'.")}}, {max_room_id, #{value => ?T("Number"), desc => ?T("This option defines the maximum number of characters that " "Room ID can have when creating a new room. " "The default value is 'infinity'.")}}, {max_room_name, #{value => ?T("Number"), desc => ?T("This option defines the maximum number of characters " "that Room Name can have when configuring the room. " "The default value is 'infinity'.")}}, {max_password, #{value => ?T("Number"), note => "added in 21.01", desc => ?T("This option defines the maximum number of characters " "that Password can have when configuring the room. " "The default value is 'infinity'.")}}, {max_captcha_whitelist, #{value => ?T("Number"), note => "added in 21.01", desc => ?T("This option defines the maximum number of characters " "that Captcha Whitelist can have when configuring the room. " "The default value is 'infinity'.")}}, {max_rooms_discoitems, #{value => ?T("Number"), desc => ?T("When there are more rooms than this 'Number', " "only the non-empty ones are returned in a Service Discovery query. " "The default value is '100'.")}}, {max_user_conferences, #{value => ?T("Number"), desc => ?T("This option defines the maximum number of rooms that any " "given user can join. The default value is '100'. This option " "is used to prevent possible abuses. Note that this is a soft " "limit: some users can sometimes join more conferences in " "cluster configurations.")}}, {max_users, #{value => ?T("Number"), desc => ?T("This option defines at the service level, the maximum " "number of users allowed per room. It can be lowered in " "each room configuration but cannot be increased in " "individual room configuration. The default value is '200'.")}}, {max_users_admin_threshold, #{value => ?T("Number"), desc => ?T("This option defines the number of service admins or room " "owners allowed to enter the room when the maximum number " "of allowed occupants was reached. The default limit is '5'.")}}, {max_users_presence, #{value => ?T("Number"), desc => ?T("This option defines after how many users in the room, " "it is considered overcrowded. When a MUC room is considered " "overcrowed, presence broadcasts are limited to reduce load, " "traffic and excessive presence \"storm\" received by participants. " "The default value is '1000'.")}}, {min_message_interval, #{value => ?T("Number"), desc => ?T("This option defines the minimum interval between two " "messages send by an occupant in seconds. This option " "is global and valid for all rooms. A decimal value can be used. " "When this option is not defined, message rate is not limited. " "This feature can be used to protect a MUC service from occupant " "abuses and limit number of messages that will be broadcasted by " "the service. A good value for this minimum message interval is 0.4 second. " "If an occupant tries to send messages faster, an error is send back " "explaining that the message has been discarded and describing the " "reason why the message is not acceptable.")}}, {min_presence_interval, #{value => ?T("Number"), desc => ?T("This option defines the minimum of time between presence " "changes coming from a given occupant in seconds. " "This option is global and valid for all rooms. A decimal " "value can be used. When this option is not defined, no " "restriction is applied. This option can be used to protect " "a MUC service for occupants abuses. If an occupant tries " "to change its presence more often than the specified interval, " "the presence is cached by ejabberd and only the last presence " "is broadcasted to all occupants in the room after expiration " "of the interval delay. Intermediate presence packets are " "silently discarded. A good value for this option is 4 seconds.")}}, {queue_type, #{value => "ram | file", desc => ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {regexp_room_id, #{value => "string()", desc => ?T("This option defines the regular expression that a Room ID " "must satisfy to allow the room creation. The default value " "is the empty string.")}}, {preload_rooms, #{value => "true | false", desc => ?T("Whether to load all persistent rooms in memory on startup. " "If disabled, the room is only loaded on first participant join. " "The default is 'true'. It makes sense to disable room preloading " "when the number of rooms is high: this will improve server startup " "time and memory consumption.")}}, {room_shaper, #{value => "none | ShaperName", desc => ?T("This option defines shaper for the MUC rooms. " "The default value is 'none'.")}}, {user_message_shaper, #{value => "none | ShaperName", desc => ?T("This option defines shaper for the users messages. " "The default value is 'none'.")}}, {user_presence_shaper, #{value => "none | ShaperName", desc => ?T("This option defines shaper for the users presences. " "The default value is 'none'.")}}, {vcard, #{value => ?T("vCard"), desc => ?T("A custom vCard of the service that will be displayed " "by some XMPP clients in Service Discovery. The value of " "'vCard' is a YAML map constructed from an XML representation " "of vCard. Since the representation has no attributes, " "the mapping is straightforward."), example => [{?T("For example, the following XML representation of vCard:"), ["", " Conferences", " ", " ", " Elm Street", " ", ""]}, {?T("will be translated to:"), ["vcard:", " fn: Conferences", " adr:", " -", " work: true", " street: Elm Street"]}]}}, {default_room_options, #{value => ?T("Options"), desc => ?T("This option allows to define the desired " "default room options. Note that the creator of a room " "can modify the options of his room at any time using an " "XMPP client with MUC capability. The 'Options' are:")}, [{allow_change_subj, #{value => "true | false", desc => ?T("Allow occupants to change the subject. " "The default value is 'true'.")}}, {allow_private_messages, #{value => "true | false", desc => ?T("Occupants can send private messages to other occupants. " "The default value is 'true'.")}}, {allow_query_users, #{value => "true | false", desc => ?T("Occupants can send IQ queries to other occupants. " "The default value is 'true'.")}}, {allow_user_invites, #{value => "true | false", desc => ?T("Allow occupants to send invitations. " "The default value is 'false'.")}}, {allow_visitor_nickchange, #{value => "true | false", desc => ?T("Allow visitors to change nickname. " "The default value is 'true'.")}}, {allow_visitor_status, #{value => "true | false", desc => ?T("Allow visitors to send status text in presence updates. " "If disallowed, the status text is stripped before broadcasting " "the presence update to all the room occupants. " "The default value is 'true'.")}}, {anonymous, #{value => "true | false", desc => ?T("The room is anonymous: occupants don't see the real " "JIDs of other occupants. Note that the room moderators " "can always see the real JIDs of the occupants. " "The default value is 'true'.")}}, {captcha_protected, #{value => "true | false", desc => ?T("When a user tries to join a room where they have no " "affiliation (not owner, admin or member), the room " "requires them to fill a CAPTCHA challenge (see section " "https://docs.ejabberd.im/admin/configuration/#captcha[CAPTCHA] " "in order to accept their join in the room. " "The default value is 'false'.")}}, {lang, #{value => ?T("Language"), desc => ?T("Preferred language for the discussions in the room. " "The language format should conform to RFC 5646. " "There is no value by default.")}}, {logging, #{value => "true | false", desc => ?T("The public messages are logged using _`mod_muc_log`_. " "The default value is 'false'.")}}, {members_by_default, #{value => "true | false", desc => ?T("The occupants that enter the room are participants " "by default, so they have \"voice\". " "The default value is 'true'.")}}, {members_only, #{value => "true | false", desc => ?T("Only members of the room can enter. " "The default value is 'false'.")}}, {moderated, #{value => "true | false", desc => ?T("Only occupants with \"voice\" can send public messages. " "The default value is 'true'.")}}, {password_protected, #{value => "true | false", desc => ?T("The password is required to enter the room. " "The default value is 'false'.")}}, {password, #{value => ?T("Password"), desc => ?T("Password of the room. Implies option 'password_protected' " "set to 'true'. There is no default value.")}}, {persistent, #{value => "true | false", desc => ?T("The room persists even if the last participant leaves. " "The default value is 'false'.")}}, {public, #{value => "true | false", desc => ?T("The room is public in the list of the MUC service, " "so it can be discovered. MUC admins and room participants " "will see private rooms in Service Discovery if their XMPP " "client supports this feature. " "The default value is 'true'.")}}, {public_list, #{value => "true | false", desc => ?T("The list of participants is public, without requiring " "to enter the room. The default value is 'true'.")}}, {mam, #{value => "true | false", desc => ?T("Enable message archiving. Implies mod_mam is enabled. " "The default value is 'false'.")}}, {allow_subscription, #{value => "true | false", desc => ?T("Allow users to subscribe to room events as described in " "https://docs.ejabberd.im/developer/xmpp-clients-bots/extensions/muc-sub/" "[Multi-User Chat Subscriptions]. " "The default value is 'false'.")}}, {title, #{value => ?T("Room Title"), desc => ?T("A human-readable title of the room. " "There is no default value")}}, {allow_private_messages_from_visitors, #{value => "anyone | moderators | nobody", desc => ?T("Visitors can send private messages to other occupants. " "The default value is 'anyone' which means visitors " "can send private messages to any occupant.")}}, {max_users, #{value => ?T("Number"), desc => ?T("Maximum number of occupants in the room. " "The default value is '200'.")}}, {presence_broadcast, #{value => "[moderator | participant | visitor, ...]", desc => ?T("List of roles for which presence is broadcasted. " "The list can contain one or several of: 'moderator', " "'participant', 'visitor'. The default value is shown " "in the example below:"), example => ["presence_broadcast:", " - moderator", " - participant", " - visitor"]}}]}]}. ejabberd-21.12/src/extauth.erl0000644000232200023220000001552214154362354016614 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 7 May 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(extauth). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). -define(CALL_TIMEOUT, timer:seconds(30)). %% API -export([start/1, stop/1, reload/1, start_link/2]). -export([check_password/3, set_password/3, try_register/3, remove_user/2, remove_user/3, user_exists/2, check_certificate/3]). -export([prog_name/1, pool_name/1, worker_name/2, pool_size/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -record(state, {port :: port(), prog :: string(), start_time :: integer(), os_pid :: integer() | undefined}). %%%=================================================================== %%% API %%%=================================================================== start(Host) -> extauth_sup:start(Host). stop(Host) -> extauth_sup:stop(Host). reload(Host) -> extauth_sup:reload(Host). start_link(Name, Prog) -> ?GEN_SERVER:start_link({local, Name}, ?MODULE, [Prog], []). check_password(User, Server, Password) -> call_port(Server, [<<"auth">>, User, Server, Password]). check_certificate(User, Server, Certificate) -> call_port(Server, [<<"certauth">>, User, Server, Certificate]). user_exists(User, Server) -> call_port(Server, [<<"isuser">>, User, Server]). set_password(User, Server, Password) -> call_port(Server, [<<"setpass">>, User, Server, Password]). try_register(User, Server, Password) -> call_port(Server, [<<"tryregister">>, User, Server, Password]). remove_user(User, Server) -> call_port(Server, [<<"removeuser">>, User, Server]). remove_user(User, Server, Password) -> call_port(Server, [<<"removeuser3">>, User, Server, Password]). -spec prog_name(binary()) -> string() | undefined. prog_name(Host) -> ejabberd_option:extauth_program(Host). -spec pool_name(binary()) -> atom(). pool_name(Host) -> case ejabberd_option:extauth_pool_name(Host) of undefined -> list_to_atom("extauth_pool_" ++ binary_to_list(Host)); Name -> list_to_atom("extauth_pool_" ++ binary_to_list(Name)) end. -spec worker_name(atom(), integer()) -> atom(). worker_name(Pool, N) -> list_to_atom(atom_to_list(Pool) ++ "_" ++ integer_to_list(N)). -spec pool_size(binary()) -> pos_integer(). pool_size(Host) -> case ejabberd_option:extauth_pool_size(Host) of undefined -> try erlang:system_info(logical_processors) catch _:_ -> 1 end; Size -> Size end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Prog]) -> process_flag(trap_exit, true), {Port, OSPid} = start_port(Prog), Time = curr_time(), {ok, #state{port = Port, start_time = Time, prog = Prog, os_pid = OSPid}}. handle_call({cmd, Cmd, EndTime}, _From, State) -> Timeout = EndTime - curr_time(), if Timeout > 0 -> Port = State#state.port, port_command(Port, Cmd), receive {Port, {data, [0, N] = Data}} when N == 0; N == 1 -> ?DEBUG("Received response from external authentication " "program: ~p", [Data]), {reply, decode_bool(N), State}; {Port, Data} -> ?ERROR_MSG("Received unexpected response from external " "authentication program '~ts': ~p " "(port = ~p, pid = ~w)", [State#state.prog, Data, Port, State#state.os_pid]), {reply, {error, unexpected_response}, State}; {'EXIT', Port, Reason} -> handle_info({'EXIT', Port, Reason}, State) after Timeout -> {stop, normal, State} end; true -> {noreply, State} end. handle_cast(_Msg, State) -> {noreply, State}. handle_info({'EXIT', Port, _Reason}, #state{port = Port, start_time = Time} = State) -> case curr_time() - Time of Diff when Diff < 1000 -> ?ERROR_MSG("Failed to start external authentication program '~ts'", [State#state.prog]), {stop, normal, State}; _ -> ?ERROR_MSG("External authentication program '~ts' has terminated " "unexpectedly (pid=~w), restarting via supervisor...", [State#state.prog, State#state.os_pid]), {stop, normal, State} end; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> catch port_close(State#state.port), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec curr_time() -> non_neg_integer(). curr_time() -> erlang:monotonic_time(millisecond). -spec start_port(string()) -> {port(), integer() | undefined}. start_port(Path) -> Port = open_port({spawn, Path}, [{packet, 2}]), link(Port), case erlang:port_info(Port, os_pid) of {os_pid, OSPid} -> {Port, OSPid}; undefined -> {Port, undefined} end. call_port(Server, Args) -> call_port(Server, Args, ?CALL_TIMEOUT). call_port(Server, Args, Timeout) -> StartTime = erlang:monotonic_time(millisecond), Pool = pool_name(Server), PoolSize = pool_size(Server), I = p1_rand:round_robin(PoolSize), Cmd = str:join(Args, <<":">>), do_call(Cmd, I, I + PoolSize, Pool, PoolSize, StartTime + Timeout, StartTime). do_call(_, Max, Max, _, _, _, _) -> {error, disconnected}; do_call(Cmd, I, Max, Pool, PoolSize, EndTime, CurrTime) -> Timeout = EndTime - CurrTime, if Timeout > 0 -> Proc = worker_name(Pool, (I rem PoolSize) + 1), try ?GEN_SERVER:call(Proc, {cmd, Cmd, EndTime}, Timeout) catch exit:{timeout, {?GEN_SERVER, call, _}} -> {error, timeout}; exit:{_, {?GEN_SERVER, call, _}} -> do_call(Cmd, I+1, Max, Pool, PoolSize, EndTime, curr_time()) end; true -> {error, timeout} end. decode_bool(0) -> false; decode_bool(1) -> true. ejabberd-21.12/src/mod_multicast_opt.erl0000644000232200023220000000315314154362354020655 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_multicast_opt). -export([access/1]). -export([host/1]). -export([hosts/1]). -export([limits/1]). -export([name/1]). -export([vcard/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_multicast, access). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_multicast, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_multicast, hosts). -spec limits(gen_mod:opts() | global | binary()) -> [{'local',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]} | {'remote',[{'message','infinite' | non_neg_integer()} | {'presence','infinite' | non_neg_integer()}]}]. limits(Opts) when is_map(Opts) -> gen_mod:get_opt(limits, Opts); limits(Host) -> gen_mod:get_module_opt(Host, mod_multicast, limits). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_multicast, name). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_multicast, vcard). ejabberd-21.12/src/mod_privacy.erl0000644000232200023220000007473214154362354017456 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_privacy.erl %%% Author : Alexey Shchepin %%% Purpose : jabber:iq:privacy support %%% Created : 21 Jul 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_privacy). -author('alexey@process-one.net'). -protocol({xep, 16, '1.6'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_iq/1, export/1, c2s_copy_session/2, push_list_update/2, disco_features/5, check_packet/4, remove_user/2, encode_list_item/1, get_user_lists/2, get_user_list/3, set_list/1, set_list/4, set_default_list/3, user_send_packet/1, mod_doc/0, import_start/2, import_stop/2, import/5, import_info/0, mod_opt_type/1, mod_options/1, depends/2]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_privacy.hrl"). -include("translate.hrl"). -define(PRIVACY_CACHE, privacy_cache). -define(PRIVACY_LIST_CACHE, privacy_list_cache). -type c2s_state() :: ejabberd_c2s:state(). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(#privacy{}) -> ok. -callback set_default(binary(), binary(), binary()) -> ok | {error, notfound | any()}. -callback unset_default(binary(), binary()) -> ok | {error, any()}. -callback remove_list(binary(), binary(), binary()) -> ok | {error, notfound | conflict | any()}. -callback remove_lists(binary(), binary()) -> ok | {error, any()}. -callback set_lists(#privacy{}) -> ok | {error, any()}. -callback set_list(binary(), binary(), binary(), [listitem()]) -> ok | {error, any()}. -callback get_list(binary(), binary(), binary() | default) -> {ok, {binary(), [listitem()]}} | error | {error, any()}. -callback get_lists(binary(), binary()) -> {ok, #privacy{}} | error | {error, any()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 50), ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, check_packet, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PRIVACY, ?MODULE, process_iq). stop(Host) -> ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 50), ejabberd_hooks:delete(privacy_check_packet, Host, ?MODULE, check_packet, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PRIVACY). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]}. disco_features({error, Err}, _From, _To, _Node, _Lang) -> {error, Err}; disco_features(empty, _From, _To, <<"">>, _Lang) -> {result, [?NS_PRIVACY]}; disco_features({result, Feats}, _From, _To, <<"">>, _Lang) -> {result, [?NS_PRIVACY|Feats]}; disco_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec process_iq(iq()) -> iq(). process_iq(#iq{type = Type, from = #jid{luser = U, lserver = S}, to = #jid{luser = U, lserver = S}} = IQ) -> case Type of get -> process_iq_get(IQ); set -> process_iq_set(IQ) end; process_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)). -spec process_iq_get(iq()) -> iq(). process_iq_get(#iq{lang = Lang, sub_els = [#privacy_query{default = Default, active = Active}]} = IQ) when Default /= undefined; Active /= undefined -> Txt = ?T("Only element is allowed in this query"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); process_iq_get(#iq{lang = Lang, sub_els = [#privacy_query{lists = Lists}]} = IQ) -> case Lists of [] -> process_lists_get(IQ); [#privacy_list{name = ListName}] -> process_list_get(IQ, ListName); _ -> Txt = ?T("Too many elements"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_iq_get(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_lists_get(iq()) -> iq(). process_lists_get(#iq{from = #jid{luser = LUser, lserver = LServer}, lang = Lang} = IQ) -> case get_user_lists(LUser, LServer) of {ok, #privacy{default = Default, lists = Lists}} -> Active = xmpp:get_meta(IQ, privacy_active_list, none), xmpp:make_iq_result( IQ, #privacy_query{active = Active, default = Default, lists = [#privacy_list{name = Name} || {Name, _} <- Lists]}); error -> xmpp:make_iq_result( IQ, #privacy_query{active = none, default = none}); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec process_list_get(iq(), binary()) -> iq(). process_list_get(#iq{from = #jid{luser = LUser, lserver = LServer}, lang = Lang} = IQ, Name) -> case get_user_list(LUser, LServer, Name) of {ok, {_, List}} -> Items = lists:map(fun encode_list_item/1, List), xmpp:make_iq_result( IQ, #privacy_query{ lists = [#privacy_list{name = Name, items = Items}]}); error -> Txt = ?T("No privacy list with this name found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec encode_list_item(listitem()) -> privacy_item(). encode_list_item(#listitem{action = Action, order = Order, type = Type, match_all = MatchAll, match_iq = MatchIQ, match_message = MatchMessage, match_presence_in = MatchPresenceIn, match_presence_out = MatchPresenceOut, value = Value}) -> Item = #privacy_item{action = Action, order = Order, type = case Type of none -> undefined; Type -> Type end, value = encode_value(Type, Value)}, case MatchAll of true -> Item; false -> Item#privacy_item{message = MatchMessage, iq = MatchIQ, presence_in = MatchPresenceIn, presence_out = MatchPresenceOut} end. -spec encode_value(listitem_type(), listitem_value()) -> binary(). encode_value(Type, Val) -> case Type of jid -> jid:encode(Val); group -> Val; subscription -> case Val of both -> <<"both">>; to -> <<"to">>; from -> <<"from">>; none -> <<"none">> end; none -> <<"">> end. -spec decode_value(jid | subscription | group | undefined, binary()) -> listitem_value(). decode_value(Type, Value) -> case Type of jid -> jid:tolower(jid:decode(Value)); subscription -> case Value of <<"from">> -> from; <<"to">> -> to; <<"both">> -> both; <<"none">> -> none end; group when Value /= <<"">> -> Value; undefined -> none end. -spec process_iq_set(iq()) -> iq(). process_iq_set(#iq{lang = Lang, sub_els = [#privacy_query{default = Default, active = Active, lists = Lists}]} = IQ) -> case Lists of [#privacy_list{items = Items, name = ListName}] when Default == undefined, Active == undefined -> process_lists_set(IQ, ListName, Items); [] when Default == undefined, Active /= undefined -> process_active_set(IQ, Active); [] when Active == undefined, Default /= undefined -> process_default_set(IQ, Default); _ -> Txt = ?T("The stanza MUST contain only one element, " "one element, or one element"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_iq_set(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec process_default_set(iq(), none | binary()) -> iq(). process_default_set(#iq{from = #jid{luser = LUser, lserver = LServer}, lang = Lang} = IQ, Value) -> case set_default_list(LUser, LServer, Value) of ok -> xmpp:make_iq_result(IQ); {error, notfound} -> Txt = ?T("No privacy list with this name found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec process_active_set(IQ, none | binary()) -> IQ. process_active_set(IQ, none) -> xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, none)); process_active_set(#iq{from = #jid{luser = LUser, lserver = LServer}, lang = Lang} = IQ, Name) -> case get_user_list(LUser, LServer, Name) of {ok, _} -> xmpp:make_iq_result(xmpp:put_meta(IQ, privacy_active_list, Name)); error -> Txt = ?T("No privacy list with this name found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec set_list(privacy()) -> ok | {error, any()}. set_list(#privacy{us = {LUser, LServer}, lists = Lists} = Privacy) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_lists(Privacy) of ok -> Names = [Name || {Name, _} <- Lists], delete_cache(Mod, LUser, LServer, Names); {error, _} = Err -> Err end. -spec process_lists_set(iq(), binary(), [privacy_item()]) -> iq(). process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer}, lang = Lang} = IQ, Name, []) -> case xmpp:get_meta(IQ, privacy_active_list, none) of Name -> Txt = ?T("Cannot remove active list"), xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); _ -> case remove_list(LUser, LServer, Name) of ok -> xmpp:make_iq_result(IQ); {error, conflict} -> Txt = ?T("Cannot remove default list"), xmpp:make_error(IQ, xmpp:err_conflict(Txt, Lang)); {error, notfound} -> Txt = ?T("No privacy list with this name found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)); {error, _} -> Txt = ?T("Database failure"), Err = xmpp:err_internal_server_error(Txt, Lang), xmpp:make_error(IQ, Err) end end; process_lists_set(#iq{from = #jid{luser = LUser, lserver = LServer} = From, lang = Lang} = IQ, Name, Items) -> case catch lists:map(fun decode_item/1, Items) of {error, Why} -> Txt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); List -> case set_list(LUser, LServer, Name, List) of ok -> push_list_update(From, Name), xmpp:make_iq_result(IQ); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end end. -spec push_list_update(jid(), binary()) -> ok. push_list_update(From, Name) -> BareFrom = jid:remove_resource(From), lists:foreach( fun(R) -> To = jid:replace_resource(From, R), IQ = #iq{type = set, from = BareFrom, to = To, id = <<"push", (p1_rand:get_string())/binary>>, sub_els = [#privacy_query{ lists = [#privacy_list{name = Name}]}]}, ejabberd_router:route(IQ) end, ejabberd_sm:get_user_resources(From#jid.luser, From#jid.lserver)). -spec decode_item(privacy_item()) -> listitem(). decode_item(#privacy_item{order = Order, action = Action, type = T, value = V, message = MatchMessage, iq = MatchIQ, presence_in = MatchPresenceIn, presence_out = MatchPresenceOut}) -> Value = try decode_value(T, V) catch _:_ -> throw({error, {bad_attr_value, <<"value">>, <<"item">>, ?NS_PRIVACY}}) end, Type = case T of undefined -> none; _ -> T end, ListItem = #listitem{order = Order, action = Action, type = Type, value = Value}, if not (MatchMessage or MatchIQ or MatchPresenceIn or MatchPresenceOut) -> ListItem#listitem{match_all = true}; true -> ListItem#listitem{match_iq = MatchIQ, match_message = MatchMessage, match_presence_in = MatchPresenceIn, match_presence_out = MatchPresenceOut} end. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{privacy_active_list := List}) -> State#{privacy_active_list => List}; c2s_copy_session(State, _) -> State. %% Adjust the client's state, so next packets (which can be already queued) %% will take the active list into account. -spec update_c2s_state_with_privacy_list(stanza(), c2s_state()) -> c2s_state(). update_c2s_state_with_privacy_list(#iq{type = set, to = #jid{luser = U, lserver = S, lresource = <<"">>} = To} = IQ, State) -> %% Match a IQ set containing a new active privacy list case xmpp:get_subtag(IQ, #privacy_query{}) of #privacy_query{default = undefined, active = Active} -> case Active of none -> ?DEBUG("Removing active privacy list for user: ~ts", [jid:encode(To)]), State#{privacy_active_list => none}; undefined -> State; _ -> case get_user_list(U, S, Active) of {ok, _} -> ?DEBUG("Setting active privacy list '~ts' for user: ~ts", [Active, jid:encode(To)]), State#{privacy_active_list => Active}; _ -> %% unknown privacy list name State end end; _ -> State end; update_c2s_state_with_privacy_list(_Packet, State) -> State. %% Add the active privacy list to packet metadata -spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_send_packet({#iq{type = Type, to = #jid{luser = U, lserver = S, lresource = <<"">>}, from = #jid{luser = U, lserver = S}, sub_els = [_]} = IQ, #{privacy_active_list := Name} = State}) when Type == get; Type == set -> NewIQ = case xmpp:has_subtag(IQ, #privacy_query{}) of true -> xmpp:put_meta(IQ, privacy_active_list, Name); false -> IQ end, {NewIQ, update_c2s_state_with_privacy_list(IQ, State)}; %% For client with no active privacy list, see if there is %% one about to be activated in this packet and update client state user_send_packet({Packet, State}) -> {Packet, update_c2s_state_with_privacy_list(Packet, State)}. -spec set_list(binary(), binary(), binary(), [listitem()]) -> ok | {error, any()}. set_list(LUser, LServer, Name, List) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:set_list(LUser, LServer, Name, List) of ok -> delete_cache(Mod, LUser, LServer, [Name]); {error, _} = Err -> Err end. -spec remove_list(binary(), binary(), binary()) -> ok | {error, conflict | notfound | any()}. remove_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:remove_list(LUser, LServer, Name) of ok -> delete_cache(Mod, LUser, LServer, [Name]); Err -> Err end. -spec get_user_lists(binary(), binary()) -> {ok, privacy()} | error | {error, any()}. get_user_lists(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PRIVACY_CACHE, {LUser, LServer}, fun() -> Mod:get_lists(LUser, LServer) end); false -> Mod:get_lists(LUser, LServer) end. -spec get_user_list(binary(), binary(), binary() | default) -> {ok, {binary(), [listitem()]}} | error | {error, any()}. get_user_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PRIVACY_LIST_CACHE, {LUser, LServer, Name}, fun() -> case ets_cache:lookup( ?PRIVACY_CACHE, {LUser, LServer}) of {ok, Privacy} -> get_list_by_name(Privacy, Name); error -> Mod:get_list(LUser, LServer, Name) end end); false -> Mod:get_list(LUser, LServer, Name) end. -spec get_list_by_name(#privacy{}, binary() | default) -> {ok, {binary(), [listitem()]}} | error. get_list_by_name(#privacy{default = Default} = Privacy, default) -> get_list_by_name(Privacy, Default); get_list_by_name(#privacy{lists = Lists}, Name) -> case lists:keyfind(Name, 1, Lists) of {_, List} -> {ok, {Name, List}}; false -> error end. -spec set_default_list(binary(), binary(), binary() | none) -> ok | {error, notfound | any()}. set_default_list(LUser, LServer, Name) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case Name of none -> Mod:unset_default(LUser, LServer); _ -> Mod:set_default(LUser, LServer, Name) end, case Res of ok -> delete_cache(Mod, LUser, LServer, []); Err -> Err end. -spec check_packet(allow | deny, c2s_state() | jid(), stanza(), in | out) -> allow | deny. check_packet(Acc, #{jid := JID} = State, Packet, Dir) -> case maps:get(privacy_active_list, State, none) of none -> check_packet(Acc, JID, Packet, Dir); ListName -> #jid{luser = LUser, lserver = LServer} = JID, case get_user_list(LUser, LServer, ListName) of {ok, {_, List}} -> do_check_packet(JID, List, Packet, Dir); _ -> ?DEBUG("Non-existing active list '~ts' is set " "for user '~ts'", [ListName, jid:encode(JID)]), check_packet(Acc, JID, Packet, Dir) end end; check_packet(_, JID, Packet, Dir) -> #jid{luser = LUser, lserver = LServer} = JID, case get_user_list(LUser, LServer, default) of {ok, {_, List}} -> do_check_packet(JID, List, Packet, Dir); _ -> allow end. %% From is the sender, To is the destination. %% If Dir = out, User@Server is the sender account (From). %% If Dir = in, User@Server is the destination account (To). -spec do_check_packet(jid(), [listitem()], stanza(), in | out) -> allow | deny. do_check_packet(_, [], _, _) -> allow; do_check_packet(#jid{luser = LUser, lserver = LServer}, List, Packet, Dir) -> From = xmpp:get_from(Packet), To = xmpp:get_to(Packet), case {From, To} of {#jid{luser = <<"">>, lserver = LServer}, #jid{lserver = LServer}} when Dir == in -> %% Allow any packets from local server allow; {#jid{lserver = LServer}, #jid{luser = <<"">>, lserver = LServer}} when Dir == out -> %% Allow any packets to local server allow; {#jid{luser = LUser, lserver = LServer, lresource = <<"">>}, #jid{luser = LUser, lserver = LServer}} when Dir == in -> %% Allow incoming packets from user's bare jid to his full jid allow; {#jid{luser = LUser, lserver = LServer}, #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} when Dir == out -> %% Allow outgoing packets from user's full jid to his bare JID allow; _ -> PType = case Packet of #message{} -> message; #iq{} -> iq; #presence{type = available} -> presence; #presence{type = unavailable} -> presence; _ -> other end, PType2 = case {PType, Dir} of {message, in} -> message; {iq, in} -> iq; {presence, in} -> presence_in; {presence, out} -> presence_out; {_, _} -> other end, LJID = case Dir of in -> jid:tolower(From); out -> jid:tolower(To) end, check_packet_aux(List, PType2, LJID, [LUser, LServer]) end. -spec check_packet_aux([listitem()], message | iq | presence_in | presence_out | other, ljid(), [binary()] | {none | both | from | to, [binary()]}) -> allow | deny. %% Ptype = message | iq | presence_in | presence_out | other check_packet_aux([], _PType, _JID, _RosterInfo) -> allow; check_packet_aux([Item | List], PType, JID, RosterInfo) -> #listitem{type = Type, value = Value, action = Action} = Item, case is_ptype_match(Item, PType) of true -> case is_type_match(Type, Value, JID, RosterInfo) of {true, _} -> Action; {false, RI} -> check_packet_aux(List, PType, JID, RI) end; false -> check_packet_aux(List, PType, JID, RosterInfo) end. -spec is_ptype_match(listitem(), message | iq | presence_in | presence_out | other) -> boolean(). is_ptype_match(Item, PType) -> case Item#listitem.match_all of true -> true; false -> case PType of message -> Item#listitem.match_message; iq -> Item#listitem.match_iq; presence_in -> Item#listitem.match_presence_in; presence_out -> Item#listitem.match_presence_out; other -> false end end. -spec is_type_match(none | jid | subscription | group, listitem_value(), ljid(), [binary()] | {none | both | from | to, [binary()]}) -> {boolean(), [binary()] | {none | both | from | to, [binary()]}}. is_type_match(none, _Value, _JID, RosterInfo) -> {true, RosterInfo}; is_type_match(jid, Value, JID, RosterInfo) -> case Value of {<<"">>, Server, <<"">>} -> case JID of {_, Server, _} -> {true, RosterInfo}; _ -> {false, RosterInfo} end; {User, Server, <<"">>} -> case JID of {User, Server, _} -> {true, RosterInfo}; _ -> {false, RosterInfo} end; {<<"">>, Server, Resource} -> case JID of {_, Server, Resource} -> {true, RosterInfo}; _ -> {false, RosterInfo} end; _ -> {Value == JID, RosterInfo} end; is_type_match(subscription, Value, JID, RosterInfo) -> {Subscription, _} = RI = resolve_roster_info(JID, RosterInfo), {Value == Subscription, RI}; is_type_match(group, Group, JID, RosterInfo) -> {_, Groups} = RI = resolve_roster_info(JID, RosterInfo), {lists:member(Group, Groups), RI}. -spec resolve_roster_info(ljid(), [binary()] | {none | both | from | to, [binary()]}) -> {none | both | from | to, [binary()]}. resolve_roster_info(JID, [LUser, LServer]) -> {Subscription, _Ask, Groups} = ejabberd_hooks:run_fold( roster_get_jid_info, LServer, {none, none, []}, [LUser, LServer, JID]), {Subscription, Groups}; resolve_roster_info(_, RosterInfo) -> RosterInfo. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Privacy = get_user_lists(LUser, LServer), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_lists(LUser, LServer), case Privacy of {ok, #privacy{lists = Lists}} -> Names = [Name || {Name, _} <- Lists], delete_cache(Mod, LUser, LServer, Names); _ -> ok end. -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?PRIVACY_CACHE, CacheOpts), ets_cache:new(?PRIVACY_LIST_CACHE, CacheOpts); false -> ets_cache:delete(?PRIVACY_CACHE), ets_cache:delete(?PRIVACY_LIST_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_privacy_opt:cache_size(Opts), CacheMissed = mod_privacy_opt:cache_missed(Opts), LifeTime = mod_privacy_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_privacy_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec delete_cache(module(), binary(), binary(), [binary()]) -> ok. delete_cache(Mod, LUser, LServer, Names) -> case use_cache(Mod, LServer) of true -> Nodes = cache_nodes(Mod, LServer), ets_cache:delete(?PRIVACY_CACHE, {LUser, LServer}, Nodes), lists:foreach( fun(Name) -> ets_cache:delete( ?PRIVACY_LIST_CACHE, {LUser, LServer, Name}, Nodes) end, [default|Names]); false -> ok end. numeric_to_binary(<<0, 0, _/binary>>) -> <<"0">>; numeric_to_binary(<<0, _, _:6/binary, T/binary>>) -> Res = lists:foldl( fun(X, Sum) -> Sum*10000 + X end, 0, [X || <> <= T]), integer_to_binary(Res). bool_to_binary(<<0>>) -> <<"0">>; bool_to_binary(<<1>>) -> <<"1">>. prepare_list_data(mysql, [ID|Row]) -> [binary_to_integer(ID)|Row]; prepare_list_data(pgsql, [<>, SType, SValue, SAction, SOrder, SMatchAll, SMatchIQ, SMatchMessage, SMatchPresenceIn, SMatchPresenceOut]) -> [ID, SType, SValue, SAction, numeric_to_binary(SOrder), bool_to_binary(SMatchAll), bool_to_binary(SMatchIQ), bool_to_binary(SMatchMessage), bool_to_binary(SMatchPresenceIn), bool_to_binary(SMatchPresenceOut)]. prepare_id(mysql, ID) -> binary_to_integer(ID); prepare_id(pgsql, <>) -> ID. import_info() -> [{<<"privacy_default_list">>, 2}, {<<"privacy_list_data">>, 10}, {<<"privacy_list">>, 4}]. import_start(LServer, DBType) -> ets:new(privacy_default_list_tmp, [private, named_table]), ets:new(privacy_list_data_tmp, [private, named_table, bag]), ets:new(privacy_list_tmp, [private, named_table, bag, {keypos, #privacy.us}]), Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []). import(LServer, {sql, _}, _DBType, <<"privacy_default_list">>, [LUser, Name]) -> US = {LUser, LServer}, ets:insert(privacy_default_list_tmp, {US, Name}), ok; import(LServer, {sql, SQLType}, _DBType, <<"privacy_list_data">>, Row1) -> [ID|Row] = prepare_list_data(SQLType, Row1), case mod_privacy_sql:raw_to_item(Row) of [Item] -> IS = {ID, LServer}, ets:insert(privacy_list_data_tmp, {IS, Item}), ok; [] -> ok end; import(LServer, {sql, SQLType}, _DBType, <<"privacy_list">>, [LUser, Name, ID, _TimeStamp]) -> US = {LUser, LServer}, IS = {prepare_id(SQLType, ID), LServer}, Default = case ets:lookup(privacy_default_list_tmp, US) of [{_, Name}] -> Name; _ -> none end, case [Item || {_, Item} <- ets:lookup(privacy_list_data_tmp, IS)] of [_|_] = Items -> Privacy = #privacy{us = {LUser, LServer}, default = Default, lists = [{Name, Items}]}, ets:insert(privacy_list_tmp, Privacy), ets:delete(privacy_list_data_tmp, IS), ok; _ -> ok end. import_stop(_LServer, DBType) -> import_next(DBType, ets:first(privacy_list_tmp)), ets:delete(privacy_default_list_tmp), ets:delete(privacy_list_data_tmp), ets:delete(privacy_list_tmp), ok. import_next(_DBType, '$end_of_table') -> ok; import_next(DBType, US) -> [P|_] = Ps = ets:lookup(privacy_list_tmp, US), Lists = lists:flatmap( fun(#privacy{lists = Lists}) -> Lists end, Ps), Privacy = P#privacy{lists = Lists}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(Privacy), import_next(DBType, ets:next(privacy_list_tmp, US)). export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). depends(_Host, _Opts) -> []. mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0016.html" "[XEP-0016: Privacy Lists]."), "", ?T("NOTE: Nowadays modern XMPP clients rely on " "https://xmpp.org/extensions/xep-0191.html" "[XEP-0191: Blocking Command] which is implemented by " "'mod_blocking' module. However, you still need " "'mod_privacy' loaded in order for _`mod_blocking`_ to work.")], opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. ejabberd-21.12/src/mod_sip_registrar.erl0000644000232200023220000004112014154362354020637 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_sip_registrar.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 23 Apr 2014 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2014-2021 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(mod_sip_registrar). -ifndef(SIP). -export([]). -else. -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). %% API -export([start_link/0, request/2, find_sockets/2, ping/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -include_lib("esip/include/esip.hrl"). -define(CALL_TIMEOUT, timer:seconds(30)). -define(DEFAULT_EXPIRES, 3600). -record(sip_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, socket = #sip_socket{} :: #sip_socket{}, call_id = <<"">> :: binary(), cseq = 0 :: non_neg_integer(), timestamp = erlang:timestamp() :: erlang:timestamp(), contact :: {binary(), #uri{}, [{binary(), binary()}]}, flow_tref :: reference() | undefined, reg_tref = make_ref() :: reference(), conn_mref = make_ref() :: reference(), expires = 0 :: non_neg_integer()}). -record(state, {}). %%%=================================================================== %%% API %%%=================================================================== start_link() -> ?GEN_SERVER:start_link({local, ?MODULE}, ?MODULE, [], []). request(#sip{hdrs = Hdrs} = Req, SIPSock) -> {_, #uri{user = U, host = S}, _} = esip:get_hdr('to', Hdrs), LUser = jid:nodeprep(U), LServer = jid:nameprep(S), {PeerIP, _} = SIPSock#sip_socket.peer, US = {LUser, LServer}, CallID = esip:get_hdr('call-id', Hdrs), CSeq = esip:get_hdr('cseq', Hdrs), Expires = esip:get_hdr('expires', Hdrs, ?DEFAULT_EXPIRES), Supported = esip:get_hdrs('supported', Hdrs), IsOutboundSupported = lists:member(<<"outbound">>, Supported), case esip:get_hdrs('contact', Hdrs) of [<<"*">>] when Expires == 0 -> case unregister_session(US, CallID, CSeq) of {ok, ContactsWithExpires} -> ?INFO_MSG("Unregister SIP session for user ~ts@~ts from ~ts", [LUser, LServer, inet_parse:ntoa(PeerIP)]), Cs = prepare_contacts_to_send(ContactsWithExpires), mod_sip:make_response( Req, #sip{type = response, status = 200, hdrs = [{'contact', Cs}]}); {error, Why} -> {Status, Reason} = make_status(Why), mod_sip:make_response( Req, #sip{type = response, status = Status, reason = Reason}) end; [{_, _URI, _Params}|_] = Contacts -> ContactsWithExpires = make_contacts_with_expires(Contacts, Expires), ContactsHaveManyRegID = contacts_have_many_reg_id(Contacts), Expires1 = lists:max([E || {_, E} <- ContactsWithExpires]), MinExpires = min_expires(), if Expires1 > 0, Expires1 < MinExpires -> mod_sip:make_response( Req, #sip{type = response, status = 423, hdrs = [{'min-expires', MinExpires}]}); ContactsHaveManyRegID -> mod_sip:make_response( Req, #sip{type = response, status = 400, reason = <<"Multiple 'reg-id' parameter">>}); true -> case register_session(US, SIPSock, CallID, CSeq, IsOutboundSupported, ContactsWithExpires) of {ok, Res} -> ?INFO_MSG("~ts SIP session for user ~ts@~ts from ~ts", [Res, LUser, LServer, inet_parse:ntoa(PeerIP)]), Cs = prepare_contacts_to_send(ContactsWithExpires), Require = case need_ob_hdrs( Contacts, IsOutboundSupported) of true -> [{'require', [<<"outbound">>]}, {'flow-timer', get_flow_timeout(LServer, SIPSock)}]; false -> [] end, mod_sip:make_response( Req, #sip{type = response, status = 200, hdrs = [{'contact', Cs}|Require]}); {error, Why} -> {Status, Reason} = make_status(Why), mod_sip:make_response( Req, #sip{type = response, status = Status, reason = Reason}) end end; [] -> case mnesia:dirty_read(sip_session, US) of [_|_] = Sessions -> ContactsWithExpires = lists:map( fun(#sip_session{contact = Contact, expires = Es}) -> {Contact, Es} end, Sessions), Cs = prepare_contacts_to_send(ContactsWithExpires), mod_sip:make_response( Req, #sip{type = response, status = 200, hdrs = [{'contact', Cs}]}); [] -> {Status, Reason} = make_status(notfound), mod_sip:make_response( Req, #sip{type = response, status = Status, reason = Reason}) end; _ -> mod_sip:make_response(Req, #sip{type = response, status = 400}) end. find_sockets(U, S) -> case mnesia:dirty_read(sip_session, {U, S}) of [_|_] = Sessions -> lists:map( fun(#sip_session{contact = {_, URI, _}, socket = Socket}) -> {Socket, URI} end, Sessions); [] -> [] end. ping(SIPSocket) -> call({ping, SIPSocket}). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> process_flag(trap_exit, true), update_table(), ejabberd_mnesia:create(?MODULE, sip_session, [{ram_copies, [node()]}, {type, bag}, {attributes, record_info(fields, sip_session)}, {index, [conn_mref,socket]}]), {ok, #state{}}. handle_call({write, Sessions, Supported}, _From, State) -> Res = write_session(Sessions, Supported), {reply, Res, State}; handle_call({delete, US, CallID, CSeq}, _From, State) -> Res = delete_session(US, CallID, CSeq), {reply, Res, State}; handle_call({ping, SIPSocket}, _From, State) -> Res = process_ping(SIPSocket), {reply, Res, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({write, Sessions, Supported}, State) -> write_session(Sessions, Supported), {noreply, State}; handle_info({delete, US, CallID, CSeq}, State) -> delete_session(US, CallID, CSeq), {noreply, State}; handle_info({timeout, TRef, US}, State) -> delete_expired_session(US, TRef), {noreply, State}; handle_info({'DOWN', MRef, process, _Pid, _Reason}, State) -> case mnesia:dirty_index_read(sip_session, MRef, #sip_session.conn_mref) of [Session] -> mnesia:dirty_delete_object(Session); _ -> ok end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== register_session(US, SIPSocket, CallID, CSeq, IsOutboundSupported, ContactsWithExpires) -> Sessions = lists:map( fun({Contact, Expires}) -> #sip_session{us = US, socket = SIPSocket, call_id = CallID, cseq = CSeq, timestamp = erlang:timestamp(), contact = Contact, expires = Expires} end, ContactsWithExpires), Msg = {write, Sessions, IsOutboundSupported}, call(Msg). unregister_session(US, CallID, CSeq) -> Msg = {delete, US, CallID, CSeq}, call(Msg). write_session([#sip_session{us = {U, S} = US}|_] = NewSessions, IsOutboundSupported) -> PrevSessions = mnesia:dirty_read(sip_session, US), Res = lists:foldl( fun(_, {error, _} = Err) -> Err; (#sip_session{call_id = CallID, expires = Expires, cseq = CSeq} = Session, {Add, Del}) -> case find_session(Session, PrevSessions, IsOutboundSupported) of {ok, normal, #sip_session{call_id = CallID, cseq = PrevCSeq}} when PrevCSeq > CSeq -> {error, cseq_out_of_order}; {ok, _Type, PrevSession} when Expires == 0 -> {Add, [PrevSession|Del]}; {ok, _Type, PrevSession} -> {[Session|Add], [PrevSession|Del]}; {error, notfound} when Expires == 0 -> {error, notfound}; {error, notfound} -> {[Session|Add], Del} end end, {[], []}, NewSessions), MaxSessions = ejabberd_sm:get_max_user_sessions(U, S), case Res of {error, Why} -> {error, Why}; {AddSessions, DelSessions} -> MaxSessions = ejabberd_sm:get_max_user_sessions(U, S), AllSessions = AddSessions ++ PrevSessions -- DelSessions, if length(AllSessions) > MaxSessions -> {error, too_many_sessions}; true -> lists:foreach(fun delete_session/1, DelSessions), lists:foreach( fun(Session) -> NewSession = set_monitor_and_timer( Session, IsOutboundSupported), mnesia:dirty_write(NewSession) end, AddSessions), case {AllSessions, AddSessions} of {[], _} -> {ok, unregister}; {_, []} -> {ok, unregister}; _ -> {ok, register} end end end. delete_session(US, CallID, CSeq) -> case mnesia:dirty_read(sip_session, US) of [_|_] = Sessions -> case lists:all( fun(S) when S#sip_session.call_id == CallID, S#sip_session.cseq > CSeq -> false; (_) -> true end, Sessions) of true -> ContactsWithExpires = lists:map( fun(#sip_session{contact = Contact} = Session) -> delete_session(Session), {Contact, 0} end, Sessions), {ok, ContactsWithExpires}; false -> {error, cseq_out_of_order} end; [] -> {error, notfound} end. delete_expired_session(US, TRef) -> case mnesia:dirty_read(sip_session, US) of [_|_] = Sessions -> lists:foreach( fun(#sip_session{reg_tref = T1, flow_tref = T2} = Session) when T1 == TRef; T2 == TRef -> if T2 /= undefined -> close_socket(Session); true -> ok end, delete_session(Session); (_) -> ok end, Sessions); [] -> ok end. min_expires() -> 60. to_integer(Bin, Min, Max) -> case catch (binary_to_integer(Bin)) of N when N >= Min, N =< Max -> {ok, N}; _ -> error end. call(Msg) -> case catch ?GEN_SERVER:call(?MODULE, Msg, ?CALL_TIMEOUT) of {'EXIT', {timeout, _}} -> {error, timeout}; {'EXIT', Why} -> {error, Why}; Reply -> Reply end. make_contacts_with_expires(Contacts, Expires) -> lists:map( fun({Name, URI, Params}) -> E1 = case to_integer(esip:get_param(<<"expires">>, Params), 0, (1 bsl 32)-1) of {ok, E} -> E; _ -> Expires end, Params1 = lists:keydelete(<<"expires">>, 1, Params), {{Name, URI, Params1}, E1} end, Contacts). prepare_contacts_to_send(ContactsWithExpires) -> lists:map( fun({{Name, URI, Params}, Expires}) -> Params1 = esip:set_param(<<"expires">>, list_to_binary( integer_to_list(Expires)), Params), {Name, URI, Params1} end, ContactsWithExpires). contacts_have_many_reg_id(Contacts) -> Sum = lists:foldl( fun({_Name, _URI, Params}, Acc) -> case get_ob_params(Params) of error -> Acc; {_, _} -> Acc + 1 end end, 0, Contacts), if Sum > 1 -> true; true -> false end. find_session(#sip_session{contact = {_, URI, Params}}, Sessions, IsOutboundSupported) -> if IsOutboundSupported -> case get_ob_params(Params) of {InstanceID, RegID} -> find_session_by_ob({InstanceID, RegID}, Sessions); error -> find_session_by_uri(URI, Sessions) end; true -> find_session_by_uri(URI, Sessions) end. find_session_by_ob({InstanceID, RegID}, [#sip_session{contact = {_, _, Params}} = Session|Sessions]) -> case get_ob_params(Params) of {InstanceID, RegID} -> {ok, flow, Session}; _ -> find_session_by_ob({InstanceID, RegID}, Sessions) end; find_session_by_ob(_, []) -> {error, notfound}. find_session_by_uri(URI1, [#sip_session{contact = {_, URI2, _}} = Session|Sessions]) -> case cmp_uri(URI1, URI2) of true -> {ok, normal, Session}; false -> find_session_by_uri(URI1, Sessions) end; find_session_by_uri(_, []) -> {error, notfound}. %% TODO: this is *totally* wrong. %% Rewrite this using URI comparison rules cmp_uri(#uri{user = U, host = H, port = P}, #uri{user = U, host = H, port = P}) -> true; cmp_uri(_, _) -> false. make_status(notfound) -> {404, esip:reason(404)}; make_status(cseq_out_of_order) -> {500, <<"CSeq is Out of Order">>}; make_status(timeout) -> {408, esip:reason(408)}; make_status(too_many_sessions) -> {503, <<"Too Many Registered Sessions">>}; make_status(_) -> {500, esip:reason(500)}. get_ob_params(Params) -> case esip:get_param(<<"+sip.instance">>, Params) of <<>> -> error; InstanceID -> case to_integer(esip:get_param(<<"reg-id">>, Params), 0, (1 bsl 32)-1) of {ok, RegID} -> {InstanceID, RegID}; error -> error end end. need_ob_hdrs(_Contacts, _IsOutboundSupported = false) -> false; need_ob_hdrs(Contacts, _IsOutboundSupported = true) -> lists:any( fun({_Name, _URI, Params}) -> case get_ob_params(Params) of error -> false; {_, _} -> true end end, Contacts). get_flow_timeout(LServer, #sip_socket{type = Type}) -> case Type of udp -> mod_sip_opt:flow_timeout_udp(LServer) div 1000; _ -> mod_sip_opt:flow_timeout_tcp(LServer) div 1000 end. update_table() -> Fields = record_info(fields, sip_session), case catch mnesia:table_info(sip_session, attributes) of Fields -> ok; [_|_] -> mnesia:delete_table(sip_session); {'EXIT', _} -> ok end. set_monitor_and_timer(#sip_session{socket = #sip_socket{type = Type, pid = Pid} = SIPSock, conn_mref = MRef, expires = Expires, us = {_, LServer}, contact = {_, _, Params}} = Session, IsOutboundSupported) -> RegTRef = set_timer(Session, Expires), Session1 = Session#sip_session{reg_tref = RegTRef}, if IsOutboundSupported -> case get_ob_params(Params) of error -> Session1; {_, _} -> FlowTimeout = get_flow_timeout(LServer, SIPSock), FlowTRef = set_timer(Session1, FlowTimeout), NewMRef = if Type == udp -> MRef; true -> erlang:monitor(process, Pid) end, Session1#sip_session{conn_mref = NewMRef, flow_tref = FlowTRef} end; true -> Session1 end. set_timer(#sip_session{us = US}, Timeout) -> erlang:start_timer(Timeout * 1000, self(), US). close_socket(#sip_session{socket = SIPSocket}) -> if SIPSocket#sip_socket.type /= udp -> esip_socket:close(SIPSocket); true -> ok end. delete_session(#sip_session{reg_tref = RegTRef, flow_tref = FlowTRef, conn_mref = MRef} = Session) -> misc:cancel_timer(RegTRef), misc:cancel_timer(FlowTRef), catch erlang:demonitor(MRef, [flush]), mnesia:dirty_delete_object(Session). process_ping(SIPSocket) -> ErrResponse = if SIPSocket#sip_socket.type == udp -> pang; true -> drop end, Sessions = mnesia:dirty_index_read( sip_session, SIPSocket, #sip_session.socket), lists:foldl( fun(#sip_session{flow_tref = TRef, us = {_, LServer}} = Session, _) when TRef /= undefined -> erlang:cancel_timer(TRef), mnesia:dirty_delete_object(Session), Timeout = get_flow_timeout(LServer, SIPSocket), NewTRef = set_timer(Session, Timeout), mnesia:dirty_write(Session#sip_session{flow_tref = NewTRef}), pong; (_, Acc) -> Acc end, ErrResponse, Sessions). -endif. ejabberd-21.12/src/mod_muc_log_opt.erl0000644000232200023220000000553114154362354020277 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_muc_log_opt). -export([access_log/1]). -export([cssfile/1]). -export([dirname/1]). -export([dirtype/1]). -export([file_format/1]). -export([file_permissions/1]). -export([outdir/1]). -export([spam_prevention/1]). -export([timezone/1]). -export([top_link/1]). -export([url/1]). -spec access_log(gen_mod:opts() | global | binary()) -> 'muc_admin' | acl:acl(). access_log(Opts) when is_map(Opts) -> gen_mod:get_opt(access_log, Opts); access_log(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, access_log). -spec cssfile(gen_mod:opts() | global | binary()) -> {'file',binary()} | {'url',binary()}. cssfile(Opts) when is_map(Opts) -> gen_mod:get_opt(cssfile, Opts); cssfile(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, cssfile). -spec dirname(gen_mod:opts() | global | binary()) -> 'room_jid' | 'room_name'. dirname(Opts) when is_map(Opts) -> gen_mod:get_opt(dirname, Opts); dirname(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, dirname). -spec dirtype(gen_mod:opts() | global | binary()) -> 'plain' | 'subdirs'. dirtype(Opts) when is_map(Opts) -> gen_mod:get_opt(dirtype, Opts); dirtype(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, dirtype). -spec file_format(gen_mod:opts() | global | binary()) -> 'html' | 'plaintext'. file_format(Opts) when is_map(Opts) -> gen_mod:get_opt(file_format, Opts); file_format(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, file_format). -spec file_permissions(gen_mod:opts() | global | binary()) -> {non_neg_integer(),non_neg_integer()}. file_permissions(Opts) when is_map(Opts) -> gen_mod:get_opt(file_permissions, Opts); file_permissions(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, file_permissions). -spec outdir(gen_mod:opts() | global | binary()) -> binary(). outdir(Opts) when is_map(Opts) -> gen_mod:get_opt(outdir, Opts); outdir(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, outdir). -spec spam_prevention(gen_mod:opts() | global | binary()) -> boolean(). spam_prevention(Opts) when is_map(Opts) -> gen_mod:get_opt(spam_prevention, Opts); spam_prevention(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, spam_prevention). -spec timezone(gen_mod:opts() | global | binary()) -> 'local' | 'universal'. timezone(Opts) when is_map(Opts) -> gen_mod:get_opt(timezone, Opts); timezone(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, timezone). -spec top_link(gen_mod:opts() | global | binary()) -> {binary(),binary()}. top_link(Opts) when is_map(Opts) -> gen_mod:get_opt(top_link, Opts); top_link(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, top_link). -spec url(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). url(Opts) when is_map(Opts) -> gen_mod:get_opt(url, Opts); url(Host) -> gen_mod:get_module_opt(Host, mod_muc_log, url). ejabberd-21.12/src/mod_s2s_dialback.erl0000644000232200023220000003533214154362354020313 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 16 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_s2s_dialback). -behaviour(gen_mod). -protocol({xep, 220, '1.1.1'}). -protocol({xep, 185, '1.0'}). %% gen_mod API -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% Hooks -export([s2s_out_auth_result/2, s2s_out_downgraded/2, s2s_in_packet/2, s2s_out_packet/2, s2s_in_recv/3, s2s_in_features/2, s2s_out_init/2, s2s_out_closed/2, s2s_out_tls_verify/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== start(Host, _Opts) -> ejabberd_hooks:add(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), ejabberd_hooks:add(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), ejabberd_hooks:add(s2s_in_pre_auth_features, Host, ?MODULE, s2s_in_features, 50), ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE, s2s_in_features, 50), ejabberd_hooks:add(s2s_in_handle_recv, Host, ?MODULE, s2s_in_recv, 50), ejabberd_hooks:add(s2s_in_unauthenticated_packet, Host, ?MODULE, s2s_in_packet, 50), ejabberd_hooks:add(s2s_in_authenticated_packet, Host, ?MODULE, s2s_in_packet, 50), ejabberd_hooks:add(s2s_out_packet, Host, ?MODULE, s2s_out_packet, 50), ejabberd_hooks:add(s2s_out_downgraded, Host, ?MODULE, s2s_out_downgraded, 50), ejabberd_hooks:add(s2s_out_auth_result, Host, ?MODULE, s2s_out_auth_result, 50), ejabberd_hooks:add(s2s_out_tls_verify, Host, ?MODULE, s2s_out_tls_verify, 50). stop(Host) -> ejabberd_hooks:delete(s2s_out_init, Host, ?MODULE, s2s_out_init, 50), ejabberd_hooks:delete(s2s_out_closed, Host, ?MODULE, s2s_out_closed, 50), ejabberd_hooks:delete(s2s_in_pre_auth_features, Host, ?MODULE, s2s_in_features, 50), ejabberd_hooks:delete(s2s_in_post_auth_features, Host, ?MODULE, s2s_in_features, 50), ejabberd_hooks:delete(s2s_in_handle_recv, Host, ?MODULE, s2s_in_recv, 50), ejabberd_hooks:delete(s2s_in_unauthenticated_packet, Host, ?MODULE, s2s_in_packet, 50), ejabberd_hooks:delete(s2s_in_authenticated_packet, Host, ?MODULE, s2s_in_packet, 50), ejabberd_hooks:delete(s2s_out_packet, Host, ?MODULE, s2s_out_packet, 50), ejabberd_hooks:delete(s2s_out_downgraded, Host, ?MODULE, s2s_out_downgraded, 50), ejabberd_hooks:delete(s2s_out_auth_result, Host, ?MODULE, s2s_out_auth_result, 50), ejabberd_hooks:delete(s2s_out_tls_verify, Host, ?MODULE, s2s_out_tls_verify, 50). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. mod_opt_type(access) -> econf:acl(). mod_options(_Host) -> [{access, all}]. mod_doc() -> #{desc => [?T("The module adds support for " "https://xmpp.org/extensions/xep-0220.html" "[XEP-0220: Server Dialback] to provide server identity " "verification based on DNS."), "", ?T("WARNING: DNS-based verification is vulnerable to " "https://en.wikipedia.org/wiki/DNS_spoofing" "[DNS cache poisoning], so modern servers rely on " "verification based on PKIX certificates. Thus this module " "is only recommended for backward compatibility " "with servers running outdated software or non-TLS servers, " "or those with invalid certificates (as long as you accept " "the risks, e.g. you assume that the remote server has " "an invalid certificate due to poor administration and " "not because it's compromised).")], opts => [{access, #{value => ?T("AccessName"), desc => ?T("An access rule that can be used to restrict " "dialback for some servers. The default value " "is 'all'.")}}], example => ["modules:", " ...", " mod_s2s_dialback:", " access:", " allow:", " server: legacy.domain.tld", " server: invalid-cert.example.org", " deny: all", " ..."]}. s2s_in_features(Acc, _) -> [#db_feature{errors = true}|Acc]. s2s_out_init({ok, State}, Opts) -> case proplists:get_value(db_verify, Opts) of {StreamID, Key, Pid} -> %% This is an outbound s2s connection created at step 1. %% The purpose of this connection is to verify dialback key ONLY. %% The connection is not registered in s2s table and thus is not %% seen by anyone. %% The connection will be closed immediately after receiving the %% verification response (at step 3) {ok, State#{db_verify => {StreamID, Key, Pid}}}; undefined -> {ok, State#{db_enabled => true}} end; s2s_out_init(Acc, _Opts) -> Acc. s2s_out_closed(#{server := LServer, remote_server := RServer, lang := Lang, db_verify := {StreamID, _Key, _Pid}} = State, Reason) -> %% Outbound s2s verificating connection (created at step 1) is %% closed suddenly without receiving the response. %% Building a response on our own Response = #db_verify{from = RServer, to = LServer, id = StreamID, type = error, sub_els = [mk_error(Reason, Lang)]}, s2s_out_packet(State, Response); s2s_out_closed(State, _Reason) -> State. s2s_out_auth_result(#{db_verify := _} = State, _) -> %% The temporary outbound s2s connect (intended for verification) %% has passed authentication state (either successfully or not, no matter) %% and at this point we can send verification request as described %% in section 2.1.2, step 2 {stop, send_verify_request(State)}; s2s_out_auth_result(#{db_enabled := true, socket := Socket, ip := IP, server := LServer, remote_server := RServer} = State, {false, _}) -> %% SASL authentication has failed, retrying with dialback %% Sending dialback request, section 2.1.1, step 1 ?INFO_MSG("(~ts) Retrying with s2s dialback authentication: ~ts -> ~ts (~ts)", [xmpp_socket:pp(Socket), LServer, RServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), State1 = maps:remove(stop_reason, State#{on_route => queue}), {stop, send_db_request(State1)}; s2s_out_auth_result(State, _) -> State. s2s_out_downgraded(#{db_verify := _} = State, _) -> %% The verifying outbound s2s connection detected non-RFC compliant %% server, send verification request immediately without auth phase, %% section 2.1.2, step 2 {stop, send_verify_request(State)}; s2s_out_downgraded(#{db_enabled := true, socket := Socket, ip := IP, server := LServer, remote_server := RServer} = State, _) -> %% non-RFC compliant server detected, send dialback request instantly, %% section 2.1.1, step 1 ?INFO_MSG("(~ts) Trying s2s dialback authentication with " "non-RFC compliant server: ~ts -> ~ts (~ts)", [xmpp_socket:pp(Socket), LServer, RServer, ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), {stop, send_db_request(State)}; s2s_out_downgraded(State, _) -> State. s2s_in_packet(#{stream_id := StreamID, lang := Lang} = State, #db_result{from = From, to = To, key = Key, type = undefined}) -> %% Received dialback request, section 2.2.1, step 1 try ok = check_from_to(From, To), %% We're creating a temporary outbound s2s connection to %% send verification request and to receive verification response {ok, Pid} = ejabberd_s2s_out:start( To, From, [{db_verify, {StreamID, Key, self()}}]), ejabberd_s2s_out:connect(Pid), {stop, State} catch _:{badmatch, {error, Reason}} -> {stop, send_db_result(State, #db_verify{from = From, to = To, type = error, sub_els = [mk_error(Reason, Lang)]})} end; s2s_in_packet(State, #db_verify{to = To, from = From, key = Key, id = StreamID, type = undefined}) -> %% Received verification request, section 2.2.2, step 2 Type = case make_key(To, From, StreamID) of Key -> valid; _ -> invalid end, Response = #db_verify{from = To, to = From, id = StreamID, type = Type}, {stop, ejabberd_s2s_in:send(State, Response)}; s2s_in_packet(State, Pkt) when is_record(Pkt, db_result); is_record(Pkt, db_verify) -> ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), State; s2s_in_packet(State, _) -> State. s2s_in_recv(#{lang := Lang} = State, El, {error, Why}) -> case xmpp:get_name(El) of Tag when Tag == <<"db:result">>; Tag == <<"db:verify">> -> case xmpp:get_type(El) of T when T /= <<"valid">>, T /= <<"invalid">>, T /= <<"error">> -> Err = xmpp:make_error(El, mk_error({codec_error, Why}, Lang)), {stop, ejabberd_s2s_in:send(State, Err)}; _ -> State end; _ -> State end; s2s_in_recv(State, _El, _Pkt) -> State. s2s_out_packet(#{server := LServer, remote_server := RServer, db_verify := {StreamID, _Key, Pid}} = State, #db_verify{from = RServer, to = LServer, id = StreamID, type = Type} = Response) when Type /= undefined -> %% Received verification response, section 2.1.2, step 3 %% This is a response for the request sent at step 2 ejabberd_s2s_in:update_state( Pid, fun(S) -> send_db_result(S, Response) end), %% At this point the connection is no longer needed and we can terminate it ejabberd_s2s_out:stop_async(self()), State; s2s_out_packet(#{server := LServer, remote_server := RServer} = State, #db_result{to = LServer, from = RServer, type = Type} = Result) when Type /= undefined -> %% Received dialback response, section 2.1.1, step 4 %% This is a response to the request sent at step 1 State1 = maps:remove(db_enabled, State), case Type of valid -> State2 = ejabberd_s2s_out:handle_auth_success(<<"dialback">>, State1), ejabberd_s2s_out:establish(State2); _ -> Reason = str:format("Peer responded with error: ~s", [format_error(Result)]), ejabberd_s2s_out:handle_auth_failure( <<"dialback">>, {auth, Reason}, State1) end; s2s_out_packet(State, Pkt) when is_record(Pkt, db_result); is_record(Pkt, db_verify) -> ?WARNING_MSG("Got stray dialback packet:~n~ts", [xmpp:pp(Pkt)]), State; s2s_out_packet(State, _) -> State. -spec s2s_out_tls_verify(boolean(), ejabberd_s2s_out:state()) -> boolean(). s2s_out_tls_verify(_, #{server_host := ServerHost, remote_server := RServer}) -> Access = mod_s2s_dialback_opt:access(ServerHost), case acl:match_rule(ServerHost, Access, jid:make(RServer)) of allow -> false; deny -> true end. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec make_key(binary(), binary(), binary()) -> binary(). make_key(From, To, StreamID) -> Secret = ejabberd_config:get_shared_key(), str:to_hexlist( misc:crypto_hmac(sha256, str:to_hexlist(crypto:hash(sha256, Secret)), [To, " ", From, " ", StreamID])). -spec send_verify_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). send_verify_request(#{server := LServer, remote_server := RServer, db_verify := {StreamID, Key, _Pid}} = State) -> Request = #db_verify{from = LServer, to = RServer, key = Key, id = StreamID}, ejabberd_s2s_out:send(State, Request). -spec send_db_request(ejabberd_s2s_out:state()) -> ejabberd_s2s_out:state(). send_db_request(#{server := LServer, remote_server := RServer, stream_remote_id := StreamID} = State) -> Key = make_key(LServer, RServer, StreamID), ejabberd_s2s_out:send(State, #db_result{from = LServer, to = RServer, key = Key}). -spec send_db_result(ejabberd_s2s_in:state(), db_verify()) -> ejabberd_s2s_in:state(). send_db_result(State, #db_verify{from = From, to = To, type = Type, sub_els = Els}) -> %% Sending dialback response, section 2.2.1, step 4 %% This is a response to the request received at step 1 Response = #db_result{from = To, to = From, type = Type, sub_els = Els}, State1 = ejabberd_s2s_in:send(State, Response), case Type of valid -> State2 = ejabberd_s2s_in:handle_auth_success( From, <<"dialback">>, undefined, State1), ejabberd_s2s_in:establish(State2); _ -> Reason = str:format("Verification failed: ~s", [format_error(Response)]), ejabberd_s2s_in:handle_auth_failure( From, <<"dialback">>, Reason, State1) end. -spec check_from_to(binary(), binary()) -> ok | {error, forbidden | host_unknown}. check_from_to(From, To) -> case ejabberd_router:is_my_route(To) of false -> {error, host_unknown}; true -> LServer = ejabberd_router:host_of_route(To), case ejabberd_s2s:allow_host(LServer, From) of true -> ok; false -> {error, forbidden} end end. -spec mk_error(term(), binary()) -> stanza_error(). mk_error(forbidden, Lang) -> xmpp:err_forbidden(?T("Access denied by service policy"), Lang); mk_error(host_unknown, Lang) -> xmpp:err_not_allowed(?T("Host unknown"), Lang); mk_error({codec_error, Why}, Lang) -> xmpp:err_bad_request(xmpp:io_format_error(Why), Lang); mk_error({_Class, _Reason} = Why, Lang) -> Txt = xmpp_stream_out:format_error(Why), xmpp:err_remote_server_not_found(Txt, Lang); mk_error(_, _) -> xmpp:err_internal_server_error(). -spec format_error(db_result()) -> binary(). format_error(#db_result{type = invalid}) -> <<"invalid dialback key">>; format_error(#db_result{type = error} = Result) -> case xmpp:get_error(Result) of #stanza_error{} = Err -> xmpp:format_stanza_error(Err); undefined -> <<"unrecognized error">> end; format_error(_) -> <<"unexpected dialback result">>. ejabberd-21.12/src/ejabberd.app.src.script0000644000232200023220000000157714154362354020764 0ustar debalancedebalanceVars = case file:consult(filename:join([filename:dirname(SCRIPT), "..", "vars.config"])) of {ok, Terms} -> Backends = [mssql, mysql, odbc, pgsql, redis, sqlite], EBs = lists:filter(fun(Backend) -> lists:member({Backend, true}, Terms) end, Backends), [lists:keyfind(description, 1, Terms), lists:keyfind(vsn, 1, Terms), {env, [{enabled_backends, EBs}]} ]; _Err -> [] end, {application, ejabberd, Vars ++ [{modules, []}, {registered, []}, {applications, [kernel, sasl, ssl, stdlib]}, {included_applications, [inets, mnesia, os_mon, cache_tab, eimp, fast_tls, fast_xml, fast_yaml, lager, p1_acme, p1_utils, pkix, stringprep, yconf, xmpp]}, {mod, {ejabberd_app, []}}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: ejabberd-21.12/src/mod_mix_pam.erl0000644000232200023220000003364314154362354017427 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 4 Dec 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix_pam). -behaviour(gen_mod). -protocol({xep, 405, '0.3.0'}). %% gen_mod callbacks -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% Hooks and handlers -export([bounce_sm_packet/1, disco_sm_features/5, remove_user/2, process_iq/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -define(MIX_PAM_CACHE, mix_pam_cache). -callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. -callback add_channel(jid(), jid(), binary()) -> ok | {error, db_failure}. -callback del_channel(jid(), jid()) -> ok | {error, db_failure}. -callback get_channel(jid(), jid()) -> {ok, binary()} | {error, notfound | db_failure}. -callback get_channels(jid()) -> {ok, [{jid(), binary()}]} | {error, db_failure}. -callback del_channels(jid()) -> ok | {error, db_failure}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), case Mod:init(Host, Opts) of ok -> init_cache(Mod, Host, Opts), ejabberd_hooks:add(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0, ?MODULE, process_iq); Err -> Err end. stop(Host) -> ejabberd_hooks:delete(bounce_sm_packet, Host, ?MODULE, bounce_sm_packet, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_PAM_0). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). depends(_Host, _Opts) -> []. mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0405.html" "[XEP-0405: Mediated Information eXchange (MIX): " "Participant Server Requirements]. " "The module is needed if MIX compatible clients " "on your server are going to join MIX channels " "(either on your server or on any remote servers)."), "", ?T("NOTE: 'mod_mix' is not required for this module " "to work, however, without 'mod_mix_pam' the MIX " "functionality of your local XMPP clients will be impaired.")], opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. -spec bounce_sm_packet({term(), stanza()}) -> {term(), stanza()}. bounce_sm_packet({_, #message{to = #jid{lresource = <<>>} = To, from = From, type = groupchat} = Msg} = Acc) -> case xmpp:has_subtag(Msg, #mix{}) of true -> {LUser, LServer, _} = jid:tolower(To), case get_channel(To, From) of {ok, _} -> lists:foreach( fun(R) -> To1 = jid:replace_resource(To, R), ejabberd_router:route(xmpp:set_to(Msg, To1)) end, ejabberd_sm:get_user_resources(LUser, LServer)), {pass, Msg}; _ -> Acc end; false -> Acc end; bounce_sm_packet(Acc) -> Acc. -spec disco_sm_features({error, stanza_error()} | empty | {result, [binary()]}, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | empty | {result, [binary()]}. disco_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; disco_sm_features(Acc, _From, _To, <<"">>, _Lang) -> {result, [?NS_MIX_PAM_0 | case Acc of {result, Features} -> Features; empty -> [] end]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec process_iq(iq()) -> iq() | ignore. process_iq(#iq{from = #jid{luser = U1, lserver = S1}, to = #jid{luser = U2, lserver = S2}} = IQ) when {U1, S1} /= {U2, S2} -> xmpp:make_error(IQ, forbidden_query_error(IQ)); process_iq(#iq{type = set, sub_els = [#mix_client_join{} = Join]} = IQ) -> case Join#mix_client_join.channel of undefined -> xmpp:make_error(IQ, missing_channel_error(IQ)); _ -> process_join(IQ) end; process_iq(#iq{type = set, sub_els = [#mix_client_leave{} = Leave]} = IQ) -> case Leave#mix_client_leave.channel of undefined -> xmpp:make_error(IQ, missing_channel_error(IQ)); _ -> process_leave(IQ) end; process_iq(IQ) -> xmpp:make_error(IQ, unsupported_query_error(IQ)). -spec remove_user(binary(), binary()) -> ok | {error, db_failure}. remove_user(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), JID = jid:make(LUser, LServer), Chans = case Mod:get_channels(JID) of {ok, Channels} -> lists:map( fun({Channel, _}) -> ejabberd_router:route( #iq{from = JID, to = Channel, id = p1_rand:get_string(), type = set, sub_els = [#mix_leave{}]}), Channel end, Channels); _ -> [] end, Mod:del_channels(jid:make(LUser, LServer)), lists:foreach( fun(Chan) -> delete_cache(Mod, JID, Chan) end, Chans). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_join(iq()) -> ignore. process_join(#iq{from = From, sub_els = [#mix_client_join{channel = Channel, join = Join}]} = IQ) -> ejabberd_router:route_iq( #iq{from = jid:remove_resource(From), to = Channel, type = set, sub_els = [Join]}, fun(ResIQ) -> process_join_result(ResIQ, IQ) end), ignore. -spec process_leave(iq()) -> iq() | error. process_leave(#iq{from = From, sub_els = [#mix_client_leave{channel = Channel, leave = Leave}]} = IQ) -> case del_channel(From, Channel) of ok -> ejabberd_router:route_iq( #iq{from = jid:remove_resource(From), to = Channel, type = set, sub_els = [Leave]}, fun(ResIQ) -> process_leave_result(ResIQ, IQ) end), ignore; {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_join_result(iq(), iq()) -> ok. process_join_result(#iq{from = Channel, type = result, sub_els = [#mix_join{id = ID} = Join]}, #iq{to = To} = IQ) -> case add_channel(To, Channel, ID) of ok -> ChanID = make_channel_id(Channel, ID), Join1 = Join#mix_join{id = <<"">>, jid = ChanID}, ResIQ = xmpp:make_iq_result(IQ, #mix_client_join{join = Join1}), ejabberd_router:route(ResIQ); {error, db_failure} -> ejabberd_router:route_error(IQ, db_error(IQ)) end; process_join_result(Err, IQ) -> process_iq_error(Err, IQ). -spec process_leave_result(iq(), iq()) -> ok. process_leave_result(#iq{type = result, sub_els = [#mix_leave{} = Leave]}, IQ) -> ResIQ = xmpp:make_iq_result(IQ, #mix_client_leave{leave = Leave}), ejabberd_router:route(ResIQ); process_leave_result(Err, IQ) -> process_iq_error(Err, IQ). -spec process_iq_error(iq(), iq()) -> ok. process_iq_error(#iq{type = error} = ErrIQ, #iq{sub_els = [El]} = IQ) -> case xmpp:get_error(ErrIQ) of undefined -> %% Not sure if this stuff is correct because %% RFC6120 section 8.3.1 bullet 4 states that %% an error stanza MUST contain an child element IQ1 = xmpp:make_iq_result(IQ, El), ejabberd_router:route(IQ1#iq{type = error}); Err -> ejabberd_router:route_error(IQ, Err) end; process_iq_error(timeout, IQ) -> Txt = ?T("Request has timed out"), Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), ejabberd_router:route_error(IQ, Err). -spec make_channel_id(jid(), binary()) -> jid(). make_channel_id(JID, ID) -> {U, S, R} = jid:split(JID), jid:make(<>, S, R). %%%=================================================================== %%% Error generators %%%=================================================================== -spec missing_channel_error(stanza()) -> stanza_error(). missing_channel_error(Pkt) -> Txt = ?T("Attribute 'channel' is required for this request"), xmpp:err_bad_request(Txt, xmpp:get_lang(Pkt)). -spec forbidden_query_error(stanza()) -> stanza_error(). forbidden_query_error(Pkt) -> Txt = ?T("Query to another users is forbidden"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). -spec unsupported_query_error(stanza()) -> stanza_error(). unsupported_query_error(Pkt) -> Txt = ?T("No module is handling this query"), xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). -spec db_error(stanza()) -> stanza_error(). db_error(Pkt) -> Txt = ?T("Database failure"), xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). %%%=================================================================== %%% Database queries %%%=================================================================== get_channel(JID, Channel) -> {LUser, LServer, _} = jid:tolower(JID), {Chan, Service, _} = jid:tolower(Channel), Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of false -> Mod:get_channel(JID, Channel); true -> case ets_cache:lookup( ?MIX_PAM_CACHE, {LUser, LServer, Chan, Service}, fun() -> Mod:get_channel(JID, Channel) end) of error -> {error, notfound}; Ret -> Ret end end. add_channel(JID, Channel, ID) -> Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), case Mod:add_channel(JID, Channel, ID) of ok -> delete_cache(Mod, JID, Channel); Err -> Err end. del_channel(JID, Channel) -> Mod = gen_mod:db_mod(JID#jid.lserver, ?MODULE), case Mod:del_channel(JID, Channel) of ok -> delete_cache(Mod, JID, Channel); Err -> Err end. %%%=================================================================== %%% Cache management %%%=================================================================== -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?MIX_PAM_CACHE, CacheOpts); false -> ets_cache:delete(?MIX_PAM_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_mix_pam_opt:cache_size(Opts), CacheMissed = mod_mix_pam_opt:cache_missed(Opts), LifeTime = mod_mix_pam_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_mix_pam_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec delete_cache(module(), jid(), jid()) -> ok. delete_cache(Mod, JID, Channel) -> {LUser, LServer, _} = jid:tolower(JID), {Chan, Service, _} = jid:tolower(Channel), case use_cache(Mod, LServer) of true -> ets_cache:delete(?MIX_PAM_CACHE, {LUser, LServer, Chan, Service}, cache_nodes(Mod, LServer)); false -> ok end. ejabberd-21.12/src/acl.erl0000644000232200023220000003101714154362354015666 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 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(acl). -behaviour(gen_server). -export([start_link/0]). -export([reload_from_config/0]). -export([match_rule/3, match_acl/3]). -export([match_rules/4, match_acls/3]). -export([access_rules_validator/0, access_validator/0]). -export([validator/1, validators/0]). -export([loaded_shared_roster_module/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("logger.hrl"). -type state() :: #{hosts := [binary()]}. -type action() :: allow | deny. -type ip_mask() :: {inet:ip4_address(), 0..32} | {inet:ip6_address(), 0..128}. -type access_rule() :: {acl, atom()} | acl_rule(). -type acl_rule() :: {user, {binary(), binary()} | binary()} | {server, binary()} | {resource, binary()} | {user_regexp, {re:mp(), binary()} | re:mp()} | {server_regexp, re:mp()} | {resource_regexp, re:mp()} | {node_regexp, {re:mp(), re:mp()}} | {user_glob, {re:mp(), binary()} | re:mp()} | {server_glob, re:mp()} | {resource_glob, re:mp()} | {node_glob, {re:mp(), re:mp()}} | {shared_group, {binary(), binary()} | binary()} | {ip, ip_mask()}. -type access() :: [{action(), [access_rule()]}]. -type acl() :: atom() | access(). -type match() :: #{ip => inet:ip_address(), usr => jid:ljid(), atom() => term()}. -export_type([acl/0, acl_rule/0, access/0, access_rule/0, match/0]). %%%=================================================================== %%% API %%%=================================================================== start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec match_rule(global | binary(), atom() | access(), jid:jid() | jid:ljid() | inet:ip_address() | match()) -> action(). match_rule(_, all, _) -> allow; match_rule(_, none, _) -> deny; match_rule(Host, Access, Match) when is_map(Match) -> Rules = if is_atom(Access) -> read_access(Access, Host); true -> Access end, match_rules(Host, Rules, Match, deny); match_rule(Host, Access, IP) when tuple_size(IP) == 4; tuple_size(IP) == 8 -> match_rule(Host, Access, #{ip => IP}); match_rule(Host, Access, JID) -> match_rule(Host, Access, #{usr => jid:tolower(JID)}). -spec match_acl(global | binary(), access_rule(), match()) -> boolean(). match_acl(_Host, {acl, all}, _) -> true; match_acl(_Host, {acl, none}, _) -> false; match_acl(Host, {acl, ACLName}, Match) -> lists:any( fun(ACL) -> match_acl(Host, ACL, Match) end, read_acl(ACLName, Host)); match_acl(_Host, {ip, {Net, Mask}}, #{ip := {IP, _Port}}) -> misc:match_ip_mask(IP, Net, Mask); match_acl(_Host, {ip, {Net, Mask}}, #{ip := IP}) -> misc:match_ip_mask(IP, Net, Mask); match_acl(_Host, {user, {U, S}}, #{usr := {U, S, _}}) -> true; match_acl(_Host, {user, U}, #{usr := {U, S, _}}) -> ejabberd_router:is_my_host(S); match_acl(_Host, {server, S}, #{usr := {_, S, _}}) -> true; match_acl(_Host, {resource, R}, #{usr := {_, _, R}}) -> true; match_acl(_Host, {shared_group, {G, H}}, #{usr := {U, S, _}}) -> case loaded_shared_roster_module(H) of undefined -> false; Mod -> Mod:is_user_in_group({U, S}, G, H) end; match_acl(Host, {shared_group, G}, Map) -> match_acl(Host, {shared_group, {G, Host}}, Map); match_acl(_Host, {user_regexp, {UR, S1}}, #{usr := {U, S2, _}}) -> S1 == S2 andalso match_regexp(U, UR); match_acl(_Host, {user_regexp, UR}, #{usr := {U, S, _}}) -> ejabberd_router:is_my_host(S) andalso match_regexp(U, UR); match_acl(_Host, {server_regexp, SR}, #{usr := {_, S, _}}) -> match_regexp(S, SR); match_acl(_Host, {resource_regexp, RR}, #{usr := {_, _, R}}) -> match_regexp(R, RR); match_acl(_Host, {node_regexp, {UR, SR}}, #{usr := {U, S, _}}) -> match_regexp(U, UR) andalso match_regexp(S, SR); match_acl(_Host, {user_glob, {UR, S1}}, #{usr := {U, S2, _}}) -> S1 == S2 andalso match_regexp(U, UR); match_acl(_Host, {user_glob, UR}, #{usr := {U, S, _}}) -> ejabberd_router:is_my_host(S) andalso match_regexp(U, UR); match_acl(_Host, {server_glob, SR}, #{usr := {_, S, _}}) -> match_regexp(S, SR); match_acl(_Host, {resource_glob, RR}, #{usr := {_, _, R}}) -> match_regexp(R, RR); match_acl(_Host, {node_glob, {UR, SR}}, #{usr := {U, S, _}}) -> match_regexp(U, UR) andalso match_regexp(S, SR); match_acl(_, _, _) -> false. -spec match_rules(global | binary(), [{T, [access_rule()]}], match(), T) -> T. match_rules(Host, [{Return, Rules} | Rest], Match, Default) -> case match_acls(Host, Rules, Match) of false -> match_rules(Host, Rest, Match, Default); true -> Return end; match_rules(_Host, [], _Match, Default) -> Default. -spec match_acls(global | binary(), [access_rule()], match()) -> boolean(). match_acls(_Host, [], _Match) -> false; match_acls(Host, Rules, Match) -> lists:all( fun(Rule) -> match_acl(Host, Rule, Match) end, Rules). -spec reload_from_config() -> ok. reload_from_config() -> gen_server:call(?MODULE, reload_from_config, timer:minutes(1)). -spec validator(access_rules | acl) -> econf:validator(). validator(access_rules) -> econf:options( #{'_' => access_rules_validator()}, [{disallowed, [all, none]}, unique]); validator(acl) -> econf:options( #{'_' => acl_validator()}, [{disallowed, [all, none]}, unique]). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -spec init([]) -> {ok, state()}. init([]) -> create_tab(acl), create_tab(access), Hosts = ejabberd_option:hosts(), load_from_config(Hosts), ejabberd_hooks:add(config_reloaded, ?MODULE, reload_from_config, 20), {ok, #{hosts => Hosts}}. -spec handle_call(term(), term(), state()) -> {reply, ok, state()} | {noreply, state()}. handle_call(reload_from_config, _, State) -> NewHosts = ejabberd_option:hosts(), load_from_config(NewHosts), {reply, ok, State#{hosts => NewHosts}}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -spec handle_cast(term(), state()) -> {noreply, state()}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. -spec handle_info(term(), state()) -> {noreply, state()}. handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -spec terminate(any(), state()) -> ok. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, reload_from_config, 20). -spec code_change(term(), state(), term()) -> {ok, state()}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== %%%=================================================================== %%% Table management %%%=================================================================== -spec load_from_config([binary()]) -> ok. load_from_config(NewHosts) -> ?DEBUG("Loading access rules from config", []), load_tab(acl, NewHosts, fun ejabberd_option:acl/1), load_tab(access, NewHosts, fun ejabberd_option:access_rules/1), ?DEBUG("Access rules loaded successfully", []). -spec create_tab(atom()) -> atom(). create_tab(Tab) -> _ = mnesia:delete_table(Tab), ets:new(Tab, [named_table, set, {read_concurrency, true}]). -spec load_tab(atom(), [binary()], fun((global | binary()) -> {atom(), list()})) -> ok. load_tab(Tab, Hosts, Fun) -> Old = ets:tab2list(Tab), New = lists:flatmap( fun(Host) -> [{{Name, Host}, List} || {Name, List} <- Fun(Host)] end, [global|Hosts]), ets:insert(Tab, New), lists:foreach( fun({Key, _}) -> case lists:keymember(Key, 1, New) of false -> ets:delete(Tab, Key); true -> ok end end, Old). -spec read_access(atom(), global | binary()) -> access(). read_access(Name, Host) -> case ets:lookup(access, {Name, Host}) of [{_, Access}] -> Access; [] -> [] end. -spec read_acl(atom(), global | binary()) -> [acl_rule()]. read_acl(Name, Host) -> case ets:lookup(acl, {Name, Host}) of [{_, ACL}] -> ACL; [] -> [] end. %%%=================================================================== %%% Validators %%%=================================================================== validators() -> #{ip => econf:list_or_single(econf:ip_mask()), user => user_validator(econf:user(), econf:domain()), user_regexp => user_validator(econf:re([unicode]), econf:domain()), user_glob => user_validator(econf:glob([unicode]), econf:domain()), server => econf:list_or_single(econf:domain()), server_regexp => econf:list_or_single(econf:re([unicode])), server_glob => econf:list_or_single(econf:glob([unicode])), resource => econf:list_or_single(econf:resource()), resource_regexp => econf:list_or_single(econf:re([unicode])), resource_glob => econf:list_or_single(econf:glob([unicode])), node_regexp => node_validator(econf:re([unicode]), econf:re([unicode])), node_glob => node_validator(econf:glob([unicode]), econf:glob([unicode])), shared_group => user_validator(econf:binary(), econf:domain()), acl => econf:atom()}. rule_validator() -> rule_validator(validators()). rule_validator(RVs) -> econf:and_then( econf:non_empty(econf:options(RVs, [])), fun(Rules) -> lists:flatmap( fun({Type, Rs}) when is_list(Rs) -> [{Type, R} || R <- Rs]; (Other) -> [Other] end, Rules) end). access_validator() -> econf:and_then( fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:atom())(K), V}; (A) -> {acl, (econf:atom())(A)} end, lists:flatten(L)); (A) -> [{acl, (econf:atom())(A)}] end, rule_validator()). access_rules_validator() -> econf:and_then( fun(L) when is_list(L) -> lists:map( fun({K, V}) -> {(econf:atom())(K), V}; (A) -> {(econf:atom())(A), [{acl, all}]} end, lists:flatten(L)); (Bad) -> Bad end, econf:non_empty( econf:options( #{allow => access_validator(), deny => access_validator()}, []))). acl_validator() -> econf:and_then( fun(L) when is_list(L) -> lists:flatten(L); (Bad) -> Bad end, rule_validator(maps:remove(acl, validators()))). user_validator(UV, SV) -> econf:and_then( econf:list_or_single( fun({U, S}) -> {UV(U), SV(S)}; (M) when is_list(M) -> (econf:map(UV, SV))(M); (Val) -> US = (econf:binary())(Val), case binary:split(US, <<"@">>, [global]) of [U, S] -> {UV(U), SV(S)}; [U] -> UV(U); _ -> econf:fail({bad_user, Val}) end end), fun lists:flatten/1). node_validator(UV, SV) -> econf:and_then( econf:and_then( econf:list(econf:any()), fun lists:flatten/1), econf:map(UV, SV)). %%%=================================================================== %%% Aux %%%=================================================================== -spec match_regexp(iodata(), re:mp()) -> boolean(). match_regexp(Data, RegExp) -> re:run(Data, RegExp) /= nomatch. -spec loaded_shared_roster_module(global | binary()) -> atom(). loaded_shared_roster_module(global) -> loaded_shared_roster_module(ejabberd_config:get_myname()); loaded_shared_roster_module(Host) -> case gen_mod:is_loaded(Host, mod_shared_roster_ldap) of true -> mod_shared_roster_ldap; false -> case gen_mod:is_loaded(Host, mod_shared_roster) of true -> mod_shared_roster; false -> undefined end end. ejabberd-21.12/src/ejabberd_router_redis.erl0000644000232200023220000001224514154362354021455 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_router_redis). -behaviour(ejabberd_router). -behaviour(gen_server). %% API -export([init/0, register_route/5, unregister_route/3, find_routes/1, get_all_routes/0]). %% gen_server callbacks -export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2, code_change/3, start_link/0]). -include("logger.hrl"). -include("ejabberd_router.hrl"). -record(state, {}). -define(ROUTES_KEY, <<"ejabberd:routes">>). -define(DOMAINS_KEY, <<"ejabberd:domains">>). %%%=================================================================== %%% API %%%=================================================================== init() -> Spec = {?MODULE, {?MODULE, start_link, []}, transient, 5000, worker, [?MODULE]}, case supervisor:start_child(ejabberd_backend_sup, Spec) of {ok, _Pid} -> ok; Err -> Err end. -spec start_link() -> {ok, pid()} | {error, any()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). register_route(Domain, ServerHost, LocalHint, _, Pid) -> DomKey = domain_key(Domain), PidKey = term_to_binary(Pid), T = term_to_binary({ServerHost, LocalHint}), case ejabberd_redis:multi( fun() -> ejabberd_redis:hset(DomKey, PidKey, T), ejabberd_redis:sadd(?DOMAINS_KEY, [Domain]), if Domain /= ServerHost -> ejabberd_redis:sadd(?ROUTES_KEY, [Domain]); true -> ok end end) of {ok, _} -> ok; {error, _} -> {error, db_failure} end. unregister_route(Domain, _, Pid) -> DomKey = domain_key(Domain), PidKey = term_to_binary(Pid), try {ok, Num} = ejabberd_redis:hdel(DomKey, [PidKey]), if Num > 0 -> {ok, Len} = ejabberd_redis:hlen(DomKey), if Len == 0 -> {ok, _} = ejabberd_redis:multi( fun() -> ejabberd_redis:del([DomKey]), ejabberd_redis:srem(?ROUTES_KEY, [Domain]), ejabberd_redis:srem(?DOMAINS_KEY, [Domain]) end), ok; true -> ok end; true -> ok end catch _:{badmatch, {error, _}} -> {error, db_failure} end. find_routes(Domain) -> DomKey = domain_key(Domain), case ejabberd_redis:hgetall(DomKey) of {ok, Vals} -> {ok, decode_routes(Domain, Vals)}; _ -> {error, db_failure} end. get_all_routes() -> case ejabberd_redis:smembers(?ROUTES_KEY) of {ok, Routes} -> {ok, Routes}; _ -> {error, db_failure} end. get_all_domains() -> case ejabberd_redis:smembers(?DOMAINS_KEY) of {ok, Domains} -> {ok, Domains}; _ -> {error, db_failure} end. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([]) -> clean_table(), {ok, #state{}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(Info, State) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== clean_table() -> ?DEBUG("Cleaning Redis route entries...", []), lists:foreach( fun(#route{domain = Domain, pid = Pid}) when node(Pid) == node() -> unregister_route(Domain, undefined, Pid); (_) -> ok end, find_routes()). find_routes() -> case get_all_domains() of {ok, Domains} -> lists:flatmap( fun(Domain) -> case find_routes(Domain) of {ok, Routes} -> Routes; {error, _} -> [] end end, Domains); {error, _} -> [] end. domain_key(Domain) -> <<"ejabberd:route:", Domain/binary>>. decode_routes(Domain, Vals) -> lists:map( fun({Pid, Data}) -> {ServerHost, LocalHint} = binary_to_term(Data), #route{domain = Domain, pid = binary_to_term(Pid), server_host = ServerHost, local_hint = LocalHint} end, Vals). ejabberd-21.12/src/mod_bosh_sql.erl0000644000232200023220000000544114154362354017602 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_bosh_sql.erl %%% Author : Evgeny Khramtsov %%% Purpose : %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_bosh_sql). -behaviour(mod_bosh). %% API -export([init/0, open_session/2, close_session/1, find_session/1]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init() -> Node = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'bosh' table...", []), case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from bosh where node=%(Node)s")) of {updated, _} -> ok; Err -> ?ERROR_MSG("Failed to clean 'route' table: ~p", [Err]), Err end. open_session(SID, Pid) -> PidS = misc:encode_pid(Pid), Node = erlang:atom_to_binary(node(Pid), latin1), case ?SQL_UPSERT(ejabberd_config:get_myname(), "bosh", ["!sid=%(SID)s", "node=%(Node)s", "pid=%(PidS)s"]) of ok -> ok; _Err -> {error, db_failure} end. close_session(SID) -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from bosh where sid=%(SID)s")) of {updated, _} -> ok; _Err -> {error, db_failure} end. find_session(SID) -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("select @(pid)s, @(node)s from bosh where sid=%(SID)s")) of {selected, [{Pid, Node}]} -> try {ok, misc:decode_pid(Pid, Node)} catch _:{bad_node, _} -> {error, notfound} end; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_muc_room.erl0000644000232200023220000060353114154362354017614 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_muc_room.erl %%% Author : Alexey Shchepin %%% Purpose : MUC room stuff %%% Created : 19 Mar 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_room). -author('alexey@process-one.net'). -protocol({xep, 317, '0.1'}). -behaviour(p1_fsm). %% External exports -export([start_link/10, start_link/8, start/10, start/8, supervisor/1, get_role/2, get_affiliation/2, is_occupant_or_admin/2, route/2, expand_opts/1, config_fields/0, destroy/1, destroy/2, shutdown/1, get_config/1, set_config/2, get_state/1, change_item/5, config_reloaded/1, subscribe/4, unsubscribe/2, is_subscribed/2, get_subscribers/1, service_message/2, get_disco_item/4]). %% gen_fsm callbacks -export([init/1, normal_state/2, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_stacktrace.hrl"). -define(MAX_USERS_DEFAULT_LIST, [5, 10, 20, 30, 50, 100, 200, 500, 1000, 2000, 5000]). -define(DEFAULT_MAX_USERS_PRESENCE,1000). -define(MUC_HAT_ADD_CMD, <<"http://prosody.im/protocol/hats#add">>). -define(MUC_HAT_REMOVE_CMD, <<"http://prosody.im/protocol/hats#remove">>). -define(MUC_HAT_LIST_CMD, <<"p1:hats#list">>). -define(MAX_HATS_USERS, 100). -define(MAX_HATS_PER_USER, 10). %-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. -type state() :: #state{}. -type fsm_stop() :: {stop, normal, state()}. -type fsm_next() :: {next_state, normal_state, state()}. -type fsm_transition() :: fsm_stop() | fsm_next(). -type disco_item_filter() :: only_non_empty | all | non_neg_integer(). -type admin_action() :: {jid(), affiliation | role, affiliation() | role(), binary()}. -export_type([state/0, disco_item_filter/0]). -callback set_affiliation(binary(), binary(), binary(), jid(), affiliation(), binary()) -> ok | {error, any()}. -callback set_affiliations(binary(), binary(), binary(), affiliations()) -> ok | {error, any()}. -callback get_affiliation(binary(), binary(), binary(), binary(), binary()) -> {ok, affiliation()} | {error, any()}. -callback get_affiliations(binary(), binary(), binary()) -> {ok, affiliations()} | {error, any()}. -callback search_affiliation(binary(), binary(), binary(), affiliation()) -> {ok, [{ljid(), {affiliation(), binary()}}]} | {error, any()}. %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- -spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), atom(), jid(), binary(), [{atom(), term()}], ram | file) -> {ok, pid()} | {error, any()}. start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType) -> supervisor:start_child( supervisor(ServerHost), [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType]). -spec start(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), atom(), [{atom(), term()}], ram | file) -> {ok, pid()} | {error, any()}. start(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> supervisor:start_child( supervisor(ServerHost), [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]). -spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), atom(), jid(), binary(), [{atom(), term()}], ram | file) -> {ok, pid()} | {error, any()}. start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType) -> p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, Nick, DefRoomOpts, QueueType], ?FSMOPTS). -spec start_link(binary(), binary(), mod_muc:access(), binary(), non_neg_integer(), atom(), [{atom(), term()}], ram | file) -> {ok, pid()} | {error, any()}. start_link(Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType) -> p1_fsm:start_link(?MODULE, [Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType], ?FSMOPTS). -spec supervisor(binary()) -> atom(). supervisor(Host) -> gen_mod:get_module_proc(Host, mod_muc_room_sup). -spec destroy(pid()) -> ok. destroy(Pid) -> p1_fsm:send_all_state_event(Pid, destroy). -spec destroy(pid(), binary()) -> ok. destroy(Pid, Reason) -> p1_fsm:send_all_state_event(Pid, {destroy, Reason}). -spec shutdown(pid()) -> boolean(). shutdown(Pid) -> ejabberd_cluster:send(Pid, shutdown). -spec config_reloaded(pid()) -> boolean(). config_reloaded(Pid) -> ejabberd_cluster:send(Pid, config_reloaded). -spec get_config(pid()) -> {ok, config()} | {error, notfound | timeout}. get_config(Pid) -> try p1_fsm:sync_send_all_state_event(Pid, get_config) catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. -spec set_config(pid(), config()) -> {ok, config()} | {error, notfound | timeout}. set_config(Pid, Config) -> try p1_fsm:sync_send_all_state_event(Pid, {change_config, Config}) catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. -spec change_item(pid(), jid(), affiliation | role, affiliation() | role(), binary()) -> {ok, state()} | {error, notfound | timeout}. change_item(Pid, JID, Type, AffiliationOrRole, Reason) -> try p1_fsm:sync_send_all_state_event( Pid, {process_item_change, {JID, Type, AffiliationOrRole, Reason}, undefined}) catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. -spec get_state(pid()) -> {ok, state()} | {error, notfound | timeout}. get_state(Pid) -> try p1_fsm:sync_send_all_state_event(Pid, get_state) catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. -spec subscribe(pid(), jid(), binary(), [binary()]) -> {ok, [binary()]} | {error, binary()}. subscribe(Pid, JID, Nick, Nodes) -> try p1_fsm:sync_send_all_state_event(Pid, {muc_subscribe, JID, Nick, Nodes}) catch _:{timeout, {p1_fsm, _, _}} -> {error, ?T("Request has timed out")}; _:{_, {p1_fsm, _, _}} -> {error, ?T("Conference room does not exist")} end. -spec unsubscribe(pid(), jid()) -> ok | {error, binary()}. unsubscribe(Pid, JID) -> try p1_fsm:sync_send_all_state_event(Pid, {muc_unsubscribe, JID}) catch _:{timeout, {p1_fsm, _, _}} -> {error, ?T("Request has timed out")}; exit:{normal, {p1_fsm, _, _}} -> ok; _:{_, {p1_fsm, _, _}} -> {error, ?T("Conference room does not exist")} end. -spec is_subscribed(pid(), jid()) -> {true, binary(), [binary()]} | false. is_subscribed(Pid, JID) -> try p1_fsm:sync_send_all_state_event(Pid, {is_subscribed, JID}) catch _:{_, {p1_fsm, _, _}} -> false end. -spec get_subscribers(pid()) -> {ok, [jid()]} | {error, notfound | timeout}. get_subscribers(Pid) -> try p1_fsm:sync_send_all_state_event(Pid, get_subscribers) catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. -spec service_message(pid(), binary()) -> ok. service_message(Pid, Text) -> p1_fsm:send_all_state_event(Pid, {service_message, Text}). -spec get_disco_item(pid(), disco_item_filter(), jid(), binary()) -> {ok, binary()} | {error, notfound | timeout}. get_disco_item(Pid, Filter, JID, Lang) -> Timeout = 100, Time = erlang:system_time(millisecond), Query = {get_disco_item, Filter, JID, Lang, Time+Timeout}, try p1_fsm:sync_send_all_state_event(Pid, Query, Timeout) of {item, Desc} -> {ok, Desc}; false -> {error, notfound} catch _:{timeout, {p1_fsm, _, _}} -> {error, timeout}; _:{_, {p1_fsm, _, _}} -> {error, notfound} end. %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Creator, _Nick, DefRoomOpts, QueueType]) -> process_flag(trap_exit, true), Shaper = ejabberd_shaper:new(RoomShaper), RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), State = set_affiliation(Creator, owner, #state{host = Host, server_host = ServerHost, access = Access, room = Room, history = lqueue_new(HistorySize, QueueType), jid = jid:make(Room, Host), just_created = true, room_queue = RoomQueue, room_shaper = Shaper}), State1 = set_opts(DefRoomOpts, State), store_room(State1), ?INFO_MSG("Created MUC room ~ts@~ts by ~ts", [Room, Host, jid:encode(Creator)]), add_to_log(room_existence, created, State1), add_to_log(room_existence, started, State1), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), {ok, normal_state, reset_hibernate_timer(State1)}; init([Host, ServerHost, Access, Room, HistorySize, RoomShaper, Opts, QueueType]) -> process_flag(trap_exit, true), Shaper = ejabberd_shaper:new(RoomShaper), RoomQueue = room_queue_new(ServerHost, Shaper, QueueType), State = set_opts(Opts, #state{host = Host, server_host = ServerHost, access = Access, room = Room, history = lqueue_new(HistorySize, QueueType), jid = jid:make(Room, Host), room_queue = RoomQueue, room_shaper = Shaper}), add_to_log(room_existence, started, State), ejabberd_hooks:run(start_room, ServerHost, [ServerHost, Room, Host]), {ok, normal_state, reset_hibernate_timer(State)}. normal_state({route, <<"">>, #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> case is_user_online(From, StateData) orelse is_subscriber(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData) of true when Type == groupchat -> Activity = get_user_activity(From, StateData), Now = erlang:system_time(microsecond), MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000000), Size = element_size(Packet), {MessageShaper, MessageShaperInterval} = ejabberd_shaper:update(Activity#activity.message_shaper, Size), if Activity#activity.message /= undefined -> ErrText = ?T("Traffic rate limit is exceeded"), Err = xmpp:err_resource_constraint(ErrText, Lang), ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData}; Now >= Activity#activity.message_time + MinMessageInterval, MessageShaperInterval == 0 -> {RoomShaper, RoomShaperInterval} = ejabberd_shaper:update(StateData#state.room_shaper, Size), RoomQueueEmpty = case StateData#state.room_queue of undefined -> true; RQ -> p1_queue:is_empty(RQ) end, if RoomShaperInterval == 0, RoomQueueEmpty -> NewActivity = Activity#activity{ message_time = Now, message_shaper = MessageShaper}, StateData1 = store_user_activity(From, NewActivity, StateData), StateData2 = StateData1#state{room_shaper = RoomShaper}, process_groupchat_message(Packet, StateData2); true -> StateData1 = if RoomQueueEmpty -> erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; true -> StateData end, NewActivity = Activity#activity{ message_time = Now, message_shaper = MessageShaper, message = Packet}, RoomQueue = p1_queue:in({message, From}, StateData#state.room_queue), StateData2 = store_user_activity(From, NewActivity, StateData1), StateData3 = StateData2#state{room_queue = RoomQueue}, {next_state, normal_state, StateData3} end; true -> MessageInterval = (Activity#activity.message_time + MinMessageInterval - Now) div 1000, Interval = lists:max([MessageInterval, MessageShaperInterval]), erlang:send_after(Interval, self(), {process_user_message, From}), NewActivity = Activity#activity{ message = Packet, message_shaper = MessageShaper}, StateData1 = store_user_activity(From, NewActivity, StateData), {next_state, normal_state, StateData1} end; true when Type == error -> case is_user_online(From, StateData) of true -> ErrorText = ?T("It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room"), NewState = expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)), close_room_if_temporary_and_empty(NewState); _ -> {next_state, normal_state, StateData} end; true when Type == chat -> ErrText = ?T("It is not allowed to send private messages " "to the conference"), Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData}; true when Type == normal -> {next_state, normal_state, try xmpp:decode_els(Packet) of Pkt -> process_normal_message(From, Pkt, StateData) catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Packet, Err), StateData end}; true -> ErrText = ?T("Improper message type"), Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData}; false when Type /= error -> handle_roommessage_from_nonparticipant(Packet, StateData, From), {next_state, normal_state, StateData}; false -> {next_state, normal_state, StateData} end; normal_state({route, <<"">>, #iq{from = From, type = Type, lang = Lang, sub_els = [_]} = IQ0}, StateData) when Type == get; Type == set -> try case ejabberd_hooks:run_fold( muc_process_iq, StateData#state.server_host, xmpp:set_from_to(xmpp:decode_els(IQ0), From, StateData#state.jid), [StateData]) of ignore -> {next_state, normal_state, StateData}; #iq{type = T} = IQRes when T == error; T == result -> ejabberd_router:route(IQRes), {next_state, normal_state, StateData}; #iq{sub_els = [SubEl]} = IQ -> Res1 = case SubEl of #muc_admin{} -> process_iq_admin(From, IQ, StateData); #muc_owner{} -> process_iq_owner(From, IQ, StateData); #disco_info{} -> process_iq_disco_info(From, IQ, StateData); #disco_items{} -> process_iq_disco_items(From, IQ, StateData); #vcard_temp{} -> process_iq_vcard(From, IQ, StateData); #muc_subscribe{} -> process_iq_mucsub(From, IQ, StateData); #muc_unsubscribe{} -> process_iq_mucsub(From, IQ, StateData); #muc_subscriptions{} -> process_iq_mucsub(From, IQ, StateData); #xcaptcha{} -> process_iq_captcha(From, IQ, StateData); #adhoc_command{} -> process_iq_adhoc(From, IQ, StateData); _ -> Txt = ?T("The feature requested is not " "supported by the conference"), {error, xmpp:err_service_unavailable(Txt, Lang)} end, {IQRes, NewStateData} = case Res1 of {result, Res, SD} -> {xmpp:make_iq_result(IQ, Res), SD}; {result, Res} -> {xmpp:make_iq_result(IQ, Res), StateData}; {ignore, SD} -> {ignore, SD}; {error, Error} -> {xmpp:make_error(IQ0, Error), StateData} end, if IQRes /= ignore -> ejabberd_router:route(IQRes); true -> ok end, case NewStateData of stop -> Conf = StateData#state.config, {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; _ when NewStateData#state.just_created -> close_room_if_temporary_and_empty(NewStateData); _ -> {next_state, normal_state, NewStateData} end end catch _:{xmpp_codec, Why} -> ErrTxt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(ErrTxt, Lang), ejabberd_router:route_error(IQ0, Err), {next_state, normal_state, StateData} end; normal_state({route, <<"">>, #iq{} = IQ}, StateData) -> Err = xmpp:err_bad_request(), ejabberd_router:route_error(IQ, Err), case StateData#state.just_created of true -> {stop, normal, StateData}; _ -> {next_state, normal_state, StateData} end; normal_state({route, Nick, #presence{from = From} = Packet}, StateData) -> Activity = get_user_activity(From, StateData), Now = erlang:system_time(microsecond), MinPresenceInterval = trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000000), if (Now >= Activity#activity.presence_time + MinPresenceInterval) and (Activity#activity.presence == undefined) -> NewActivity = Activity#activity{presence_time = Now}, StateData1 = store_user_activity(From, NewActivity, StateData), process_presence(Nick, Packet, StateData1); true -> if Activity#activity.presence == undefined -> Interval = (Activity#activity.presence_time + MinPresenceInterval - Now) div 1000, erlang:send_after(Interval, self(), {process_user_presence, From}); true -> ok end, NewActivity = Activity#activity{presence = {Nick, Packet}}, StateData1 = store_user_activity(From, NewActivity, StateData), {next_state, normal_state, StateData1} end; normal_state({route, ToNick, #message{from = From, type = Type, lang = Lang} = Packet}, StateData) -> case decide_fate_message(Packet, From, StateData) of {expulse_sender, Reason} -> ?DEBUG(Reason, []), ErrorText = ?T("It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room"), NewState = expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)), {next_state, normal_state, NewState}; forget_message -> {next_state, normal_state, StateData}; continue_delivery -> case {(StateData#state.config)#config.allow_private_messages, is_user_online(From, StateData) orelse is_subscriber(From, StateData) orelse is_user_allowed_message_nonparticipant(From, StateData)} of {true, true} when Type == groupchat -> ErrText = ?T("It is not allowed to send private messages " "of type \"groupchat\""), Err = xmpp:err_bad_request(ErrText, Lang), ejabberd_router:route_error(Packet, Err); {true, true} -> case find_jids_by_nick(ToNick, StateData) of [] -> ErrText = ?T("Recipient is not in the conference room"), Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); ToJIDs -> SrcIsVisitor = is_visitor(From, StateData), DstIsModerator = is_moderator(hd(ToJIDs), StateData), PmFromVisitors = (StateData#state.config)#config.allow_private_messages_from_visitors, if SrcIsVisitor == false; PmFromVisitors == anyone; (PmFromVisitors == moderators) and DstIsModerator -> {FromNick, _} = get_participant_data(From, StateData), FromNickJID = jid:replace_resource(StateData#state.jid, FromNick), X = #muc_user{}, PrivMsg = xmpp:set_from( xmpp:set_subtag(Packet, X), FromNickJID), lists:foreach( fun(ToJID) -> ejabberd_router:route(xmpp:set_to(PrivMsg, ToJID)) end, ToJIDs); true -> ErrText = ?T("It is not allowed to send private messages"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end end; {true, false} -> ErrText = ?T("Only occupants are allowed to send messages " "to the conference"), Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err); {false, _} -> ErrText = ?T("It is not allowed to send private messages"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end, {next_state, normal_state, StateData} end; normal_state({route, ToNick, #iq{from = From, lang = Lang} = Packet}, #state{config = #config{allow_query_users = AllowQuery}} = StateData) -> try maps:get(jid:tolower(From), StateData#state.users) of #user{nick = FromNick} when AllowQuery orelse ToNick == FromNick -> case find_jid_by_nick(ToNick, StateData) of false -> ErrText = ?T("Recipient is not in the conference room"), Err = xmpp:err_item_not_found(ErrText, Lang), ejabberd_router:route_error(Packet, Err); To -> FromJID = jid:replace_resource(StateData#state.jid, FromNick), case direct_iq_type(Packet) of vcard -> ejabberd_router:route_iq( xmpp:set_from_to(Packet, FromJID, jid:remove_resource(To)), Packet, self()); ping when ToNick == FromNick -> %% Self-ping optimization from XEP-0410 ejabberd_router:route(xmpp:make_iq_result(Packet)); response -> ejabberd_router:route(xmpp:set_from_to(Packet, FromJID, To)); #stanza_error{} = Err -> ejabberd_router:route_error(Packet, Err); _OtherRequest -> ejabberd_router:route_iq( xmpp:set_from_to(Packet, FromJID, To), Packet, self()) end end; _ -> ErrText = ?T("Queries to the conference members are " "not allowed in this room"), Err = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error(Packet, Err) catch _:{badkey, _} -> ErrText = ?T("Only occupants are allowed to send queries " "to the conference"), Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err) end, {next_state, normal_state, StateData}; normal_state(hibernate, StateData) -> case maps:size(StateData#state.users) of 0 -> store_room_no_checks(StateData, []), ?INFO_MSG("Hibernating room ~ts@~ts", [StateData#state.room, StateData#state.host]), {stop, normal, StateData#state{hibernate_timer = hibernating}}; _ -> {next_state, normal_state, StateData} end; normal_state(_Event, StateData) -> {next_state, normal_state, StateData}. handle_event({service_message, Msg}, _StateName, StateData) -> MessagePkt = #message{type = groupchat, body = xmpp:mk_text(Msg)}, send_wrapped_multiple( StateData#state.jid, get_users_and_subscribers_with_node(?NS_MUCSUB_NODES_MESSAGES, StateData), MessagePkt, ?NS_MUCSUB_NODES_MESSAGES, StateData), NSD = add_message_to_history(<<"">>, StateData#state.jid, MessagePkt, StateData), {next_state, normal_state, NSD}; handle_event({destroy, Reason}, _StateName, StateData) -> _ = destroy_room(#muc_destroy{xmlns = ?NS_MUC_OWNER, reason = Reason}, StateData), ?INFO_MSG("Destroyed MUC room ~ts with reason: ~p", [jid:encode(StateData#state.jid), Reason]), add_to_log(room_existence, destroyed, StateData), Conf = StateData#state.config, {stop, shutdown, StateData#state{config = Conf#config{persistent = false}}}; handle_event(destroy, StateName, StateData) -> ?INFO_MSG("Destroyed MUC room ~ts", [jid:encode(StateData#state.jid)]), handle_event({destroy, <<"">>}, StateName, StateData); handle_event({set_affiliations, Affiliations}, StateName, StateData) -> NewStateData = set_affiliations(Affiliations, StateData), {next_state, StateName, NewStateData}; handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. handle_sync_event({get_disco_item, Filter, JID, Lang, Time}, _From, StateName, StateData) -> Len = maps:size(StateData#state.nicks), Reply = case (Filter == all) or (Filter == Len) or ((Filter /= 0) and (Len /= 0)) of true -> get_roomdesc_reply(JID, StateData, get_roomdesc_tail(StateData, Lang)); false -> false end, CurrentTime = erlang:system_time(millisecond), if CurrentTime < Time -> {reply, Reply, StateName, StateData}; true -> {next_state, StateName, StateData} end; %% These two clauses are only for backward compatibility with nodes running old code handle_sync_event({get_disco_item, JID, Lang}, From, StateName, StateData) -> handle_sync_event({get_disco_item, any, JID, Lang}, From, StateName, StateData); handle_sync_event({get_disco_item, Filter, JID, Lang}, From, StateName, StateData) -> handle_sync_event({get_disco_item, Filter, JID, Lang, infinity}, From, StateName, StateData); handle_sync_event(get_config, _From, StateName, StateData) -> {reply, {ok, StateData#state.config}, StateName, StateData}; handle_sync_event(get_state, _From, StateName, StateData) -> {reply, {ok, StateData}, StateName, StateData}; handle_sync_event({change_config, Config}, _From, StateName, StateData) -> {result, undefined, NSD} = change_config(Config, StateData), {reply, {ok, NSD#state.config}, StateName, NSD}; handle_sync_event({change_state, NewStateData}, _From, StateName, _StateData) -> Mod = gen_mod:db_mod(NewStateData#state.server_host, mod_muc), case erlang:function_exported(Mod, get_subscribed_rooms, 3) of true -> ok; _ -> erlang:put(muc_subscribers, NewStateData#state.muc_subscribers#muc_subscribers.subscribers) end, {reply, {ok, NewStateData}, StateName, NewStateData}; handle_sync_event({process_item_change, Item, UJID}, _From, StateName, StateData) -> case process_item_change(Item, StateData, UJID) of {error, _} = Err -> {reply, Err, StateName, StateData}; NSD -> store_room(NSD), {reply, {ok, NSD}, StateName, NSD} end; handle_sync_event(get_subscribers, _From, StateName, StateData) -> JIDs = muc_subscribers_fold( fun(_LBareJID, #subscriber{jid = JID}, Acc) -> [JID | Acc] end, [], StateData#state.muc_subscribers), {reply, {ok, JIDs}, StateName, StateData}; handle_sync_event({muc_subscribe, From, Nick, Nodes}, _From, StateName, StateData) -> IQ = #iq{type = set, id = p1_rand:get_string(), from = From, sub_els = [#muc_subscribe{nick = Nick, events = Nodes}]}, Config = StateData#state.config, CaptchaRequired = Config#config.captcha_protected, PasswordProtected = Config#config.password_protected, TmpConfig = Config#config{captcha_protected = false, password_protected = false}, TmpState = StateData#state{config = TmpConfig}, case process_iq_mucsub(From, IQ, TmpState) of {result, #muc_subscribe{events = NewNodes}, NewState} -> NewConfig = (NewState#state.config)#config{ captcha_protected = CaptchaRequired, password_protected = PasswordProtected}, {reply, {ok, NewNodes}, StateName, NewState#state{config = NewConfig}}; {ignore, NewState} -> NewConfig = (NewState#state.config)#config{ captcha_protected = CaptchaRequired, password_protected = PasswordProtected}, {reply, {error, ?T("Request is ignored")}, NewState#state{config = NewConfig}}; {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; handle_sync_event({muc_unsubscribe, From}, _From, StateName, #state{config = Conf} = StateData) -> IQ = #iq{type = set, id = p1_rand:get_string(), from = From, sub_els = [#muc_unsubscribe{}]}, case process_iq_mucsub(From, IQ, StateData) of {result, _, stop} -> {stop, normal, StateData#state{config = Conf#config{persistent = false}}}; {result, _, NewState} -> {reply, ok, StateName, NewState}; {ignore, NewState} -> {reply, {error, ?T("Request is ignored")}, NewState}; {error, Err} -> {reply, {error, get_error_text(Err)}, StateName, StateData} end; handle_sync_event({is_subscribed, From}, _From, StateName, StateData) -> IsSubs = try muc_subscribers_get( jid:split(From), StateData#state.muc_subscribers) of #subscriber{nick = Nick, nodes = Nodes} -> {true, Nick, Nodes} catch _:{badkey, _} -> false end, {reply, IsSubs, StateName, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. handle_info({process_user_presence, From}, normal_state = _StateName, StateData) -> RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue), RoomQueue = p1_queue:in({presence, From}, StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), {next_state, normal_state, StateData2}; true -> {next_state, normal_state, StateData1} end; handle_info({process_user_message, From}, normal_state = _StateName, StateData) -> RoomQueueEmpty = p1_queue:is_empty(StateData#state.room_queue), RoomQueue = p1_queue:in({message, From}, StateData#state.room_queue), StateData1 = StateData#state{room_queue = RoomQueue}, if RoomQueueEmpty -> StateData2 = prepare_room_queue(StateData1), {next_state, normal_state, StateData2}; true -> {next_state, normal_state, StateData1} end; handle_info(process_room_queue, normal_state = StateName, StateData) -> case p1_queue:out(StateData#state.room_queue) of {{value, {message, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), Packet = Activity#activity.message, NewActivity = Activity#activity{message = undefined}, StateData1 = store_user_activity(From, NewActivity, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), process_groupchat_message(Packet, StateData3); {{value, {presence, From}}, RoomQueue} -> Activity = get_user_activity(From, StateData), {Nick, Packet} = Activity#activity.presence, NewActivity = Activity#activity{presence = undefined}, StateData1 = store_user_activity(From, NewActivity, StateData), StateData2 = StateData1#state{room_queue = RoomQueue}, StateData3 = prepare_room_queue(StateData2), process_presence(Nick, Packet, StateData3); {empty, _} -> {next_state, StateName, StateData} end; handle_info({captcha_succeed, From}, normal_state, StateData) -> NewState = case maps:get(From, StateData#state.robots, passed) of {Nick, Packet} -> Robots = maps:put(From, passed, StateData#state.robots), add_new_user(From, Nick, Packet, StateData#state{robots = Robots}); passed -> StateData end, {next_state, normal_state, NewState}; handle_info({captcha_failed, From}, normal_state, StateData) -> NewState = case maps:get(From, StateData#state.robots, passed) of {_Nick, Packet} -> Robots = maps:remove(From, StateData#state.robots), Txt = ?T("The CAPTCHA verification has failed"), Lang = xmpp:get_lang(Packet), Err = xmpp:err_not_authorized(Txt, Lang), ejabberd_router:route_error(Packet, Err), StateData#state{robots = Robots}; passed -> StateData end, {next_state, normal_state, NewState}; handle_info(shutdown, _StateName, StateData) -> {stop, shutdown, StateData}; handle_info({iq_reply, #iq{type = Type, sub_els = Els}, #iq{from = From, to = To} = IQ}, StateName, StateData) -> ejabberd_router:route( xmpp:set_from_to( IQ#iq{type = Type, sub_els = Els}, To, From)), {next_state, StateName, StateData}; handle_info({iq_reply, timeout, IQ}, StateName, StateData) -> Txt = ?T("Request has timed out"), Err = xmpp:err_recipient_unavailable(Txt, IQ#iq.lang), ejabberd_router:route_error(IQ, Err), {next_state, StateName, StateData}; handle_info(config_reloaded, StateName, StateData) -> Max = mod_muc_opt:history_size(StateData#state.server_host), History1 = StateData#state.history, Q1 = History1#lqueue.queue, Q2 = case p1_queue:len(Q1) of Len when Len > Max -> lqueue_cut(Q1, Len-Max); _ -> Q1 end, History2 = History1#lqueue{queue = Q2, max = Max}, {next_state, StateName, StateData#state{history = History2}}; handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. terminate(Reason, _StateName, #state{server_host = LServer, host = Host, room = Room} = StateData) -> try ?INFO_MSG("Stopping MUC room ~ts@~ts", [Room, Host]), ReasonT = case Reason of shutdown -> ?T("You are being removed from the room " "because of a system shutdown"); _ -> ?T("Room terminates") end, Packet = #presence{ type = unavailable, sub_els = [#muc_user{items = [#muc_item{affiliation = none, reason = ReasonT, role = none}], status_codes = [332,110]}]}, maps:fold( fun(_, #user{nick = Nick, jid = JID}, _) -> case Reason of shutdown -> send_wrapped(jid:replace_resource(StateData#state.jid, Nick), JID, Packet, ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); _ -> ok end, tab_remove_online_user(JID, StateData) end, [], get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), disable_hibernate_timer(StateData), case StateData#state.hibernate_timer of hibernating -> ok; _ -> add_to_log(room_existence, stopped, StateData), case (StateData#state.config)#config.persistent of false -> ejabberd_hooks:run(room_destroyed, LServer, [LServer, Room, Host]); _ -> ok end end catch ?EX_RULE(E, R, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Got exception on room termination:~n** ~ts", [misc:format_exception(2, E, R, StackTrace)]) end. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- -spec route(pid(), stanza()) -> ok. route(Pid, Packet) -> ?DEBUG("Routing to MUC room ~p:~n~ts", [Pid, xmpp:pp(Packet)]), #jid{lresource = Nick} = xmpp:get_to(Packet), p1_fsm:send_event(Pid, {route, Nick, Packet}). -spec process_groupchat_message(message(), state()) -> fsm_next(). process_groupchat_message(#message{from = From, lang = Lang} = Packet, StateData) -> 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} = get_participant_data(From, StateData), #config{moderated = Moderated} = StateData#state.config, AllowedByModerationRules = case {Role == moderator orelse Role == participant orelse not Moderated, IsSubscriber} of {true, _} -> true; {_, true} -> case get_default_role(get_affiliation(From, StateData), StateData) of moderator -> true; participant -> true; _ -> false end; _ -> false end, if AllowedByModerationRules -> Subject = check_subject(Packet), {NewStateData1, IsAllowed} = case Subject of [] -> {StateData, true}; _ -> case can_change_subject(Role, IsSubscriber, StateData) of true -> NSD = StateData#state{subject = Subject, subject_author = FromNick}, store_room(NSD), {NSD, true}; _ -> {StateData, false} end end, case IsAllowed of true -> case ejabberd_hooks:run_fold(muc_filter_message, StateData#state.server_host, Packet, [StateData, FromNick]) of drop -> {next_state, normal_state, StateData}; NewPacket1 -> NewPacket = xmpp:put_meta(xmpp:remove_subtag(NewPacket1, #nick{}), muc_sender_real_jid, From), Node = if Subject == [] -> ?NS_MUCSUB_NODES_MESSAGES; true -> ?NS_MUCSUB_NODES_SUBJECT end, send_wrapped_multiple( jid:replace_resource(StateData#state.jid, FromNick), get_users_and_subscribers_with_node(Node, StateData), NewPacket, Node, NewStateData1), NewStateData2 = case has_body_or_subject(NewPacket) of true -> add_message_to_history(FromNick, From, NewPacket, NewStateData1); false -> NewStateData1 end, {next_state, normal_state, NewStateData2} end; _ -> Err = case (StateData#state.config)#config.allow_change_subj of true -> xmpp:err_forbidden( ?T("Only moderators and participants are " "allowed to change the subject in this " "room"), Lang); _ -> xmpp:err_forbidden( ?T("Only moderators are allowed to change " "the subject in this room"), Lang) end, ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; true -> ErrText = ?T("Visitors are not allowed to send messages " "to all occupants"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end; false -> ErrText = ?T("Only occupants are allowed to send messages " "to the conference"), Err = xmpp:err_not_acceptable(ErrText, Lang), ejabberd_router:route_error(Packet, Err), {next_state, normal_state, StateData} end. -spec process_normal_message(jid(), message(), state()) -> state(). process_normal_message(From, #message{lang = Lang} = Pkt, StateData) -> Action = lists:foldl( fun(_, {error, _} = Err) -> Err; (_, {ok, _} = Result) -> Result; (#muc_user{invites = [_|_] = Invites}, _) -> case check_invitation(From, Invites, Lang, StateData) of ok -> {ok, Invites}; {error, _} = Err -> Err end; (#xdata{type = submit, fields = Fs}, _) -> try {ok, muc_request:decode(Fs)} catch _:{muc_request, Why} -> Txt = muc_request:format_error(Why), {error, xmpp:err_bad_request(Txt, Lang)} end; (_, Acc) -> Acc end, ok, xmpp:get_els(Pkt)), case Action of {ok, [#muc_invite{}|_] = Invitations} -> lists:foldl( fun(Invitation, AccState) -> process_invitation(From, Pkt, Invitation, Lang, AccState) end, StateData, Invitations); {ok, [{role, participant}]} -> process_voice_request(From, Pkt, StateData); {ok, VoiceApproval} -> process_voice_approval(From, Pkt, VoiceApproval, StateData); {error, Err} -> ejabberd_router:route_error(Pkt, Err), StateData; ok -> StateData end. -spec process_invitation(jid(), message(), muc_invite(), binary(), state()) -> state(). process_invitation(From, Pkt, Invitation, Lang, StateData) -> IJID = route_invitation(From, Pkt, Invitation, Lang, StateData), 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), NSD; _ -> StateData end; false -> StateData end. -spec process_voice_request(jid(), message(), state()) -> state(). process_voice_request(From, Pkt, StateData) -> Lang = xmpp:get_lang(Pkt), case (StateData#state.config)#config.allow_voice_requests of true -> MinInterval = (StateData#state.config)#config.voice_request_min_interval, BareFrom = jid:remove_resource(jid:tolower(From)), NowPriority = -erlang:system_time(microsecond), CleanPriority = NowPriority + MinInterval * 1000000, Times = clean_treap(StateData#state.last_voice_request_time, CleanPriority), case treap:lookup(BareFrom, Times) of error -> Times1 = treap:insert(BareFrom, NowPriority, true, Times), NSD = StateData#state{last_voice_request_time = Times1}, send_voice_request(From, Lang, NSD), NSD; {ok, _, _} -> ErrText = ?T("Please, wait for a while before sending " "new voice request"), Err = xmpp:err_resource_constraint(ErrText, Lang), ejabberd_router:route_error(Pkt, Err), StateData#state{last_voice_request_time = Times} end; false -> ErrText = ?T("Voice requests are disabled in this conference"), Err = xmpp:err_forbidden(ErrText, Lang), ejabberd_router:route_error(Pkt, Err), StateData end. -spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state(). process_voice_approval(From, Pkt, VoiceApproval, StateData) -> Lang = xmpp:get_lang(Pkt), case is_moderator(From, StateData) of true -> case lists:keyfind(jid, 1, VoiceApproval) of {_, TargetJid} -> Allow = proplists:get_bool(request_allow, VoiceApproval), case is_visitor(TargetJid, StateData) of true when Allow -> Reason = <<>>, NSD = set_role(TargetJid, participant, StateData), catch send_new_presence( TargetJid, Reason, NSD, StateData), NSD; _ -> StateData end; false -> ErrText = ?T("Failed to extract JID from your voice " "request approval"), Err = xmpp:err_bad_request(ErrText, Lang), ejabberd_router:route_error(Pkt, Err), StateData end; false -> ErrText = ?T("Only moderators can approve voice requests"), Err = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error(Pkt, Err), StateData end. -spec direct_iq_type(iq()) -> vcard | ping | request | response | stanza_error(). direct_iq_type(#iq{type = T, sub_els = SubEls, lang = Lang}) when T == get; T == set -> case SubEls of [El] -> case xmpp:get_ns(El) of ?NS_VCARD when T == get -> vcard; ?NS_PING when T == get -> ping; _ -> request end; [] -> xmpp:err_bad_request(?T("No child elements found"), Lang); [_|_] -> xmpp:err_bad_request(?T("Too many child elements"), Lang) end; direct_iq_type(#iq{}) -> response. %% @doc Check if this non participant can send message to room. %% %% XEP-0045 v1.23: %% 7.9 Sending a Message to All Occupants %% an implementation MAY allow users with certain privileges %% (e.g., a room owner, room admin, or service-level admin) %% to send messages to the room even if those users are not occupants. -spec is_user_allowed_message_nonparticipant(jid(), state()) -> boolean(). is_user_allowed_message_nonparticipant(JID, StateData) -> case get_service_affiliation(JID, StateData) of owner -> true; _ -> false end. %% @doc Get information of this participant, or default values. %% If the JID is not a participant, return values for a service message. -spec get_participant_data(jid(), state()) -> {binary(), role()}. get_participant_data(From, StateData) -> try maps:get(jid:tolower(From), StateData#state.users) of #user{nick = FromNick, role = Role} -> {FromNick, Role} catch _:{badkey, _} -> try muc_subscribers_get(jid:tolower(jid:remove_resource(From)), StateData#state.muc_subscribers) of #subscriber{nick = FromNick} -> {FromNick, none} catch _:{badkey, _} -> {From#jid.luser, moderator} end end. -spec process_presence(binary(), presence(), state()) -> fsm_transition(). process_presence(Nick, #presence{from = From, type = Type0} = Packet0, StateData) -> IsOnline = is_user_online(From, StateData), if Type0 == available; IsOnline and ((Type0 == unavailable) or (Type0 == error)) -> case ejabberd_hooks:run_fold(muc_filter_presence, StateData#state.server_host, Packet0, [StateData, Nick]) of drop -> {next_state, normal_state, StateData}; #presence{} = Packet -> close_room_if_temporary_and_empty( do_process_presence(Nick, Packet, StateData)) end; true -> {next_state, normal_state, StateData} end. -spec do_process_presence(binary(), presence(), state()) -> state(). do_process_presence(Nick, #presence{from = From, type = available, lang = Lang} = Packet, StateData) -> case is_user_online(From, StateData) of false -> add_new_user(From, Nick, Packet, StateData); true -> case is_nick_change(From, Nick, StateData) of true -> case {nick_collision(From, Nick, StateData), mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick), {(StateData#state.config)#config.allow_visitor_nickchange, is_visitor(From, StateData)}} of {_, _, {false, true}} -> Packet1 = Packet#presence{sub_els = [#muc{}]}, ErrText = ?T("Visitors are not allowed to change their " "nicknames in this room"), Err = xmpp:err_not_allowed(ErrText, Lang), ejabberd_router:route_error(Packet1, Err), StateData; {true, _, _} -> Packet1 = Packet#presence{sub_els = [#muc{}]}, ErrText = ?T("That nickname is already in use by another " "occupant"), Err = xmpp:err_conflict(ErrText, Lang), ejabberd_router:route_error(Packet1, Err), StateData; {_, false, _} -> Packet1 = Packet#presence{sub_els = [#muc{}]}, Err = case Nick of <<>> -> xmpp:err_jid_malformed(?T("Nickname can't be empty"), Lang); _ -> xmpp:err_conflict(?T("That nickname is registered" " by another person"), Lang) end, ejabberd_router:route_error(Packet1, Err), StateData; _ -> change_nick(From, Nick, StateData) end; false -> Stanza = maybe_strip_status_from_presence( From, Packet, StateData), NewState = add_user_presence(From, Stanza, StateData), case xmpp:has_subtag(Packet, #muc{}) of true -> send_initial_presences_and_messages( From, Nick, Packet, NewState, StateData); false -> send_new_presence(From, NewState, StateData) end, NewState end end; do_process_presence(Nick, #presence{from = From, type = unavailable} = Packet, StateData) -> NewPacket = case {(StateData#state.config)#config.allow_visitor_status, is_visitor(From, StateData)} of {false, true} -> strip_status(Packet); _ -> Packet end, NewState = add_user_presence_un(From, NewPacket, StateData), case maps:get(Nick, StateData#state.nicks, []) of [_, _ | _] -> Aff = get_affiliation(From, StateData), Item = #muc_item{affiliation = Aff, role = none, jid = From}, Pres = xmpp:set_subtag( Packet, #muc_user{items = [Item], status_codes = [110]}), send_wrapped(jid:replace_resource(StateData#state.jid, Nick), From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); _ -> send_new_presence(From, NewState, StateData) end, Reason = xmpp:get_text(NewPacket#presence.status), remove_online_user(From, NewState, Reason); do_process_presence(_Nick, #presence{from = From, type = error, lang = Lang} = Packet, StateData) -> ErrorText = ?T("It is not allowed to send error messages to the" " room. The participant (~s) has sent an error " "message (~s) and got kicked from the room"), expulse_participant(Packet, From, StateData, translate:translate(Lang, ErrorText)). -spec maybe_strip_status_from_presence(jid(), presence(), state()) -> presence(). maybe_strip_status_from_presence(From, Packet, StateData) -> case {(StateData#state.config)#config.allow_visitor_status, is_visitor(From, StateData)} of {false, true} -> strip_status(Packet); _Allowed -> Packet end. -spec close_room_if_temporary_and_empty(state()) -> fsm_transition(). close_room_if_temporary_and_empty(StateData1) -> case not (StateData1#state.config)#config.persistent andalso maps:size(StateData1#state.users) == 0 andalso muc_subscribers_size(StateData1#state.muc_subscribers) == 0 of true -> ?INFO_MSG("Destroyed MUC room ~ts because it's temporary " "and empty", [jid:encode(StateData1#state.jid)]), add_to_log(room_existence, destroyed, StateData1), forget_room(StateData1), {stop, normal, StateData1}; _ -> {next_state, normal_state, StateData1} end. -spec get_users_and_subscribers(state()) -> users(). get_users_and_subscribers(StateData) -> get_users_and_subscribers_aux( StateData#state.muc_subscribers#muc_subscribers.subscribers, StateData). -spec get_users_and_subscribers_with_node(binary(), state()) -> users(). get_users_and_subscribers_with_node(Node, StateData) -> get_users_and_subscribers_aux( muc_subscribers_get_by_node(Node, StateData#state.muc_subscribers), StateData). get_users_and_subscribers_aux(Subscribers, StateData) -> OnlineSubscribers = maps: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), maps:fold( fun(LBareJID, #subscriber{nick = Nick}, Acc) -> case ?SETS:is_element(LBareJID, OnlineSubscribers) of false -> maps:put(LBareJID, #user{jid = jid:make(LBareJID), nick = Nick, role = none, last_presence = undefined}, Acc); true -> Acc end end, StateData#state.users, Subscribers). -spec is_user_online(jid(), state()) -> boolean(). is_user_online(JID, StateData) -> LJID = jid:tolower(JID), maps:is_key(LJID, StateData#state.users). -spec is_subscriber(jid(), state()) -> boolean(). is_subscriber(JID, StateData) -> LJID = jid:tolower(jid:remove_resource(JID)), muc_subscribers_is_key(LJID, StateData#state.muc_subscribers). %% Check if the user is occupant of the room, or at least is an admin or owner. -spec is_occupant_or_admin(jid(), state()) -> boolean(). is_occupant_or_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FRole = get_role(JID, StateData), case FRole /= none orelse FAffiliation == member orelse FAffiliation == admin orelse FAffiliation == owner of true -> true; _ -> false end. %% Check if the user is an admin or owner. -spec is_admin(jid(), state()) -> boolean(). is_admin(JID, StateData) -> FAffiliation = get_affiliation(JID, StateData), FAffiliation == admin orelse FAffiliation == owner. %% Decide the fate of the message and its sender %% Returns: continue_delivery | forget_message | {expulse_sender, Reason} -spec decide_fate_message(message(), jid(), state()) -> continue_delivery | forget_message | {expulse_sender, binary()}. decide_fate_message(#message{type = error} = Msg, From, StateData) -> Err = xmpp:get_error(Msg), PD = case check_error_kick(Err) of %% If this is an error stanza and its condition matches a criteria true -> Reason = str:format("This participant is considered a ghost " "and is expulsed: ~s", [jid:encode(From)]), {expulse_sender, Reason}; false -> continue_delivery end, case PD of {expulse_sender, R} -> case is_user_online(From, StateData) of true -> {expulse_sender, R}; false -> forget_message end; Other -> Other end; decide_fate_message(_, _, _) -> continue_delivery. %% Check if the elements of this error stanza indicate %% that the sender is a dead participant. %% If so, return true to kick the participant. -spec check_error_kick(stanza_error()) -> boolean(). check_error_kick(#stanza_error{reason = Reason}) -> case Reason of #gone{} -> true; 'internal-server-error' -> true; 'item-not-found' -> true; 'jid-malformed' -> true; 'recipient-unavailable' -> true; #redirect{} -> true; 'remote-server-not-found' -> true; 'remote-server-timeout' -> true; 'service-unavailable' -> true; _ -> false end; check_error_kick(undefined) -> false. -spec get_error_condition(stanza_error()) -> string(). get_error_condition(#stanza_error{reason = Reason}) -> case Reason of #gone{} -> "gone"; #redirect{} -> "redirect"; Atom -> atom_to_list(Atom) end; get_error_condition(undefined) -> "undefined". -spec get_error_text(stanza_error()) -> binary(). get_error_text(#stanza_error{text = Txt}) -> xmpp:get_text(Txt). -spec make_reason(stanza(), jid(), state(), binary()) -> binary(). make_reason(Packet, From, StateData, Reason1) -> #user{nick = FromNick} = maps:get(jid:tolower(From), StateData#state.users), Condition = get_error_condition(xmpp:get_error(Packet)), Reason2 = unicode:characters_to_list(Reason1), str:format(Reason2, [FromNick, Condition]). -spec expulse_participant(stanza(), jid(), state(), binary()) -> state(). expulse_participant(Packet, From, StateData, Reason1) -> Reason2 = make_reason(Packet, From, StateData, Reason1), NewState = add_user_presence_un(From, #presence{type = unavailable, status = xmpp:mk_text(Reason2)}, StateData), LJID = jid:tolower(From), #user{nick = Nick} = maps:get(LJID, StateData#state.users), case maps:get(Nick, StateData#state.nicks, []) of [_, _ | _] -> Aff = get_affiliation(From, StateData), Item = #muc_item{affiliation = Aff, role = none, jid = From}, Pres = xmpp:set_subtag( Packet, #muc_user{items = [Item], status_codes = [110]}), send_wrapped(jid:replace_resource(StateData#state.jid, Nick), From, Pres, ?NS_MUCSUB_NODES_PRESENCE, StateData); _ -> send_new_presence(From, NewState, StateData) end, remove_online_user(From, NewState). -spec set_affiliation(jid(), affiliation(), state()) -> state(). set_affiliation(JID, Affiliation, StateData) -> set_affiliation(JID, Affiliation, StateData, <<"">>). -spec set_affiliation(jid(), affiliation(), state(), binary()) -> state(). set_affiliation(JID, Affiliation, #state{config = #config{persistent = false}} = StateData, Reason) -> set_affiliation_fallback(JID, Affiliation, StateData, Reason); set_affiliation(JID, Affiliation, StateData, Reason) -> ServerHost = StateData#state.server_host, Room = StateData#state.room, Host = StateData#state.host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:set_affiliation(ServerHost, Room, Host, JID, Affiliation, Reason) of ok -> StateData; {error, _} -> set_affiliation_fallback(JID, Affiliation, StateData, Reason) end. -spec set_affiliation_fallback(jid(), affiliation(), state(), binary()) -> state(). set_affiliation_fallback(JID, Affiliation, StateData, Reason) -> LJID = jid:remove_resource(jid:tolower(JID)), Affiliations = case Affiliation of none -> maps:remove(LJID, StateData#state.affiliations); _ -> maps:put(LJID, {Affiliation, Reason}, StateData#state.affiliations) end, StateData#state{affiliations = Affiliations}. -spec set_affiliations(affiliations(), state()) -> state(). set_affiliations(Affiliations, #state{config = #config{persistent = false}} = StateData) -> set_affiliations_fallback(Affiliations, StateData); set_affiliations(Affiliations, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:set_affiliations(ServerHost, Room, Host, Affiliations) of ok -> StateData; {error, _} -> set_affiliations_fallback(Affiliations, StateData) end. -spec set_affiliations_fallback(affiliations(), state()) -> state(). set_affiliations_fallback(Affiliations, StateData) -> StateData#state{affiliations = Affiliations}. -spec get_affiliation(ljid() | jid(), state()) -> affiliation(). get_affiliation(#jid{} = JID, StateData) -> case get_service_affiliation(JID, StateData) of owner -> owner; none -> case do_get_affiliation(JID, StateData) of {Affiliation, _Reason} -> Affiliation; Affiliation -> Affiliation end end; get_affiliation(LJID, StateData) -> get_affiliation(jid:make(LJID), StateData). -spec do_get_affiliation(jid(), state()) -> affiliation() | {affiliation(), binary()}. do_get_affiliation(JID, #state{config = #config{persistent = false}} = StateData) -> do_get_affiliation_fallback(JID, StateData); do_get_affiliation(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, LServer = JID#jid.lserver, LUser = JID#jid.luser, ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliation(ServerHost, Room, Host, LUser, LServer) of {error, _} -> do_get_affiliation_fallback(JID, StateData); {ok, Affiliation} -> Affiliation end. -spec do_get_affiliation_fallback(jid(), state()) -> affiliation() | {affiliation(), binary()}. do_get_affiliation_fallback(JID, StateData) -> LJID = jid:tolower(JID), try maps:get(LJID, StateData#state.affiliations) catch _:{badkey, _} -> BareLJID = jid:remove_resource(LJID), try maps:get(BareLJID, StateData#state.affiliations) catch _:{badkey, _} -> DomainLJID = setelement(1, LJID, <<"">>), try maps:get(DomainLJID, StateData#state.affiliations) catch _:{badkey, _} -> DomainBareLJID = jid:remove_resource(DomainLJID), try maps:get(DomainBareLJID, StateData#state.affiliations) catch _:{badkey, _} -> none end end end end. -spec get_affiliations(state()) -> affiliations(). get_affiliations(#state{config = #config{persistent = false}} = StateData) -> get_affiliations_callback(StateData); get_affiliations(StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:get_affiliations(ServerHost, Room, Host) of {error, _} -> get_affiliations_callback(StateData); {ok, Affiliations} -> Affiliations end. -spec get_affiliations_callback(state()) -> affiliations(). get_affiliations_callback(StateData) -> StateData#state.affiliations. -spec get_service_affiliation(jid(), state()) -> owner | none. get_service_affiliation(JID, StateData) -> {_AccessRoute, _AccessCreate, AccessAdmin, _AccessPersistent, _AccessMam} = StateData#state.access, case acl:match_rule(StateData#state.server_host, AccessAdmin, JID) of allow -> owner; _ -> none end. -spec set_role(jid(), role(), state()) -> state(). set_role(JID, Role, StateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> maps:fold(fun (J, _, Js) -> case J of {U, S, _} -> [J | Js]; _ -> Js end end, [], StateData#state.users); _ -> case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, {Users, Nicks} = case Role of none -> lists:foldl( fun (J, {Us, Ns}) -> NewNs = try maps:get(J, Us) of #user{nick = Nick} -> maps:remove(Nick, Ns) catch _:{badkey, _} -> Ns end, {maps:remove(J, Us), NewNs} end, {StateData#state.users, StateData#state.nicks}, LJIDs); _ -> {lists:foldl( fun (J, Us) -> User = maps:get(J, Us), if User#user.last_presence == undefined -> Us; true -> maps:put(J, User#user{role = Role}, Us) end end, StateData#state.users, LJIDs), StateData#state.nicks} end, StateData#state{users = Users, nicks = Nicks}. -spec get_role(jid(), state()) -> role(). get_role(JID, StateData) -> LJID = jid:tolower(JID), try maps:get(LJID, StateData#state.users) of #user{role = Role} -> Role catch _:{badkey, _} -> none end. -spec get_default_role(affiliation(), state()) -> role(). get_default_role(Affiliation, StateData) -> case Affiliation of owner -> moderator; admin -> moderator; member -> participant; outcast -> none; none -> case (StateData#state.config)#config.members_only of true -> none; _ -> case (StateData#state.config)#config.members_by_default of true -> participant; _ -> visitor end end end. -spec is_visitor(jid(), state()) -> boolean(). is_visitor(Jid, StateData) -> get_role(Jid, StateData) =:= visitor. -spec is_moderator(jid(), state()) -> boolean(). is_moderator(Jid, StateData) -> get_role(Jid, StateData) =:= moderator. -spec get_max_users(state()) -> non_neg_integer(). get_max_users(StateData) -> MaxUsers = (StateData#state.config)#config.max_users, ServiceMaxUsers = get_service_max_users(StateData), if MaxUsers =< ServiceMaxUsers -> MaxUsers; true -> ServiceMaxUsers end. -spec get_service_max_users(state()) -> pos_integer(). get_service_max_users(StateData) -> mod_muc_opt:max_users(StateData#state.server_host). -spec get_max_users_admin_threshold(state()) -> pos_integer(). get_max_users_admin_threshold(StateData) -> mod_muc_opt:max_users_admin_threshold(StateData#state.server_host). -spec room_queue_new(binary(), ejabberd_shaper:shaper(), _) -> p1_queue:queue({message | presence, jid()}) | undefined. room_queue_new(ServerHost, Shaper, QueueType) -> HaveRoomShaper = Shaper /= none, HaveMessageShaper = mod_muc_opt:user_message_shaper(ServerHost) /= none, HavePresenceShaper = mod_muc_opt:user_presence_shaper(ServerHost) /= none, HaveMinMessageInterval = mod_muc_opt:min_message_interval(ServerHost) /= 0, HaveMinPresenceInterval = mod_muc_opt:min_presence_interval(ServerHost) /= 0, if HaveRoomShaper or HaveMessageShaper or HavePresenceShaper or HaveMinMessageInterval or HaveMinPresenceInterval -> p1_queue:new(QueueType); true -> undefined end. -spec get_user_activity(jid(), state()) -> #activity{}. get_user_activity(JID, StateData) -> case treap:lookup(jid:tolower(JID), StateData#state.activity) of {ok, _P, A} -> A; error -> MessageShaper = ejabberd_shaper:new(mod_muc_opt:user_message_shaper(StateData#state.server_host)), PresenceShaper = ejabberd_shaper:new(mod_muc_opt:user_presence_shaper(StateData#state.server_host)), #activity{message_shaper = MessageShaper, presence_shaper = PresenceShaper} end. -spec store_user_activity(jid(), #activity{}, state()) -> state(). store_user_activity(JID, UserActivity, StateData) -> MinMessageInterval = trunc(mod_muc_opt:min_message_interval(StateData#state.server_host) * 1000), MinPresenceInterval = trunc(mod_muc_opt:min_presence_interval(StateData#state.server_host) * 1000), Key = jid:tolower(JID), Now = erlang:system_time(microsecond), Activity1 = clean_treap(StateData#state.activity, {1, -Now}), Activity = case treap:lookup(Key, Activity1) of {ok, _P, _A} -> treap:delete(Key, Activity1); error -> Activity1 end, StateData1 = case MinMessageInterval == 0 andalso MinPresenceInterval == 0 andalso UserActivity#activity.message_shaper == none andalso UserActivity#activity.presence_shaper == none andalso UserActivity#activity.message == undefined andalso UserActivity#activity.presence == undefined of true -> StateData#state{activity = Activity}; false -> case UserActivity#activity.message == undefined andalso UserActivity#activity.presence == undefined of true -> {_, MessageShaperInterval} = ejabberd_shaper:update(UserActivity#activity.message_shaper, 100000), {_, PresenceShaperInterval} = ejabberd_shaper:update(UserActivity#activity.presence_shaper, 100000), Delay = lists:max([MessageShaperInterval, PresenceShaperInterval, MinMessageInterval, MinPresenceInterval]) * 1000, Priority = {1, -(Now + Delay)}, StateData#state{activity = treap:insert(Key, Priority, UserActivity, Activity)}; false -> Priority = {0, 0}, StateData#state{activity = treap:insert(Key, Priority, UserActivity, Activity)} end end, reset_hibernate_timer(StateData1). -spec clean_treap(treap:treap(), integer() | {1, integer()}) -> treap:treap(). clean_treap(Treap, CleanPriority) -> case treap:is_empty(Treap) of true -> Treap; false -> {_Key, Priority, _Value} = treap:get_root(Treap), if Priority > CleanPriority -> clean_treap(treap:delete_root(Treap), CleanPriority); true -> Treap end end. -spec prepare_room_queue(state()) -> state(). prepare_room_queue(StateData) -> case p1_queue:out(StateData#state.room_queue) of {{value, {message, From}}, _RoomQueue} -> Activity = get_user_activity(From, StateData), Packet = Activity#activity.message, Size = element_size(Packet), {RoomShaper, RoomShaperInterval} = ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; {{value, {presence, From}}, _RoomQueue} -> Activity = get_user_activity(From, StateData), {_Nick, Packet} = Activity#activity.presence, Size = element_size(Packet), {RoomShaper, RoomShaperInterval} = ejabberd_shaper:update(StateData#state.room_shaper, Size), erlang:send_after(RoomShaperInterval, self(), process_room_queue), StateData#state{room_shaper = RoomShaper}; {empty, _} -> StateData end. -spec update_online_user(jid(), #user{}, state()) -> state(). update_online_user(JID, #user{nick = Nick} = User, StateData) -> LJID = jid:tolower(JID), add_to_log(join, Nick, StateData), Nicks1 = try maps:get(LJID, StateData#state.users) of #user{nick = OldNick} -> case lists:delete( LJID, maps:get(OldNick, StateData#state.nicks)) of [] -> maps:remove(OldNick, StateData#state.nicks); LJIDs -> maps:put(OldNick, LJIDs, StateData#state.nicks) end catch _:{badkey, _} -> StateData#state.nicks end, Nicks = maps:update_with(Nick, fun (LJIDs) -> [LJID|LJIDs -- [LJID]] end, [LJID], Nicks1), Users = maps:update_with(LJID, fun(U) -> U#user{nick = Nick} end, User, StateData#state.users), NewStateData = StateData#state{users = Users, nicks = Nicks}, case {maps:get(LJID, StateData#state.users, error), maps:get(LJID, NewStateData#state.users, error)} of {#user{nick = Old}, #user{nick = New}} when Old /= New -> send_nick_changing(JID, Old, NewStateData, true, true); _ -> ok end, NewStateData. -spec set_subscriber(jid(), binary(), [binary()], state()) -> state(). set_subscriber(JID, Nick, Nodes, #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(JID), LBareJID = jid:tolower(BareJID), MUCSubscribers = muc_subscribers_put( #subscriber{jid = BareJID, nick = Nick, nodes = Nodes}, StateData#state.muc_subscribers), NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, store_room(NewStateData, [{add_subscription, BareJID, Nick, Nodes}]), case not muc_subscribers_is_key(LBareJID, StateData#state.muc_subscribers) of true -> send_subscriptions_change_notifications(BareJID, Nick, subscribe, NewStateData), ejabberd_hooks:run(muc_subscribed, ServerHost, [ServerHost, Room, Host, BareJID]); _ -> ok end, NewStateData. -spec add_online_user(jid(), binary(), role(), state()) -> state(). add_online_user(JID, Nick, Role, StateData) -> tab_add_online_user(JID, StateData), User = #user{jid = JID, nick = Nick, role = Role}, reset_hibernate_timer(update_online_user(JID, User, StateData)). -spec remove_online_user(jid(), state()) -> state(). remove_online_user(JID, StateData) -> remove_online_user(JID, StateData, <<"">>). -spec remove_online_user(jid(), state(), binary()) -> state(). remove_online_user(JID, StateData, Reason) -> LJID = jid:tolower(JID), #user{nick = Nick} = maps:get(LJID, StateData#state.users), add_to_log(leave, {Nick, Reason}, StateData), tab_remove_online_user(JID, StateData), Users = maps:remove(LJID, StateData#state.users), Nicks = try maps:get(Nick, StateData#state.nicks) of [LJID] -> maps:remove(Nick, StateData#state.nicks); U -> maps:put(Nick, U -- [LJID], StateData#state.nicks) catch _:{badkey, _} -> StateData#state.nicks end, reset_hibernate_timer(StateData#state{users = Users, nicks = Nicks}). -spec filter_presence(presence()) -> presence(). filter_presence(Presence) -> Els = lists:filter( fun(El) -> XMLNS = xmpp:get_ns(El), case catch binary:part(XMLNS, 0, size(?NS_MUC)) of ?NS_MUC -> false; _ -> XMLNS /= ?NS_HATS end end, xmpp:get_els(Presence)), xmpp:set_els(Presence, Els). -spec strip_status(presence()) -> presence(). strip_status(Presence) -> Presence#presence{status = []}. -spec add_user_presence(jid(), presence(), state()) -> state(). add_user_presence(JID, Presence, StateData) -> LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = maps:update_with(LJID, fun (#user{} = User) -> User#user{last_presence = FPresence} end, StateData#state.users), StateData#state{users = Users}. -spec add_user_presence_un(jid(), presence(), state()) -> state(). add_user_presence_un(JID, Presence, StateData) -> LJID = jid:tolower(JID), FPresence = filter_presence(Presence), Users = maps:update_with(LJID, fun (#user{} = User) -> User#user{last_presence = FPresence, role = none} end, StateData#state.users), StateData#state{users = Users}. %% Find and return a list of the full JIDs of the users of Nick. %% Return jid record. -spec find_jids_by_nick(binary(), state()) -> [jid()]. find_jids_by_nick(Nick, StateData) -> Users = case maps:get(Nick, StateData#state.nicks, []) of [] -> muc_subscribers_get_by_nick( Nick, StateData#state.muc_subscribers); Us -> Us end, [jid:make(LJID) || LJID <- Users]. %% Find and return the full JID of the user of Nick with %% highest-priority presence. Return jid record. -spec find_jid_by_nick(binary(), state()) -> jid() | false. find_jid_by_nick(Nick, StateData) -> try maps:get(Nick, StateData#state.nicks) of [User] -> jid:make(User); [FirstUser | Users] -> #user{last_presence = FirstPresence} = maps:get(FirstUser, StateData#state.users), {LJID, _} = lists:foldl( fun(Compare, {HighestUser, HighestPresence}) -> #user{last_presence = P1} = maps:get(Compare, StateData#state.users), case higher_presence(P1, HighestPresence) of true -> {Compare, P1}; false -> {HighestUser, HighestPresence} end end, {FirstUser, FirstPresence}, Users), jid:make(LJID) catch _:{badkey, _} -> false end. -spec higher_presence(undefined | presence(), undefined | presence()) -> boolean(). higher_presence(Pres1, Pres2) when Pres1 /= undefined, Pres2 /= undefined -> Pri1 = get_priority_from_presence(Pres1), Pri2 = get_priority_from_presence(Pres2), Pri1 > Pri2; higher_presence(Pres1, Pres2) -> Pres1 > Pres2. -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of undefined -> 0; _ -> Prio end. -spec find_nick_by_jid(jid(), state()) -> binary(). find_nick_by_jid(JID, StateData) -> LJID = jid:tolower(JID), #user{nick = Nick} = maps:get(LJID, StateData#state.users), Nick. -spec is_nick_change(jid(), binary(), state()) -> boolean(). is_nick_change(JID, Nick, StateData) -> LJID = jid:tolower(JID), case Nick of <<"">> -> false; _ -> #user{nick = OldNick} = maps:get(LJID, StateData#state.users), Nick /= OldNick end. -spec nick_collision(jid(), binary(), state()) -> boolean(). nick_collision(User, Nick, StateData) -> UserOfNick = case find_jid_by_nick(Nick, StateData) of false -> case muc_subscribers_get_by_nick(Nick, StateData#state.muc_subscribers) of [J] -> J; [] -> false end; J -> J end, (UserOfNick /= false andalso jid:remove_resource(jid:tolower(UserOfNick)) /= jid:remove_resource(jid:tolower(User))). -spec add_new_user(jid(), binary(), presence(), state()) -> state(); (jid(), binary(), iq(), state()) -> {error, stanza_error()} | {ignore, state()} | {result, muc_subscribe(), state()}. add_new_user(From, Nick, Packet, StateData) -> Lang = xmpp:get_lang(Packet), MaxUsers = get_max_users(StateData), MaxAdminUsers = MaxUsers + get_max_users_admin_threshold(StateData), NUsers = maps:size(StateData#state.users), Affiliation = get_affiliation(From, StateData), ServiceAffiliation = get_service_affiliation(From, StateData), NConferences = tab_count_user(From, StateData), MaxConferences = mod_muc_opt:max_user_conferences(StateData#state.server_host), Collision = nick_collision(From, Nick, StateData), IsSubscribeRequest = not is_record(Packet, presence), case {(ServiceAffiliation == owner orelse ((Affiliation == admin orelse Affiliation == owner) andalso NUsers < MaxAdminUsers) orelse NUsers < MaxUsers) andalso NConferences < MaxConferences, Collision, mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick), get_default_role(Affiliation, StateData)} of {false, _, _, _} when NUsers >= MaxUsers orelse NUsers >= MaxAdminUsers -> Txt = ?T("Too many users in this conference"), Err = xmpp:err_resource_constraint(Txt, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {false, _, _, _} when NConferences >= MaxConferences -> Txt = ?T("You have joined too many conferences"), Err = xmpp:err_resource_constraint(Txt, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {false, _, _, _} -> Err = xmpp:err_service_unavailable(), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {_, _, _, none} -> Err = case Affiliation of outcast -> ErrText = ?T("You have been banned from this room"), xmpp:err_forbidden(ErrText, Lang); _ -> ErrText = ?T("Membership is required to enter this room"), xmpp:err_registration_required(ErrText, Lang) end, if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {_, true, _, _} -> ErrText = ?T("That nickname is already in use by another occupant"), Err = xmpp:err_conflict(ErrText, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {_, _, false, _} -> Err = case Nick of <<>> -> xmpp:err_jid_malformed(?T("Nickname can't be empty"), Lang); _ -> xmpp:err_conflict(?T("That nickname is registered" " by another person"), Lang) end, if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; {_, _, _, Role} -> case check_password(ServiceAffiliation, Affiliation, Packet, From, StateData) of true -> Nodes = get_subscription_nodes(Packet), NewStateData = if not IsSubscribeRequest -> NewState = add_user_presence( From, Packet, add_online_user(From, Nick, Role, StateData)), send_initial_presences_and_messages( From, Nick, Packet, NewState, StateData), NewState; true -> set_subscriber(From, Nick, Nodes, StateData) end, ResultState = case NewStateData#state.just_created of true -> NewStateData#state{just_created = erlang:system_time(microsecond)}; _ -> Robots = maps:remove(From, StateData#state.robots), NewStateData#state{robots = Robots} end, if not IsSubscribeRequest -> ResultState; true -> {result, subscribe_result(Packet), ResultState} end; need_password -> ErrText = ?T("A password is required to enter this room"), Err = xmpp:err_not_authorized(ErrText, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; captcha_required -> SID = xmpp:get_id(Packet), RoomJID = StateData#state.jid, To = jid:replace_resource(RoomJID, Nick), Limiter = {From#jid.luser, From#jid.lserver}, case ejabberd_captcha:create_captcha(SID, RoomJID, To, Lang, Limiter, From) of {ok, ID, Body, CaptchaEls} -> MsgPkt = #message{from = RoomJID, to = From, id = ID, body = Body, sub_els = CaptchaEls}, Robots = maps:put(From, {Nick, Packet}, StateData#state.robots), ejabberd_router:route(MsgPkt), NewState = StateData#state{robots = Robots}, if not IsSubscribeRequest -> NewState; true -> {ignore, NewState} end; {error, limit} -> ErrText = ?T("Too many CAPTCHA requests"), Err = xmpp:err_resource_constraint(ErrText, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end; _ -> ErrText = ?T("Unable to generate a CAPTCHA"), Err = xmpp:err_internal_server_error(ErrText, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end end; _ -> ErrText = ?T("Incorrect password"), Err = xmpp:err_not_authorized(ErrText, Lang), if not IsSubscribeRequest -> ejabberd_router:route_error(Packet, Err), StateData; true -> {error, Err} end end end. -spec check_password(affiliation(), affiliation(), presence() | iq(), jid(), state()) -> boolean() | need_password | captcha_required. check_password(owner, _Affiliation, _Packet, _From, _StateData) -> %% Don't check pass if user is owner in MUC service (access_admin option) true; check_password(_ServiceAffiliation, Affiliation, Packet, From, StateData) -> case (StateData#state.config)#config.password_protected of false -> check_captcha(Affiliation, From, StateData); true -> Pass = extract_password(Packet), case Pass of false -> need_password; _ -> case (StateData#state.config)#config.password of Pass -> true; _ -> false end end end. -spec check_captcha(affiliation(), jid(), state()) -> true | captcha_required. check_captcha(Affiliation, From, StateData) -> case (StateData#state.config)#config.captcha_protected andalso ejabberd_captcha:is_feature_available() of true when Affiliation == none -> case maps:get(From, StateData#state.robots, error) of passed -> true; _ -> WList = (StateData#state.config)#config.captcha_whitelist, #jid{luser = U, lserver = S, lresource = R} = From, case (?SETS):is_element({U, S, R}, WList) of true -> true; false -> case (?SETS):is_element({U, S, <<"">>}, WList) of true -> true; false -> case (?SETS):is_element({<<"">>, S, <<"">>}, WList) of true -> true; false -> captcha_required end end end end; _ -> true end. -spec extract_password(presence() | iq()) -> binary() | false. extract_password(#presence{} = Pres) -> case xmpp:get_subtag(Pres, #muc{}) of #muc{password = Password} when is_binary(Password) -> Password; _ -> false end; extract_password(#iq{} = IQ) -> case xmpp:get_subtag(IQ, #muc_subscribe{}) of #muc_subscribe{password = Password} when Password /= <<"">> -> Password; _ -> false end. -spec get_history(binary(), stanza(), state()) -> [lqueue_elem()]. get_history(Nick, Packet, #state{history = History}) -> case xmpp:get_subtag(Packet, #muc{}) of #muc{history = #muc_history{} = MUCHistory} -> Now = erlang:timestamp(), Q = History#lqueue.queue, filter_history(Q, Now, Nick, MUCHistory); _ -> p1_queue:to_list(History#lqueue.queue) end. -spec filter_history(p1_queue:queue(lqueue_elem()), erlang:timestamp(), binary(), muc_history()) -> [lqueue_elem()]. filter_history(Queue, Now, Nick, #muc_history{since = Since, seconds = Seconds, maxstanzas = MaxStanzas, maxchars = MaxChars}) -> {History, _, _} = lists:foldr( fun({_, _, _, TimeStamp, Size} = Elem, {Elems, NumStanzas, NumChars} = Acc) -> NowDiff = timer:now_diff(Now, TimeStamp) div 1000000, Chars = Size + byte_size(Nick) + 1, if (NumStanzas < MaxStanzas) andalso (TimeStamp > Since) andalso (NowDiff =< Seconds) andalso (NumChars + Chars =< MaxChars) -> {[Elem|Elems], NumStanzas + 1, NumChars + Chars}; true -> Acc end end, {[], 0, 0}, p1_queue:to_list(Queue)), History. -spec is_room_overcrowded(state()) -> boolean(). is_room_overcrowded(StateData) -> MaxUsersPresence = mod_muc_opt:max_users_presence(StateData#state.server_host), maps:size(StateData#state.users) > MaxUsersPresence. -spec presence_broadcast_allowed(jid(), state()) -> boolean(). presence_broadcast_allowed(JID, StateData) -> Role = get_role(JID, StateData), lists:member(Role, (StateData#state.config)#config.presence_broadcast). -spec send_initial_presences_and_messages( jid(), binary(), presence(), state(), state()) -> ok. send_initial_presences_and_messages(From, Nick, Presence, NewState, OldState) -> advertise_entity_capabilities(From, NewState), send_existing_presences(From, NewState), send_self_presence(From, NewState, OldState), History = get_history(Nick, Presence, NewState), send_history(From, History, NewState), send_subject(From, OldState). -spec advertise_entity_capabilities(jid(), state()) -> ok. advertise_entity_capabilities(JID, State) -> AvatarHash = (State#state.config)#config.vcard_xupdate, DiscoInfo = make_disco_info(JID, State), Extras = iq_disco_info_extras(<<"en">>, State, true), DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, DiscoHash = mod_caps:compute_disco_hash(DiscoInfo1, sha), Els1 = [#caps{hash = <<"sha-1">>, node = ejabberd_config:get_uri(), version = DiscoHash}], Els2 = if is_binary(AvatarHash) -> [#vcard_xupdate{hash = AvatarHash}|Els1]; true -> Els1 end, ejabberd_router:route(#presence{from = State#state.jid, to = JID, id = p1_rand:get_string(), sub_els = Els2}). -spec send_self_presence(jid(), state(), state()) -> ok. send_self_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, true, StateData, OldStateData). -spec send_update_presence(jid(), state(), state()) -> ok. send_update_presence(JID, StateData, OldStateData) -> send_update_presence(JID, <<"">>, StateData, OldStateData). -spec send_update_presence(jid(), binary(), state(), state()) -> ok. send_update_presence(JID, Reason, StateData, OldStateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_update_presence1(JID, Reason, StateData, OldStateData) end. -spec send_update_presence1(jid(), binary(), state(), state()) -> ok. send_update_presence1(JID, Reason, StateData, OldStateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> maps:fold(fun (J, _, Js) -> case J of {U, S, _} -> [J | Js]; _ -> Js end end, [], StateData#state.users); _ -> case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, lists:foreach(fun (J) -> send_new_presence(J, Reason, false, StateData, OldStateData) end, LJIDs). -spec send_new_presence(jid(), state(), state()) -> ok. send_new_presence(NJID, StateData, OldStateData) -> send_new_presence(NJID, <<"">>, false, StateData, OldStateData). -spec send_new_presence(jid(), binary(), state(), state()) -> ok. send_new_presence(NJID, Reason, StateData, OldStateData) -> send_new_presence(NJID, Reason, false, StateData, OldStateData). -spec is_ra_changed(jid(), boolean(), state(), state()) -> boolean(). is_ra_changed(_, _IsInitialPresence = true, _, _) -> false; is_ra_changed(JID, _IsInitialPresence = false, NewStateData, OldStateData) -> NewRole = get_role(JID, NewStateData), NewAff = get_affiliation(JID, NewStateData), OldRole = get_role(JID, OldStateData), OldAff = get_affiliation(JID, OldStateData), if (NewRole == none) and (NewAff == OldAff) -> %% A user is leaving the room; false; true -> (NewRole /= OldRole) or (NewAff /= OldAff) end. -spec send_new_presence(jid(), binary(), boolean(), state(), state()) -> ok. send_new_presence(NJID, Reason, IsInitialPresence, StateData, OldStateData) -> LNJID = jid:tolower(NJID), #user{nick = Nick} = maps:get(LNJID, StateData#state.users), LJID = find_jid_by_nick(Nick, StateData), #user{jid = RealJID, role = Role0, last_presence = Presence0} = UserInfo = maps:get(jid:tolower(LJID), StateData#state.users), {Role1, Presence1} = case (presence_broadcast_allowed(NJID, StateData) orelse presence_broadcast_allowed(NJID, OldStateData)) of true -> {Role0, Presence0}; false -> {none, #presence{type = unavailable}} end, Affiliation = get_affiliation(LJID, StateData), Node1 = case is_ra_changed(NJID, IsInitialPresence, StateData, OldStateData) of true -> ?NS_MUCSUB_NODES_AFFILIATIONS; false -> ?NS_MUCSUB_NODES_PRESENCE end, Node2 = ?NS_MUCSUB_NODES_PARTICIPANTS, UserMap = case is_room_overcrowded(StateData) orelse (not (presence_broadcast_allowed(NJID, StateData) orelse presence_broadcast_allowed(NJID, OldStateData))) of true -> #{LNJID => UserInfo}; false -> %% TODO: optimize further UM1 = get_users_and_subscribers_with_node(Node1, StateData), UM2 = get_users_and_subscribers_with_node(Node2, StateData), maps:merge(UM1, UM2) end, maps:fold( fun(LUJID, Info, _) -> IsSelfPresence = LNJID == LUJID, {Role, Presence} = if IsSelfPresence -> {Role0, Presence0}; true -> {Role1, Presence1} end, Item0 = #muc_item{affiliation = Affiliation, role = Role}, Item1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false orelse IsSelfPresence of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Item = Item1#muc_item{reason = Reason}, StatusCodes = status_codes(IsInitialPresence, IsSelfPresence, StateData), Pres = if Presence == undefined -> #presence{}; true -> Presence end, Packet = xmpp:set_subtag( add_presence_hats(NJID, Pres, StateData), #muc_user{items = [Item], status_codes = StatusCodes}), send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node1, StateData), Type = xmpp:get_type(Packet), IsSubscriber = is_subscriber(Info#user.jid, StateData), IsOccupant = Info#user.last_presence /= undefined, if (IsSubscriber and not IsOccupant) and (IsInitialPresence or (Type == unavailable)) -> send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, Node2, StateData); true -> ok end end, ok, UserMap). -spec send_existing_presences(jid(), state()) -> ok. send_existing_presences(ToJID, StateData) -> case is_room_overcrowded(StateData) of true -> ok; false -> send_existing_presences1(ToJID, StateData) end. -spec send_existing_presences1(jid(), state()) -> ok. send_existing_presences1(ToJID, StateData) -> LToJID = jid:tolower(ToJID), #user{jid = RealToJID, role = Role} = maps:get(LToJID, StateData#state.users), maps:fold( fun(FromNick, _Users, _) -> LJID = find_jid_by_nick(FromNick, StateData), #user{jid = FromJID, role = FromRole, last_presence = Presence} = maps:get(jid:tolower(LJID), StateData#state.users), PresenceBroadcast = lists:member( FromRole, (StateData#state.config)#config.presence_broadcast), case {RealToJID, PresenceBroadcast} of {FromJID, _} -> ok; {_, false} -> ok; _ -> FromAffiliation = get_affiliation(LJID, StateData), Item0 = #muc_item{affiliation = FromAffiliation, role = FromRole}, Item = case Role == moderator orelse (StateData#state.config)#config.anonymous == false of true -> Item0#muc_item{jid = FromJID}; false -> Item0 end, Packet = xmpp:set_subtag( add_presence_hats( FromJID, Presence, StateData), #muc_user{items = [Item]}), send_wrapped(jid:replace_resource(StateData#state.jid, FromNick), RealToJID, Packet, ?NS_MUCSUB_NODES_PRESENCE, StateData) end end, ok, StateData#state.nicks). -spec set_nick(jid(), binary(), state()) -> state(). set_nick(JID, Nick, State) -> LJID = jid:tolower(JID), #user{nick = OldNick} = maps:get(LJID, State#state.users), Users = maps:update_with(LJID, fun (#user{} = User) -> User#user{nick = Nick} end, State#state.users), OldNickUsers = maps:get(OldNick, State#state.nicks), NewNickUsers = maps:get(Nick, State#state.nicks, []), Nicks = case OldNickUsers of [LJID] -> maps:put(Nick, [LJID | NewNickUsers -- [LJID]], maps:remove(OldNick, State#state.nicks)); [_ | _] -> maps:put(Nick, [LJID | NewNickUsers -- [LJID]], maps:put(OldNick, OldNickUsers -- [LJID], State#state.nicks)) end, State#state{users = Users, nicks = Nicks}. -spec change_nick(jid(), binary(), state()) -> state(). change_nick(JID, Nick, StateData) -> LJID = jid:tolower(JID), #user{nick = OldNick} = maps:get(LJID, StateData#state.users), OldNickUsers = maps:get(OldNick, StateData#state.nicks), NewNickUsers = maps:get(Nick, StateData#state.nicks, []), SendOldUnavailable = length(OldNickUsers) == 1, SendNewAvailable = SendOldUnavailable orelse NewNickUsers == [], NewStateData = set_nick(JID, Nick, StateData), case presence_broadcast_allowed(JID, NewStateData) of true -> send_nick_changing(JID, OldNick, NewStateData, SendOldUnavailable, SendNewAvailable); false -> ok end, add_to_log(nickchange, {OldNick, Nick}, StateData), NewStateData. -spec send_nick_changing(jid(), binary(), state(), boolean(), boolean()) -> ok. send_nick_changing(JID, OldNick, StateData, SendOldUnavailable, SendNewAvailable) -> #user{jid = RealJID, nick = Nick, role = Role, last_presence = Presence} = maps:get(jid:tolower(JID), StateData#state.users), Affiliation = get_affiliation(JID, StateData), maps:fold( fun(LJID, Info, _) when Presence /= undefined -> IsSelfPresence = LJID == jid:tolower(JID), Item0 = #muc_item{affiliation = Affiliation, role = Role}, Item = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false orelse IsSelfPresence of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Status110 = case IsSelfPresence of true -> [110]; false -> [] end, Packet1 = #presence{ type = unavailable, sub_els = [#muc_user{ items = [Item#muc_item{nick = Nick}], status_codes = [303|Status110]}]}, Packet2 = xmpp:set_subtag(Presence, #muc_user{items = [Item], status_codes = Status110}), if SendOldUnavailable -> send_wrapped( jid:replace_resource(StateData#state.jid, OldNick), Info#user.jid, Packet1, ?NS_MUCSUB_NODES_PRESENCE, StateData); true -> ok end, if SendNewAvailable -> send_wrapped( jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet2, ?NS_MUCSUB_NODES_PRESENCE, StateData); true -> ok end; (_, _, _) -> ok end, ok, get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_PRESENCE, StateData)). -spec maybe_send_affiliation(jid(), affiliation(), state()) -> ok. maybe_send_affiliation(JID, Affiliation, StateData) -> LJID = jid:tolower(JID), %% TODO: there should be a better way to check IsOccupant Users = get_users_and_subscribers(StateData), IsOccupant = case LJID of {LUser, LServer, <<"">>} -> #{} /= maps:filter( fun({U, S, _}, _) -> U == LUser andalso S == LServer end, Users); {_LUser, _LServer, _LResource} -> maps:is_key(LJID, Users) end, case IsOccupant of true -> ok; % The new affiliation is published via presence. false -> send_affiliation(JID, Affiliation, StateData) end. -spec send_affiliation(jid(), affiliation(), state()) -> ok. send_affiliation(JID, Affiliation, StateData) -> Item = #muc_item{jid = JID, affiliation = Affiliation, role = none}, Message = #message{id = p1_rand:get_string(), sub_els = [#muc_user{items = [Item]}]}, Users = get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), Recipients = case (StateData#state.config)#config.anonymous of true -> maps:filter(fun(_, #user{role = moderator}) -> true; (_, _) -> false end, Users); false -> Users end, send_wrapped_multiple(StateData#state.jid, Recipients, Message, ?NS_MUCSUB_NODES_AFFILIATIONS, StateData). -spec status_codes(boolean(), boolean(), state()) -> [pos_integer()]. status_codes(IsInitialPresence, _IsSelfPresence = true, StateData) -> S0 = [110], case IsInitialPresence of true -> S1 = case StateData#state.just_created of true -> [201|S0]; _ -> S0 end, S2 = case (StateData#state.config)#config.anonymous of true -> S1; false -> [100|S1] end, S3 = case (StateData#state.config)#config.logging of true -> [170|S2]; false -> S2 end, S3; false -> S0 end; status_codes(_IsInitialPresence, _IsSelfPresence = false, _StateData) -> []. -spec lqueue_new(non_neg_integer(), ram | file) -> lqueue(). lqueue_new(Max, Type) -> #lqueue{queue = p1_queue:new(Type), max = Max}. -spec lqueue_in(lqueue_elem(), lqueue()) -> lqueue(). %% If the message queue limit is set to 0, do not store messages. lqueue_in(_Item, LQ = #lqueue{max = 0}) -> LQ; %% Otherwise, rotate messages in the queue store. lqueue_in(Item, #lqueue{queue = Q1, max = Max}) -> Len = p1_queue:len(Q1), Q2 = p1_queue:in(Item, Q1), if Len >= Max -> Q3 = lqueue_cut(Q2, Len - Max + 1), #lqueue{queue = Q3, max = Max}; true -> #lqueue{queue = Q2, max = Max} end. -spec lqueue_cut(p1_queue:queue(lqueue_elem()), non_neg_integer()) -> p1_queue:queue(lqueue_elem()). lqueue_cut(Q, 0) -> Q; lqueue_cut(Q, N) -> {_, Q1} = p1_queue:out(Q), lqueue_cut(Q1, N - 1). -spec add_message_to_history(binary(), jid(), message(), state()) -> state(). add_message_to_history(FromNick, FromJID, Packet, StateData) -> add_to_log(text, {FromNick, Packet}, StateData), case check_subject(Packet) of [] -> TimeStamp = erlang:timestamp(), AddrPacket = case (StateData#state.config)#config.anonymous of true -> Packet; false -> Addresses = #addresses{ list = [#address{type = ofrom, jid = FromJID}]}, xmpp:set_subtag(Packet, Addresses) end, TSPacket = misc:add_delay_info( AddrPacket, StateData#state.jid, TimeStamp), SPacket = xmpp:set_from_to( TSPacket, jid:replace_resource(StateData#state.jid, FromNick), StateData#state.jid), Size = element_size(SPacket), Q1 = lqueue_in({FromNick, TSPacket, false, TimeStamp, Size}, StateData#state.history), StateData#state{history = Q1, just_created = erlang:system_time(microsecond)}; _ -> StateData#state{just_created = erlang:system_time(microsecond)} end. -spec send_history(jid(), [lqueue_elem()], state()) -> ok. send_history(JID, History, StateData) -> lists:foreach( fun({Nick, Packet, _HaveSubject, _TimeStamp, _Size}) -> ejabberd_router:route( xmpp:set_from_to( Packet, jid:replace_resource(StateData#state.jid, Nick), JID)) end, History). -spec send_subject(jid(), state()) -> ok. send_subject(JID, #state{subject_author = Nick} = StateData) -> Subject = case StateData#state.subject of [] -> [#text{}]; [_|_] = S -> S end, Packet = #message{from = jid:replace_resource(StateData#state.jid, Nick), to = JID, type = groupchat, subject = Subject}, ejabberd_router:route(Packet). -spec check_subject(message()) -> [text()]. check_subject(#message{subject = [_|_] = Subj, body = [], thread = undefined}) -> Subj; check_subject(_) -> []. -spec can_change_subject(role(), boolean(), state()) -> boolean(). can_change_subject(Role, IsSubscriber, StateData) -> case (StateData#state.config)#config.allow_change_subj of true -> Role == moderator orelse Role == participant orelse IsSubscriber == true; _ -> Role == moderator end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Admin stuff -spec process_iq_admin(jid(), iq(), #state{}) -> {error, stanza_error()} | {result, undefined, #state{}} | {result, muc_admin()}. process_iq_admin(_From, #iq{lang = Lang, sub_els = [#muc_admin{items = []}]}, _StateData) -> Txt = ?T("No 'item' element found"), {error, xmpp:err_bad_request(Txt, Lang)}; process_iq_admin(_From, #iq{type = get, lang = Lang, sub_els = [#muc_admin{items = [_, _|_]}]}, _StateData) -> ErrText = ?T("Too many elements"), {error, xmpp:err_bad_request(ErrText, Lang)}; process_iq_admin(From, #iq{type = set, lang = Lang, sub_els = [#muc_admin{items = Items}]}, StateData) -> process_admin_items_set(From, Items, Lang, StateData); process_iq_admin(From, #iq{type = get, lang = Lang, sub_els = [#muc_admin{items = [Item]}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), case Item of #muc_item{role = undefined, affiliation = undefined} -> Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), {error, xmpp:err_bad_request(Txt, Lang)}; #muc_item{role = undefined, affiliation = Affiliation} -> if (FAffiliation == owner) or (FAffiliation == admin) or ((FAffiliation == member) and not (StateData#state.config)#config.anonymous) -> Items = items_with_affiliation(Affiliation, StateData), {result, #muc_admin{items = Items}}; true -> ErrText = ?T("Administrator privileges required"), {error, xmpp:err_forbidden(ErrText, Lang)} end; #muc_item{role = Role} -> if FRole == moderator -> Items = items_with_role(Role, StateData), {result, #muc_admin{items = Items}}; true -> ErrText = ?T("Moderator privileges required"), {error, xmpp:err_forbidden(ErrText, Lang)} end end. -spec items_with_role(role(), state()) -> [muc_item()]. items_with_role(SRole, StateData) -> lists:map(fun ({_, U}) -> user_to_item(U, StateData) end, search_role(SRole, StateData)). -spec items_with_affiliation(affiliation(), state()) -> [muc_item()]. items_with_affiliation(SAffiliation, StateData) -> lists:map( fun({JID, {Affiliation, Reason}}) -> #muc_item{affiliation = Affiliation, jid = jid:make(JID), reason = Reason}; ({JID, Affiliation}) -> #muc_item{affiliation = Affiliation, jid = jid:make(JID)} end, search_affiliation(SAffiliation, StateData)). -spec user_to_item(#user{}, state()) -> muc_item(). user_to_item(#user{role = Role, nick = Nick, jid = JID}, StateData) -> Affiliation = get_affiliation(JID, StateData), #muc_item{role = Role, affiliation = Affiliation, nick = Nick, jid = JID}. -spec search_role(role(), state()) -> [{ljid(), #user{}}]. search_role(Role, StateData) -> lists:filter(fun ({_, #user{role = R}}) -> Role == R end, maps:to_list(StateData#state.users)). -spec search_affiliation(affiliation(), state()) -> [{ljid(), affiliation() | {affiliation(), binary()}}]. search_affiliation(Affiliation, #state{config = #config{persistent = false}} = StateData) -> search_affiliation_fallback(Affiliation, StateData); search_affiliation(Affiliation, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, Mod = gen_mod:db_mod(ServerHost, mod_muc), case Mod:search_affiliation(ServerHost, Room, Host, Affiliation) of {ok, AffiliationList} -> AffiliationList; {error, _} -> search_affiliation_fallback(Affiliation, StateData) end. -spec search_affiliation_fallback(affiliation(), state()) -> [{ljid(), affiliation() | {affiliation(), binary()}}]. search_affiliation_fallback(Affiliation, StateData) -> lists:filter( fun({_, A}) -> case A of {A1, _Reason} -> Affiliation == A1; _ -> Affiliation == A end end, maps:to_list(StateData#state.affiliations)). -spec process_admin_items_set(jid(), [muc_item()], binary(), #state{}) -> {result, undefined, #state{}} | {error, stanza_error()}. process_admin_items_set(UJID, Items, Lang, StateData) -> UAffiliation = get_affiliation(UJID, StateData), URole = get_role(UJID, StateData), case catch find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, []) of {result, Res} -> ?INFO_MSG("Processing MUC admin query from ~ts in " "room ~ts:~n ~p", [jid:encode(UJID), jid:encode(StateData#state.jid), Res]), case lists:foldl(process_item_change(UJID), StateData, lists:flatten(Res)) of {error, _} = Err -> Err; NSD -> store_room(NSD), {result, undefined, NSD} end; {error, Err} -> {error, Err} end. -spec process_item_change(jid()) -> fun((admin_action(), state() | {error, stanza_error()}) -> state() | {error, stanza_error()}). process_item_change(UJID) -> fun(_, {error, _} = Err) -> Err; (Item, SD) -> process_item_change(Item, SD, UJID) end. -spec process_item_change(admin_action(), state(), undefined | jid()) -> state() | {error, stanza_error()}. process_item_change(Item, SD, UJID) -> try case Item of {JID, affiliation, owner, _} when JID#jid.luser == <<"">> -> %% If the provided JID does not have username, %% forget the affiliation completely SD; {JID, role, none, Reason} -> send_kickban_presence(UJID, JID, Reason, 307, SD), set_role(JID, none, SD); {JID, affiliation, none, Reason} -> case (SD#state.config)#config.members_only of true -> send_kickban_presence(UJID, JID, Reason, 321, none, SD), maybe_send_affiliation(JID, none, SD), SD1 = set_affiliation(JID, none, SD), set_role(JID, none, SD1); _ -> SD1 = set_affiliation(JID, none, SD), SD2 = case (SD1#state.config)#config.moderated of true -> set_role(JID, visitor, SD1); false -> set_role(JID, participant, SD1) end, send_update_presence(JID, Reason, SD2, SD), maybe_send_affiliation(JID, none, SD2), SD2 end; {JID, affiliation, outcast, Reason} -> send_kickban_presence(UJID, JID, Reason, 301, outcast, SD), maybe_send_affiliation(JID, outcast, SD), {result, undefined, SD2} = process_iq_mucsub(JID, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, SD), set_affiliation(JID, outcast, set_role(JID, none, SD2), Reason); {JID, affiliation, A, Reason} when (A == admin) or (A == owner) -> SD1 = set_affiliation(JID, A, SD, Reason), SD2 = set_role(JID, moderator, SD1), send_update_presence(JID, Reason, SD2, SD), maybe_send_affiliation(JID, A, SD2), SD2; {JID, affiliation, member, Reason} -> SD1 = set_affiliation(JID, member, SD, Reason), SD2 = set_role(JID, participant, SD1), send_update_presence(JID, Reason, SD2, SD), maybe_send_affiliation(JID, member, SD2), SD2; {JID, role, Role, Reason} -> SD1 = set_role(JID, Role, SD), send_new_presence(JID, Reason, SD1, SD), SD1; {JID, affiliation, A, _Reason} -> SD1 = set_affiliation(JID, A, SD), send_update_presence(JID, SD1, SD), maybe_send_affiliation(JID, A, SD1), SD1 end catch ?EX_RULE(E, R, St) -> StackTrace = ?EX_STACK(St), FromSuffix = case UJID of #jid{} -> JidString = jid:encode(UJID), <<" from ", JidString/binary>>; undefined -> <<"">> end, ?ERROR_MSG("Failed to set item ~p~ts:~n** ~ts", [Item, FromSuffix, misc:format_exception(2, E, R, StackTrace)]), {error, xmpp:err_internal_server_error()} end. -spec find_changed_items(jid(), affiliation(), role(), [muc_item()], binary(), state(), [admin_action()]) -> {result, [admin_action()]}. find_changed_items(_UJID, _UAffiliation, _URole, [], _Lang, _StateData, Res) -> {result, Res}; find_changed_items(_UJID, _UAffiliation, _URole, [#muc_item{jid = undefined, nick = <<"">>}|_], Lang, _StateData, _Res) -> Txt = ?T("Neither 'jid' nor 'nick' attribute found"), throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(_UJID, _UAffiliation, _URole, [#muc_item{role = undefined, affiliation = undefined}|_], Lang, _StateData, _Res) -> Txt = ?T("Neither 'role' nor 'affiliation' attribute found"), throw({error, xmpp:err_bad_request(Txt, Lang)}); find_changed_items(UJID, UAffiliation, URole, [#muc_item{jid = J, nick = Nick, reason = Reason, role = Role, affiliation = Affiliation}|Items], Lang, StateData, Res) -> [JID | _] = JIDs = if J /= undefined -> [J]; Nick /= <<"">> -> case find_jids_by_nick(Nick, StateData) of [] -> ErrText = {?T("Nickname ~s does not exist in the room"), [Nick]}, throw({error, xmpp:err_not_acceptable(ErrText, Lang)}); JIDList -> JIDList end end, {RoleOrAff, RoleOrAffValue} = if Role == undefined -> {affiliation, Affiliation}; true -> {role, Role} end, TAffiliation = get_affiliation(JID, StateData), TRole = get_role(JID, StateData), ServiceAf = get_service_affiliation(JID, StateData), UIsSubscriber = is_subscriber(UJID, StateData), URole1 = case {URole, UIsSubscriber} of {none, true} -> subscriber; {UR, _} -> UR end, CanChangeRA = case can_change_ra(UAffiliation, URole1, TAffiliation, TRole, RoleOrAff, RoleOrAffValue, ServiceAf) of nothing -> nothing; true -> true; check_owner -> case search_affiliation(owner, StateData) of [{OJID, _}] -> jid:remove_resource(OJID) /= jid:tolower(jid:remove_resource(UJID)); _ -> true end; _ -> false end, case CanChangeRA of nothing -> find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, Res); true -> MoreRes = case RoleOrAff of affiliation -> [{jid:remove_resource(Jidx), RoleOrAff, RoleOrAffValue, Reason} || Jidx <- JIDs]; role -> [{Jidx, RoleOrAff, RoleOrAffValue, Reason} || Jidx <- JIDs] end, find_changed_items(UJID, UAffiliation, URole, Items, Lang, StateData, MoreRes ++ Res); false -> Txt = ?T("Changing role/affiliation is not allowed"), throw({error, xmpp:err_not_allowed(Txt, Lang)}) end. -spec can_change_ra(affiliation(), role(), affiliation(), role(), affiliation, affiliation(), affiliation()) -> boolean() | nothing | check_owner; (affiliation(), role(), affiliation(), role(), role, role(), affiliation()) -> boolean() | nothing | check_owner. can_change_ra(_FAffiliation, _FRole, owner, _TRole, affiliation, owner, owner) -> %% A room owner tries to add as persistent owner a %% participant that is already owner because he is MUC admin true; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, _RoleorAffiliation, _Value, owner) -> %% Nobody can decrease MUC admin's role/affiliation false; can_change_ra(_FAffiliation, _FRole, TAffiliation, _TRole, affiliation, Value, _ServiceAf) when TAffiliation == Value -> nothing; can_change_ra(_FAffiliation, _FRole, _TAffiliation, TRole, role, Value, _ServiceAf) when TRole == Value -> nothing; can_change_ra(FAffiliation, _FRole, outcast, _TRole, affiliation, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, outcast, _TRole, affiliation, member, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, outcast, _TRole, affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, outcast, _TRole, affiliation, owner, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, none, _TRole, affiliation, outcast, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, none, _TRole, affiliation, member, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, none, _TRole, affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, none, _TRole, affiliation, owner, _ServiceAf) -> true; can_change_ra(FAffiliation, _FRole, member, _TRole, affiliation, outcast, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, member, _TRole, affiliation, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(owner, _FRole, member, _TRole, affiliation, admin, _ServiceAf) -> true; can_change_ra(owner, _FRole, member, _TRole, affiliation, owner, _ServiceAf) -> true; can_change_ra(owner, _FRole, admin, _TRole, affiliation, _Affiliation, _ServiceAf) -> true; can_change_ra(owner, _FRole, owner, _TRole, affiliation, _Affiliation, _ServiceAf) -> check_owner; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, affiliation, _Value, _ServiceAf) -> false; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, none, _ServiceAf) -> true; can_change_ra(FAffiliation, subscriber, _TAffiliation, visitor, role, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, visitor, role, participant, _ServiceAf) -> true; can_change_ra(FAffiliation, subscriber, _TAffiliation, visitor, role, participant, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, _TAffiliation, visitor, role, moderator, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, none, _ServiceAf) -> true; can_change_ra(FAffiliation, subscriber, _TAffiliation, participant, role, none, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, moderator, _TAffiliation, participant, role, visitor, _ServiceAf) -> true; can_change_ra(FAffiliation, subscriber, _TAffiliation, participant, role, visitor, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(FAffiliation, _FRole, _TAffiliation, participant, role, moderator, _ServiceAf) when (FAffiliation == owner) or (FAffiliation == admin) -> true; can_change_ra(_FAffiliation, _FRole, owner, moderator, role, visitor, _ServiceAf) -> false; can_change_ra(owner, _FRole, _TAffiliation, moderator, role, visitor, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, admin, moderator, role, visitor, _ServiceAf) -> false; can_change_ra(admin, _FRole, _TAffiliation, moderator, role, visitor, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, owner, moderator, role, participant, _ServiceAf) -> false; can_change_ra(owner, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; can_change_ra(_FAffiliation, _FRole, admin, moderator, role, participant, _ServiceAf) -> false; can_change_ra(admin, _FRole, _TAffiliation, moderator, role, participant, _ServiceAf) -> true; can_change_ra(owner, moderator, TAffiliation, moderator, role, none, _ServiceAf) when TAffiliation /= owner -> true; can_change_ra(owner, subscriber, TAffiliation, moderator, role, none, _ServiceAf) when TAffiliation /= owner -> true; can_change_ra(admin, moderator, TAffiliation, moderator, role, none, _ServiceAf) when (TAffiliation /= owner) and (TAffiliation /= admin) -> true; can_change_ra(admin, subscriber, TAffiliation, moderator, role, none, _ServiceAf) when (TAffiliation /= owner) and (TAffiliation /= admin) -> true; can_change_ra(_FAffiliation, _FRole, _TAffiliation, _TRole, role, _Value, _ServiceAf) -> false. -spec send_kickban_presence(undefined | jid(), jid(), binary(), pos_integer(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, StateData) -> NewAffiliation = get_affiliation(JID, StateData), send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData). -spec send_kickban_presence(undefined | jid(), jid(), binary(), pos_integer(), affiliation(), state()) -> ok. send_kickban_presence(UJID, JID, Reason, Code, NewAffiliation, StateData) -> LJID = jid:tolower(JID), LJIDs = case LJID of {U, S, <<"">>} -> maps:fold(fun (J, _, Js) -> case J of {U, S, _} -> [J | Js]; _ -> Js end end, [], StateData#state.users); _ -> case maps:is_key(LJID, StateData#state.users) of true -> [LJID]; _ -> [] end end, lists:foreach(fun (LJ) -> #user{nick = Nick, jid = J} = maps:get(LJ, StateData#state.users), add_to_log(kickban, {Nick, Reason, Code}, StateData), tab_remove_online_user(J, StateData), send_kickban_presence1(UJID, J, Reason, Code, NewAffiliation, StateData) end, LJIDs). -spec send_kickban_presence1(undefined | jid(), jid(), binary(), pos_integer(), affiliation(), state()) -> ok. send_kickban_presence1(MJID, UJID, Reason, Code, Affiliation, StateData) -> #user{jid = RealJID, nick = Nick} = maps:get(jid:tolower(UJID), StateData#state.users), ActorNick = get_actor_nick(MJID, StateData), %% TODO: optimize further UserMap = maps:merge( get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_PARTICIPANTS, StateData)), maps:fold( fun(LJID, Info, _) -> IsSelfPresence = jid:tolower(UJID) == LJID, Item0 = #muc_item{affiliation = Affiliation, role = none}, Item1 = case Info#user.role == moderator orelse (StateData#state.config)#config.anonymous == false orelse IsSelfPresence of true -> Item0#muc_item{jid = RealJID}; false -> Item0 end, Item2 = Item1#muc_item{reason = Reason}, Item = case ActorNick of <<"">> -> Item2; _ -> Item2#muc_item{actor = #muc_actor{nick = ActorNick}} end, Codes = if IsSelfPresence -> [110, Code]; true -> [Code] end, Packet = #presence{type = unavailable, sub_els = [#muc_user{items = [Item], status_codes = Codes}]}, RoomJIDNick = jid:replace_resource(StateData#state.jid, Nick), send_wrapped(RoomJIDNick, Info#user.jid, Packet, ?NS_MUCSUB_NODES_AFFILIATIONS, StateData), 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, ?NS_MUCSUB_NODES_PARTICIPANTS, StateData); true -> ok end end, ok, UserMap). -spec get_actor_nick(undefined | jid(), state()) -> binary(). get_actor_nick(undefined, _StateData) -> <<"">>; get_actor_nick(MJID, StateData) -> try maps:get(jid:tolower(MJID), StateData#state.users) of #user{nick = ActorNick} -> ActorNick catch _:{badkey, _} -> <<"">> end. -spec convert_legacy_fields([xdata_field()]) -> [xdata_field()]. convert_legacy_fields(Fs) -> lists:map( fun(#xdata_field{var = Var} = F) -> NewVar = case Var of <<"muc#roomconfig_allowvisitorstatus">> -> <<"allow_visitor_status">>; <<"muc#roomconfig_allowvisitornickchange">> -> <<"allow_visitor_nickchange">>; <<"muc#roomconfig_allowvoicerequests">> -> <<"allow_voice_requests">>; <<"muc#roomconfig_allow_subscription">> -> <<"allow_subscription">>; <<"muc#roomconfig_voicerequestmininterval">> -> <<"voice_request_min_interval">>; <<"muc#roomconfig_captcha_whitelist">> -> <<"captcha_whitelist">>; <<"muc#roomconfig_mam">> -> <<"mam">>; _ -> Var end, F#xdata_field{var = NewVar} end, Fs). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Owner stuff -spec process_iq_owner(jid(), iq(), state()) -> {result, undefined | muc_owner()} | {result, undefined | muc_owner(), state() | stop} | {error, stanza_error()}. process_iq_owner(From, #iq{type = set, lang = Lang, sub_els = [#muc_owner{destroy = Destroy, config = Config, items = Items}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), if FAffiliation /= owner -> ErrText = ?T("Owner privileges required"), {error, xmpp:err_forbidden(ErrText, Lang)}; Destroy /= undefined, Config == undefined, Items == [] -> ?INFO_MSG("Destroyed MUC room ~ts by the owner ~ts", [jid:encode(StateData#state.jid), jid:encode(From)]), add_to_log(room_existence, destroyed, StateData), destroy_room(Destroy, StateData); Config /= undefined, Destroy == undefined, Items == [] -> case Config of #xdata{type = cancel} -> {result, undefined}; #xdata{type = submit, fields = Fs} -> Fs1 = convert_legacy_fields(Fs), try muc_roomconfig:decode(Fs1) of Options -> case is_allowed_log_change(Options, StateData, From) andalso is_allowed_persistent_change(Options, StateData, From) andalso is_allowed_mam_change(Options, StateData, From) andalso is_allowed_string_limits(Options, StateData) andalso is_password_settings_correct(Options, StateData) of true -> set_config(Options, StateData, Lang); false -> {error, xmpp:err_not_acceptable()} end catch _:{muc_roomconfig, Why} -> Txt = muc_roomconfig:format_error(Why), {error, xmpp:err_bad_request(Txt, Lang)} end; _ -> Txt = ?T("Incorrect data form"), {error, xmpp:err_bad_request(Txt, Lang)} end; Items /= [], Config == undefined, Destroy == undefined -> process_admin_items_set(From, Items, Lang, StateData); true -> {error, xmpp:err_bad_request()} end; process_iq_owner(From, #iq{type = get, lang = Lang, sub_els = [#muc_owner{destroy = Destroy, config = Config, items = Items}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), if FAffiliation /= owner -> ErrText = ?T("Owner privileges required"), {error, xmpp:err_forbidden(ErrText, Lang)}; Destroy == undefined, Config == undefined -> case Items of [] -> {result, #muc_owner{config = get_config(Lang, StateData, From)}}; [#muc_item{affiliation = undefined}] -> Txt = ?T("No 'affiliation' attribute found"), {error, xmpp:err_bad_request(Txt, Lang)}; [#muc_item{affiliation = Affiliation}] -> Items = items_with_affiliation(Affiliation, StateData), {result, #muc_owner{items = Items}}; [_|_] -> Txt = ?T("Too many elements"), {error, xmpp:err_bad_request(Txt, Lang)} end; true -> {error, xmpp:err_bad_request()} end. -spec is_allowed_log_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_log_change(Options, StateData, From) -> case proplists:is_defined(enablelogging, Options) of false -> true; true -> allow == mod_muc_log:check_access_log(StateData#state.server_host, From) end. -spec is_allowed_persistent_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_persistent_change(Options, StateData, From) -> case proplists:is_defined(persistentroom, Options) of false -> true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} = StateData#state.access, allow == acl:match_rule(StateData#state.server_host, AccessPersistent, From) end. -spec is_allowed_mam_change(muc_roomconfig:result(), state(), jid()) -> boolean(). is_allowed_mam_change(Options, StateData, From) -> case proplists:is_defined(mam, Options) of false -> true; true -> {_AccessRoute, _AccessCreate, _AccessAdmin, _AccessPersistent, AccessMam} = StateData#state.access, allow == acl:match_rule(StateData#state.server_host, AccessMam, From) end. %% Check if the string fields defined in the Data Form %% are conformant to the configured limits -spec is_allowed_string_limits(muc_roomconfig:result(), state()) -> boolean(). is_allowed_string_limits(Options, StateData) -> RoomName = proplists:get_value(roomname, Options, <<"">>), RoomDesc = proplists:get_value(roomdesc, Options, <<"">>), Password = proplists:get_value(roomsecret, Options, <<"">>), CaptchaWhitelist = proplists:get_value(captcha_whitelist, Options, []), CaptchaWhitelistSize = lists:foldl( fun(Jid, Sum) -> byte_size(jid:encode(Jid)) + Sum end, 0, CaptchaWhitelist), MaxRoomName = mod_muc_opt:max_room_name(StateData#state.server_host), MaxRoomDesc = mod_muc_opt:max_room_desc(StateData#state.server_host), MaxPassword = mod_muc_opt:max_password(StateData#state.server_host), MaxCaptchaWhitelist = mod_muc_opt:max_captcha_whitelist(StateData#state.server_host), (byte_size(RoomName) =< MaxRoomName) andalso (byte_size(RoomDesc) =< MaxRoomDesc) andalso (byte_size(Password) =< MaxPassword) andalso (CaptchaWhitelistSize =< MaxCaptchaWhitelist). %% Return false if: %% "the password for a password-protected room is blank" -spec is_password_settings_correct(muc_roomconfig:result(), state()) -> boolean(). is_password_settings_correct(Options, StateData) -> Config = StateData#state.config, OldProtected = Config#config.password_protected, OldPassword = Config#config.password, NewProtected = proplists:get_value(passwordprotectedroom, Options), NewPassword = proplists:get_value(roomsecret, Options), case {OldProtected, NewProtected, OldPassword, NewPassword} of {true, undefined, <<"">>, undefined} -> false; {true, undefined, _, <<"">>} -> false; {_, true, <<"">>, undefined} -> false; {_, true, _, <<"">>} -> false; _ -> true end. -spec get_default_room_maxusers(state()) -> non_neg_integer(). get_default_room_maxusers(RoomState) -> DefRoomOpts = mod_muc_opt:default_room_options(RoomState#state.server_host), RoomState2 = set_opts(DefRoomOpts, RoomState), (RoomState2#state.config)#config.max_users. -spec get_config(binary(), state(), jid()) -> xdata(). get_config(Lang, StateData, From) -> {_AccessRoute, _AccessCreate, _AccessAdmin, AccessPersistent, _AccessMam} = StateData#state.access, ServiceMaxUsers = get_service_max_users(StateData), DefaultRoomMaxUsers = get_default_room_maxusers(StateData), Config = StateData#state.config, MaxUsersRoom = get_max_users(StateData), Title = str:translate_and_format( Lang, ?T("Configuration of room ~s"), [jid:encode(StateData#state.jid)]), Fs = [{roomname, Config#config.title}, {roomdesc, Config#config.description}, {lang, Config#config.lang}] ++ case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of allow -> [{persistentroom, Config#config.persistent}]; deny -> [] end ++ [{publicroom, Config#config.public}, {public_list, Config#config.public_list}, {passwordprotectedroom, Config#config.password_protected}, {roomsecret, case Config#config.password_protected of true -> Config#config.password; false -> <<"">> end}, {maxusers, MaxUsersRoom, [if is_integer(ServiceMaxUsers) -> []; true -> [{?T("No limit"), <<"none">>}] end] ++ [{integer_to_binary(N), N} || N <- lists:usort([ServiceMaxUsers, DefaultRoomMaxUsers, MaxUsersRoom | ?MAX_USERS_DEFAULT_LIST]), N =< ServiceMaxUsers]}, {whois, if Config#config.anonymous -> moderators; true -> anyone end}, {presencebroadcast, Config#config.presence_broadcast}, {membersonly, Config#config.members_only}, {moderatedroom, Config#config.moderated}, {members_by_default, Config#config.members_by_default}, {changesubject, Config#config.allow_change_subj}, {allow_private_messages, Config#config.allow_private_messages}, {allow_private_messages_from_visitors, Config#config.allow_private_messages_from_visitors}, {allow_query_users, Config#config.allow_query_users}, {allowinvites, Config#config.allow_user_invites}, {allow_visitor_status, Config#config.allow_visitor_status}, {allow_visitor_nickchange, Config#config.allow_visitor_nickchange}, {allow_voice_requests, Config#config.allow_voice_requests}, {allow_subscription, Config#config.allow_subscription}, {voice_request_min_interval, Config#config.voice_request_min_interval}, {pubsub, Config#config.pubsub}, {enable_hats, Config#config.enable_hats}] ++ case ejabberd_captcha:is_feature_available() of true -> [{captcha_protected, Config#config.captcha_protected}, {captcha_whitelist, lists:map( fun jid:make/1, ?SETS:to_list(Config#config.captcha_whitelist))}]; false -> [] end ++ case mod_muc_log:check_access_log(StateData#state.server_host, From) of allow -> [{enablelogging, Config#config.logging}]; deny -> [] end, Fields = ejabberd_hooks:run_fold(get_room_config, StateData#state.server_host, Fs, [StateData, From, Lang]), #xdata{type = form, title = Title, fields = muc_roomconfig:encode(Fields, Lang)}. -spec set_config(muc_roomconfig:result(), state(), binary()) -> {error, stanza_error()} | {result, undefined, state()}. set_config(Options, StateData, Lang) -> try #config{} = Config = set_config(Options, StateData#state.config, StateData#state.server_host, Lang), {result, _, NSD} = Res = change_config(Config, StateData), Type = case {(StateData#state.config)#config.logging, Config#config.logging} of {true, false} -> roomconfig_change_disabledlogging; {false, true} -> roomconfig_change_enabledlogging; {_, _} -> roomconfig_change end, Users = [{U#user.jid, U#user.nick, U#user.role} || U <- maps:values(StateData#state.users)], add_to_log(Type, Users, NSD), Res catch _:{badmatch, {error, #stanza_error{}} = Err} -> Err end. -spec get_config_opt_name(pos_integer()) -> atom(). get_config_opt_name(Pos) -> Fs = [config|record_info(fields, config)], lists:nth(Pos, Fs). -spec set_config([muc_roomconfig:property()], #config{}, binary(), binary()) -> #config{} | {error, stanza_error()}. set_config(Opts, Config, ServerHost, Lang) -> lists:foldl( fun(_, {error, _} = Err) -> Err; ({roomname, Title}, C) -> C#config{title = Title}; ({roomdesc, Desc}, C) -> C#config{description = Desc}; ({changesubject, V}, C) -> C#config{allow_change_subj = V}; ({allow_query_users, V}, C) -> C#config{allow_query_users = V}; ({allow_private_messages, V}, C) -> C#config{allow_private_messages = V}; ({allow_private_messages_from_visitors, V}, C) -> C#config{allow_private_messages_from_visitors = V}; ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V}; ({allow_visitor_nickchange, V}, C) -> C#config{allow_visitor_nickchange = V}; ({publicroom, V}, C) -> C#config{public = V}; ({public_list, V}, C) -> C#config{public_list = V}; ({persistentroom, V}, C) -> C#config{persistent = V}; ({moderatedroom, V}, C) -> C#config{moderated = V}; ({members_by_default, V}, C) -> C#config{members_by_default = V}; ({membersonly, V}, C) -> C#config{members_only = V}; ({captcha_protected, V}, C) -> C#config{captcha_protected = V}; ({allowinvites, V}, C) -> C#config{allow_user_invites = V}; ({allow_subscription, V}, C) -> C#config{allow_subscription = V}; ({passwordprotectedroom, V}, C) -> C#config{password_protected = V}; ({roomsecret, V}, C) -> C#config{password = V}; ({anonymous, V}, C) -> C#config{anonymous = V}; ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V}; ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V}; ({voice_request_min_interval, V}, C) -> C#config{voice_request_min_interval = V}; ({whois, moderators}, C) -> C#config{anonymous = true}; ({whois, anyone}, C) -> C#config{anonymous = false}; ({maxusers, V}, C) -> C#config{max_users = V}; ({enablelogging, V}, C) -> C#config{logging = V}; ({pubsub, V}, C) -> C#config{pubsub = V}; ({enable_hats, V}, C) -> C#config{enable_hats = V}; ({lang, L}, C) -> C#config{lang = L}; ({captcha_whitelist, Js}, C) -> LJIDs = [jid:tolower(J) || J <- Js], C#config{captcha_whitelist = ?SETS:from_list(LJIDs)}; ({O, V} = Opt, C) -> case ejabberd_hooks:run_fold(set_room_option, ServerHost, {0, undefined}, [Opt, Lang]) of {0, undefined} -> ?ERROR_MSG("set_room_option hook failed for " "option '~ts' with value ~p", [O, V]), Txt = {?T("Failed to process option '~s'"), [O]}, {error, xmpp:err_internal_server_error(Txt, Lang)}; {Pos, Val} -> setelement(Pos, C, Val) end end, Config, Opts). -spec change_config(#config{}, state()) -> {result, undefined, state()}. change_config(Config, StateData) -> send_config_change_info(Config, StateData), StateData0 = StateData#state{config = Config}, StateData1 = remove_subscriptions(StateData0), StateData2 = case {(StateData#state.config)#config.persistent, Config#config.persistent} of {WasPersistent, true} -> if not WasPersistent -> set_affiliations(StateData1#state.affiliations, StateData1); true -> ok end, store_room(StateData1), StateData1; {true, false} -> Affiliations = get_affiliations(StateData), maybe_forget_room(StateData), StateData1#state{affiliations = Affiliations}; _ -> StateData1 end, case {(StateData#state.config)#config.members_only, Config#config.members_only} of {false, true} -> StateData3 = remove_nonmembers(StateData2), {result, undefined, StateData3}; _ -> {result, undefined, StateData2} end. -spec send_config_change_info(#config{}, state()) -> ok. send_config_change_info(Config, #state{config = Config}) -> ok; send_config_change_info(New, #state{config = Old} = StateData) -> Codes = case {Old#config.logging, New#config.logging} of {false, true} -> [170]; {true, false} -> [171]; _ -> [] end ++ case {Old#config.anonymous, New#config.anonymous} of {true, false} -> [172]; {false, true} -> [173]; _ -> [] end ++ case Old#config{anonymous = New#config.anonymous, logging = New#config.logging} of New -> []; _ -> [104] end, if Codes /= [] -> maps:fold( fun(_LJID, #user{jid = JID}, _) -> advertise_entity_capabilities(JID, StateData#state{config = New}) end, ok, StateData#state.users), Message = #message{type = groupchat, id = p1_rand:get_string(), sub_els = [#muc_user{status_codes = Codes}]}, send_wrapped_multiple(StateData#state.jid, get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_CONFIG, StateData), Message, ?NS_MUCSUB_NODES_CONFIG, StateData); true -> ok end. -spec remove_nonmembers(state()) -> state(). remove_nonmembers(StateData) -> maps:fold( fun(_LJID, #user{jid = JID}, SD) -> Affiliation = get_affiliation(JID, SD), case Affiliation of none -> catch send_kickban_presence(undefined, JID, <<"">>, 322, SD), set_role(JID, none, SD); _ -> SD end end, StateData, get_users_and_subscribers(StateData)). -spec set_opts([{atom(), any()}], state()) -> state(). set_opts([], StateData) -> set_vcard_xupdate(StateData); set_opts([{Opt, Val} | Opts], StateData) -> NSD = case Opt of title -> StateData#state{config = (StateData#state.config)#config{title = Val}}; description -> StateData#state{config = (StateData#state.config)#config{description = Val}}; allow_change_subj -> StateData#state{config = (StateData#state.config)#config{allow_change_subj = Val}}; allow_query_users -> StateData#state{config = (StateData#state.config)#config{allow_query_users = Val}}; allow_private_messages -> StateData#state{config = (StateData#state.config)#config{allow_private_messages = Val}}; allow_private_messages_from_visitors -> StateData#state{config = (StateData#state.config)#config{allow_private_messages_from_visitors = Val}}; allow_visitor_nickchange -> StateData#state{config = (StateData#state.config)#config{allow_visitor_nickchange = Val}}; allow_visitor_status -> StateData#state{config = (StateData#state.config)#config{allow_visitor_status = Val}}; public -> StateData#state{config = (StateData#state.config)#config{public = Val}}; public_list -> StateData#state{config = (StateData#state.config)#config{public_list = Val}}; persistent -> StateData#state{config = (StateData#state.config)#config{persistent = Val}}; moderated -> StateData#state{config = (StateData#state.config)#config{moderated = Val}}; members_by_default -> StateData#state{config = (StateData#state.config)#config{members_by_default = Val}}; members_only -> StateData#state{config = (StateData#state.config)#config{members_only = Val}}; allow_user_invites -> StateData#state{config = (StateData#state.config)#config{allow_user_invites = Val}}; password_protected -> StateData#state{config = (StateData#state.config)#config{password_protected = Val}}; captcha_protected -> StateData#state{config = (StateData#state.config)#config{captcha_protected = Val}}; password -> StateData#state{config = (StateData#state.config)#config{password = Val}}; anonymous -> StateData#state{config = (StateData#state.config)#config{anonymous = Val}}; presence_broadcast -> StateData#state{config = (StateData#state.config)#config{presence_broadcast = Val}}; logging -> StateData#state{config = (StateData#state.config)#config{logging = Val}}; mam -> StateData#state{config = (StateData#state.config)#config{mam = Val}}; captcha_whitelist -> StateData#state{config = (StateData#state.config)#config{captcha_whitelist = (?SETS):from_list(Val)}}; allow_voice_requests -> StateData#state{config = (StateData#state.config)#config{allow_voice_requests = Val}}; voice_request_min_interval -> StateData#state{config = (StateData#state.config)#config{voice_request_min_interval = Val}}; max_users -> ServiceMaxUsers = get_service_max_users(StateData), MaxUsers = if Val =< ServiceMaxUsers -> Val; true -> ServiceMaxUsers end, StateData#state{config = (StateData#state.config)#config{max_users = MaxUsers}}; vcard -> StateData#state{config = (StateData#state.config)#config{vcard = Val}}; vcard_xupdate -> StateData#state{config = (StateData#state.config)#config{vcard_xupdate = Val}}; pubsub -> StateData#state{config = (StateData#state.config)#config{pubsub = Val}}; allow_subscription -> StateData#state{config = (StateData#state.config)#config{allow_subscription = Val}}; enable_hats -> StateData#state{config = (StateData#state.config)#config{enable_hats = Val}}; lang -> StateData#state{config = (StateData#state.config)#config{lang = Val}}; subscribers -> MUCSubscribers = lists:foldl( fun({JID, Nick, Nodes}, MUCSubs) -> BareJID = case JID of #jid{} -> jid:remove_resource(JID); _ -> ?ERROR_MSG("Invalid subscriber JID in set_opts ~p", [JID]), jid:remove_resource(jid:make(JID)) end, muc_subscribers_put( #subscriber{jid = BareJID, nick = Nick, nodes = Nodes}, MUCSubs) end, muc_subscribers_new(), Val), StateData#state{muc_subscribers = MUCSubscribers}; affiliations -> StateData#state{affiliations = maps:from_list(Val)}; subject -> Subj = if Val == <<"">> -> []; is_binary(Val) -> [#text{data = Val}]; is_list(Val) -> Val end, StateData#state{subject = Subj}; subject_author -> StateData#state{subject_author = Val}; hats_users -> Hats = maps:from_list( lists:map(fun({U, H}) -> {U, maps:from_list(H)} end, Val)), StateData#state{hats_users = Hats}; _ -> StateData end, set_opts(Opts, NSD). -spec set_vcard_xupdate(state()) -> state(). set_vcard_xupdate(#state{config = #config{vcard = VCardRaw, vcard_xupdate = undefined} = Config} = State) when VCardRaw /= <<"">> -> case fxml_stream:parse_element(VCardRaw) of {error, _} -> State; El -> Hash = mod_vcard_xupdate:compute_hash(El), State#state{config = Config#config{vcard_xupdate = Hash}} end; set_vcard_xupdate(State) -> State. -define(MAKE_CONFIG_OPT(Opt), {get_config_opt_name(Opt), element(Opt, Config)}). -spec make_opts(state()) -> [{atom(), any()}]. make_opts(StateData) -> Config = StateData#state.config, Subscribers = muc_subscribers_fold( fun(_LJID, Sub, Acc) -> [{Sub#subscriber.jid, Sub#subscriber.nick, Sub#subscriber.nodes}|Acc] end, [], StateData#state.muc_subscribers), [?MAKE_CONFIG_OPT(#config.title), ?MAKE_CONFIG_OPT(#config.description), ?MAKE_CONFIG_OPT(#config.allow_change_subj), ?MAKE_CONFIG_OPT(#config.allow_query_users), ?MAKE_CONFIG_OPT(#config.allow_private_messages), ?MAKE_CONFIG_OPT(#config.allow_private_messages_from_visitors), ?MAKE_CONFIG_OPT(#config.allow_visitor_status), ?MAKE_CONFIG_OPT(#config.allow_visitor_nickchange), ?MAKE_CONFIG_OPT(#config.public), ?MAKE_CONFIG_OPT(#config.public_list), ?MAKE_CONFIG_OPT(#config.persistent), ?MAKE_CONFIG_OPT(#config.moderated), ?MAKE_CONFIG_OPT(#config.members_by_default), ?MAKE_CONFIG_OPT(#config.members_only), ?MAKE_CONFIG_OPT(#config.allow_user_invites), ?MAKE_CONFIG_OPT(#config.password_protected), ?MAKE_CONFIG_OPT(#config.captcha_protected), ?MAKE_CONFIG_OPT(#config.password), ?MAKE_CONFIG_OPT(#config.anonymous), ?MAKE_CONFIG_OPT(#config.logging), ?MAKE_CONFIG_OPT(#config.max_users), ?MAKE_CONFIG_OPT(#config.allow_voice_requests), ?MAKE_CONFIG_OPT(#config.allow_subscription), ?MAKE_CONFIG_OPT(#config.mam), ?MAKE_CONFIG_OPT(#config.presence_broadcast), ?MAKE_CONFIG_OPT(#config.voice_request_min_interval), ?MAKE_CONFIG_OPT(#config.vcard), ?MAKE_CONFIG_OPT(#config.vcard_xupdate), ?MAKE_CONFIG_OPT(#config.pubsub), ?MAKE_CONFIG_OPT(#config.enable_hats), ?MAKE_CONFIG_OPT(#config.lang), {captcha_whitelist, (?SETS):to_list((StateData#state.config)#config.captcha_whitelist)}, {affiliations, maps:to_list(StateData#state.affiliations)}, {subject, StateData#state.subject}, {subject_author, StateData#state.subject_author}, {hats_users, lists:map(fun({U, H}) -> {U, maps:to_list(H)} end, maps:to_list(StateData#state.hats_users))}, {hibernation_time, erlang:system_time(microsecond)}, {subscribers, Subscribers}]. expand_opts(CompactOpts) -> DefConfig = #config{}, Fields = record_info(fields, config), {_, Opts1} = lists:foldl( fun(Field, {Pos, Opts}) -> case lists:keyfind(Field, 1, CompactOpts) of false -> DefV = element(Pos, DefConfig), DefVal = case (?SETS):is_set(DefV) of true -> (?SETS):to_list(DefV); false -> DefV end, {Pos+1, [{Field, DefVal}|Opts]}; {_, Val} -> {Pos+1, [{Field, Val}|Opts]} end end, {2, []}, Fields), SubjectAuthor = proplists:get_value(subject_author, CompactOpts, <<"">>), Subject = proplists:get_value(subject, CompactOpts, <<"">>), Subscribers = proplists:get_value(subscribers, CompactOpts, []), HibernationTime = proplists:get_value(hibernation_time, CompactOpts, 0), [{subject, Subject}, {subject_author, SubjectAuthor}, {subscribers, Subscribers}, {hibernation_time, HibernationTime} | lists:reverse(Opts1)]. config_fields() -> [subject, subject_author, subscribers, hibernate_time | record_info(fields, config)]. -spec destroy_room(muc_destroy(), state()) -> {result, undefined, stop}. destroy_room(DEl, StateData) -> Destroy = DEl#muc_destroy{xmlns = ?NS_MUC_USER}, maps:fold( fun(_LJID, Info, _) -> Nick = Info#user.nick, Item = #muc_item{affiliation = none, role = none}, Packet = #presence{ type = unavailable, sub_els = [#muc_user{items = [Item], destroy = Destroy}]}, send_wrapped(jid:replace_resource(StateData#state.jid, Nick), Info#user.jid, Packet, ?NS_MUCSUB_NODES_CONFIG, StateData) end, ok, get_users_and_subscribers_with_node( ?NS_MUCSUB_NODES_CONFIG, StateData)), forget_room(StateData), {result, undefined, stop}. -spec forget_room(state()) -> state(). forget_room(StateData) -> mod_muc:forget_room(StateData#state.server_host, StateData#state.host, StateData#state.room), StateData. -spec maybe_forget_room(state()) -> state(). maybe_forget_room(StateData) -> Forget = case (StateData#state.config)#config.persistent of true -> true; _ -> Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), erlang:function_exported(Mod, get_subscribed_rooms, 3) end, case Forget of true -> forget_room(StateData); _ -> StateData end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Disco -define(CONFIG_OPT_TO_FEATURE(Opt, Fiftrue, Fiffalse), case Opt of true -> Fiftrue; false -> Fiffalse end). -spec make_disco_info(jid(), state()) -> disco_info(). make_disco_info(_From, StateData) -> Config = StateData#state.config, Feats = [?NS_VCARD, ?NS_MUC, ?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_COMMANDS, ?CONFIG_OPT_TO_FEATURE((Config#config.public), <<"muc_public">>, <<"muc_hidden">>), ?CONFIG_OPT_TO_FEATURE((Config#config.persistent), <<"muc_persistent">>, <<"muc_temporary">>), ?CONFIG_OPT_TO_FEATURE((Config#config.members_only), <<"muc_membersonly">>, <<"muc_open">>), ?CONFIG_OPT_TO_FEATURE((Config#config.anonymous), <<"muc_semianonymous">>, <<"muc_nonanonymous">>), ?CONFIG_OPT_TO_FEATURE((Config#config.moderated), <<"muc_moderated">>, <<"muc_unmoderated">>), ?CONFIG_OPT_TO_FEATURE((Config#config.password_protected), <<"muc_passwordprotected">>, <<"muc_unsecured">>)] ++ case Config#config.allow_subscription of true -> [?NS_MUCSUB]; false -> [] end ++ case {gen_mod:is_loaded(StateData#state.server_host, mod_mam), Config#config.mam} of {true, true} -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0]; _ -> [] end, #disco_info{identities = [#identity{category = <<"conference">>, type = <<"text">>, name = (StateData#state.config)#config.title}], features = Feats}. -spec process_iq_disco_info(jid(), iq(), state()) -> {result, disco_info()} | {error, stanza_error()}. process_iq_disco_info(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = <<>>}]}, StateData) -> DiscoInfo = make_disco_info(From, StateData), Extras = iq_disco_info_extras(Lang, StateData, false), {result, DiscoInfo#disco_info{xdata = [Extras]}}; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = ?NS_COMMANDS}]}, StateData) -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_info{ identities = [#identity{category = <<"automation">>, type = <<"command-list">>, name = translate:translate( Lang, ?T("Commands"))}]}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = ?MUC_HAT_ADD_CMD}]}, StateData) -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_info{ identities = [#identity{category = <<"automation">>, type = <<"command-node">>, name = translate:translate( Lang, ?T("Add a hat to a user"))}], features = [?NS_COMMANDS]}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = ?MUC_HAT_REMOVE_CMD}]}, StateData) -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_info{ identities = [#identity{category = <<"automation">>, type = <<"command-node">>, name = translate:translate( Lang, ?T("Remove a hat from a user"))}], features = [?NS_COMMANDS]}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = ?MUC_HAT_LIST_CMD}]}, StateData) -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_info{ identities = [#identity{category = <<"automation">>, type = <<"command-node">>, name = translate:translate( Lang, ?T("List users with hats"))}], features = [?NS_COMMANDS]}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_info(From, #iq{type = get, lang = Lang, sub_els = [#disco_info{node = Node}]}, StateData) -> try true = mod_caps:is_valid_node(Node), DiscoInfo = make_disco_info(From, StateData), Extras = iq_disco_info_extras(Lang, StateData, true), DiscoInfo1 = DiscoInfo#disco_info{xdata = [Extras]}, Hash = mod_caps:compute_disco_hash(DiscoInfo1, sha), Node = <<(ejabberd_config:get_uri())/binary, $#, Hash/binary>>, {result, DiscoInfo1#disco_info{node = Node}} catch _:{badmatch, _} -> Txt = ?T("Invalid node name"), {error, xmpp:err_item_not_found(Txt, Lang)} end. -spec iq_disco_info_extras(binary(), state(), boolean()) -> xdata(). iq_disco_info_extras(Lang, StateData, Static) -> Config = StateData#state.config, AllowPM = case Config#config.allow_private_messages of false -> none; true -> case Config#config.allow_private_messages_from_visitors of nobody -> participants; _ -> anyone end end, Fs1 = [{roomname, Config#config.title}, {description, Config#config.description}, {changesubject, Config#config.allow_change_subj}, {allowinvites, Config#config.allow_user_invites}, {allowpm, AllowPM}, {lang, Config#config.lang}], Fs2 = case Config#config.pubsub of Node when is_binary(Node), Node /= <<"">> -> [{pubsub, Node}|Fs1]; _ -> Fs1 end, Fs3 = case Static of false -> [{occupants, maps:size(StateData#state.nicks)}|Fs2]; true -> Fs2 end, Fs4 = case Config#config.logging of true -> case mod_muc_log:get_url(StateData) of {ok, URL} -> [{logs, URL}|Fs3]; error -> Fs3 end; false -> Fs3 end, #xdata{type = result, fields = muc_roominfo:encode(Fs4, Lang)}. -spec process_iq_disco_items(jid(), iq(), state()) -> {error, stanza_error()} | {result, disco_items()}. process_iq_disco_items(_From, #iq{type = set, lang = Lang}, _StateData) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_disco_items(From, #iq{type = get, sub_els = [#disco_items{node = <<>>}]}, StateData) -> case (StateData#state.config)#config.public_list of true -> {result, get_mucroom_disco_items(StateData)}; _ -> case is_occupant_or_admin(From, StateData) of true -> {result, get_mucroom_disco_items(StateData)}; _ -> %% If the list of occupants is private, %% the room MUST return an empty element %% (http://xmpp.org/extensions/xep-0045.html#disco-roomitems) {result, #disco_items{}} end end; process_iq_disco_items(From, #iq{type = get, lang = Lang, sub_els = [#disco_items{node = ?NS_COMMANDS}]}, StateData) -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_items{ items = [#disco_item{jid = StateData#state.jid, node = ?MUC_HAT_ADD_CMD, name = translate:translate( Lang, ?T("Add a hat to a user"))}, #disco_item{jid = StateData#state.jid, node = ?MUC_HAT_REMOVE_CMD, name = translate:translate( Lang, ?T("Remove a hat from a user"))}, #disco_item{jid = StateData#state.jid, node = ?MUC_HAT_LIST_CMD, name = translate:translate( Lang, ?T("List users with hats"))}]}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_items(From, #iq{type = get, lang = Lang, sub_els = [#disco_items{node = Node}]}, StateData) when Node == ?MUC_HAT_ADD_CMD; Node == ?MUC_HAT_REMOVE_CMD; Node == ?MUC_HAT_LIST_CMD -> case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> {result, #disco_items{}}; false -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)} end; process_iq_disco_items(_From, #iq{lang = Lang}, _StateData) -> Txt = ?T("Node not found"), {error, xmpp:err_item_not_found(Txt, Lang)}. -spec process_iq_captcha(jid(), iq(), state()) -> {error, stanza_error()} | {result, undefined}. process_iq_captcha(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), {error, xmpp:err_not_allowed(Txt, Lang)}; process_iq_captcha(_From, #iq{type = set, lang = Lang, sub_els = [SubEl]}, _StateData) -> case ejabberd_captcha:process_reply(SubEl) of ok -> {result, undefined}; {error, malformed} -> Txt = ?T("Incorrect CAPTCHA submit"), {error, xmpp:err_bad_request(Txt, Lang)}; _ -> Txt = ?T("The CAPTCHA verification has failed"), {error, xmpp:err_not_allowed(Txt, Lang)} end. -spec process_iq_vcard(jid(), iq(), state()) -> {result, vcard_temp() | xmlel()} | {result, undefined, state()} | {error, stanza_error()}. process_iq_vcard(_From, #iq{type = get}, StateData) -> #state{config = #config{vcard = VCardRaw}} = StateData, case fxml_stream:parse_element(VCardRaw) of #xmlel{} = VCard -> {result, VCard}; {error, _} -> {error, xmpp:err_item_not_found()} end; process_iq_vcard(From, #iq{type = set, lang = Lang, sub_els = [Pkt]}, StateData) -> case get_affiliation(From, StateData) of owner -> SubEl = xmpp:encode(Pkt), VCardRaw = fxml:element_to_binary(SubEl), Hash = mod_vcard_xupdate:compute_hash(SubEl), Config = StateData#state.config, NewConfig = Config#config{vcard = VCardRaw, vcard_xupdate = Hash}, change_config(NewConfig, StateData); _ -> ErrText = ?T("Owner privileges required"), {error, xmpp:err_forbidden(ErrText, Lang)} end. -spec process_iq_mucsub(jid(), iq(), state()) -> {error, stanza_error()} | {result, undefined | muc_subscribe() | muc_subscriptions(), stop | state()} | {ignore, state()}. process_iq_mucsub(_From, #iq{type = set, lang = Lang, sub_els = [#muc_subscribe{}]}, #state{just_created = Just, config = #config{allow_subscription = false}}) when Just /= true -> {error, xmpp:err_not_allowed(?T("Subscriptions are not allowed"), Lang)}; process_iq_mucsub(From, #iq{type = set, lang = Lang, sub_els = [#muc_subscribe{jid = #jid{} = SubJid} = Mucsub]}, StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> process_iq_mucsub(SubJid, #iq{type = set, lang = Lang, sub_els = [Mucsub#muc_subscribe{jid = undefined}]}, StateData); true -> Txt = ?T("Moderator privileges required"), {error, xmpp:err_forbidden(Txt, Lang)} end; process_iq_mucsub(From, #iq{type = set, lang = Lang, sub_els = [#muc_subscribe{nick = Nick}]} = Packet, StateData) -> LBareJID = jid:tolower(jid:remove_resource(From)), try muc_subscribers_get(LBareJID, StateData#state.muc_subscribers) of #subscriber{nick = Nick1} when Nick1 /= Nick -> Nodes = get_subscription_nodes(Packet), case nick_collision(From, Nick, StateData) of true -> ErrText = ?T("That nickname is already in use by another occupant"), {error, xmpp:err_conflict(ErrText, Lang)}; false -> case mod_muc:can_use_nick(StateData#state.server_host, StateData#state.host, From, Nick) of false -> Err = case Nick of <<>> -> xmpp:err_jid_malformed( ?T("Nickname can't be empty"), Lang); _ -> xmpp:err_conflict( ?T("That nickname is registered" " by another person"), Lang) end, {error, Err}; true -> NewStateData = set_subscriber(From, Nick, Nodes, StateData), {result, subscribe_result(Packet), NewStateData} end end; #subscriber{} -> Nodes = get_subscription_nodes(Packet), NewStateData = set_subscriber(From, Nick, Nodes, StateData), {result, subscribe_result(Packet), NewStateData} catch _:{badkey, _} -> SD2 = StateData#state{config = (StateData#state.config)#config{allow_subscription = true}}, add_new_user(From, Nick, Packet, SD2) end; process_iq_mucsub(From, #iq{type = set, lang = Lang, sub_els = [#muc_unsubscribe{jid = #jid{} = UnsubJid}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), if FRole == moderator; FAffiliation == owner; FAffiliation == admin -> process_iq_mucsub(UnsubJid, #iq{type = set, lang = Lang, sub_els = [#muc_unsubscribe{jid = undefined}]}, StateData); true -> Txt = ?T("Moderator privileges required"), {error, xmpp:err_forbidden(Txt, Lang)} end; process_iq_mucsub(From, #iq{type = set, sub_els = [#muc_unsubscribe{}]}, #state{room = Room, host = Host, server_host = ServerHost} = StateData) -> BareJID = jid:remove_resource(From), LBareJID = jid:tolower(BareJID), try muc_subscribers_remove_exn(LBareJID, StateData#state.muc_subscribers) of {MUCSubscribers, #subscriber{nick = Nick}} -> NewStateData = StateData#state{muc_subscribers = MUCSubscribers}, store_room(NewStateData, [{del_subscription, LBareJID}]), send_subscriptions_change_notifications(BareJID, Nick, unsubscribe, StateData), ejabberd_hooks:run(muc_unsubscribed, ServerHost, [ServerHost, Room, Host, BareJID]), NewStateData2 = case close_room_if_temporary_and_empty(NewStateData) of {stop, normal, _} -> stop; {next_state, normal_state, SD} -> SD end, {result, undefined, NewStateData2} catch _:{badkey, _} -> {result, undefined, StateData} end; process_iq_mucsub(From, #iq{type = get, lang = Lang, sub_els = [#muc_subscriptions{}]}, StateData) -> FAffiliation = get_affiliation(From, StateData), FRole = get_role(From, StateData), IsModerator = FRole == moderator orelse FAffiliation == owner orelse FAffiliation == admin, case IsModerator orelse is_subscriber(From, StateData) of true -> ShowJid = IsModerator orelse (StateData#state.config)#config.anonymous == false, Subs = muc_subscribers_fold( fun(_, #subscriber{jid = J, nick = N, nodes = Nodes}, Acc) -> case ShowJid of true -> [#muc_subscription{jid = J, nick = N, events = Nodes}|Acc]; _ -> [#muc_subscription{nick = N, events = Nodes}|Acc] end end, [], StateData#state.muc_subscribers), {result, #muc_subscriptions{list = Subs}, StateData}; _ -> Txt = ?T("Moderator privileges required"), {error, xmpp:err_forbidden(Txt, Lang)} end; process_iq_mucsub(_From, #iq{type = get, lang = Lang}, _StateData) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), {error, xmpp:err_bad_request(Txt, Lang)}. -spec remove_subscriptions(state()) -> state(). remove_subscriptions(StateData) -> if not (StateData#state.config)#config.allow_subscription -> StateData#state{muc_subscribers = muc_subscribers_new()}; true -> StateData end. -spec get_subscription_nodes(stanza()) -> [binary()]. get_subscription_nodes(#iq{sub_els = [#muc_subscribe{events = Nodes}]}) -> lists:filter( fun(Node) -> lists:member(Node, [?NS_MUCSUB_NODES_PRESENCE, ?NS_MUCSUB_NODES_MESSAGES, ?NS_MUCSUB_NODES_AFFILIATIONS, ?NS_MUCSUB_NODES_SUBJECT, ?NS_MUCSUB_NODES_CONFIG, ?NS_MUCSUB_NODES_PARTICIPANTS, ?NS_MUCSUB_NODES_SUBSCRIBERS]) end, Nodes); get_subscription_nodes(_) -> []. -spec subscribe_result(iq()) -> muc_subscribe(). subscribe_result(#iq{sub_els = [#muc_subscribe{nick = Nick}]} = Packet) -> #muc_subscribe{nick = Nick, events = get_subscription_nodes(Packet)}. -spec get_title(state()) -> binary(). get_title(StateData) -> case (StateData#state.config)#config.title of <<"">> -> StateData#state.room; Name -> Name end. -spec get_roomdesc_reply(jid(), state(), binary()) -> {item, binary()} | false. get_roomdesc_reply(JID, StateData, Tail) -> IsOccupantOrAdmin = is_occupant_or_admin(JID, StateData), if (StateData#state.config)#config.public or IsOccupantOrAdmin -> if (StateData#state.config)#config.public_list or IsOccupantOrAdmin -> {item, <<(get_title(StateData))/binary,Tail/binary>>}; true -> {item, get_title(StateData)} end; true -> false end. -spec get_roomdesc_tail(state(), binary()) -> binary(). get_roomdesc_tail(StateData, Lang) -> Desc = case (StateData#state.config)#config.public of true -> <<"">>; _ -> translate:translate(Lang, ?T("private, ")) end, Len = maps:size(StateData#state.nicks), <<" (", Desc/binary, (integer_to_binary(Len))/binary, ")">>. -spec get_mucroom_disco_items(state()) -> disco_items(). get_mucroom_disco_items(StateData) -> Items = maps:fold( fun(Nick, _, Acc) -> [#disco_item{jid = jid:make(StateData#state.room, StateData#state.host, Nick), name = Nick}|Acc] end, [], StateData#state.nicks), #disco_items{items = Items}. -spec process_iq_adhoc(jid(), iq(), state()) -> {result, adhoc_command()} | {result, adhoc_command(), state()} | {error, stanza_error()}. process_iq_adhoc(_From, #iq{type = get}, _StateData) -> {error, xmpp:err_bad_request()}; process_iq_adhoc(From, #iq{type = set, lang = Lang1, sub_els = [#adhoc_command{} = Request]}, StateData) -> % Ad-Hoc Commands are used only for Hats here case (StateData#state.config)#config.enable_hats andalso is_admin(From, StateData) of true -> #adhoc_command{lang = Lang2, node = Node, action = Action, xdata = XData} = Request, Lang = case Lang2 of <<"">> -> Lang1; _ -> Lang2 end, case {Node, Action} of {_, cancel} -> {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{status = canceled, lang = Lang, node = Node})}; {?MUC_HAT_ADD_CMD, execute} -> Form = #xdata{ title = translate:translate( Lang, ?T("Add a hat to a user")), type = form, fields = [#xdata_field{ type = 'jid-single', label = translate:translate(Lang, ?T("Jabber ID")), required = true, var = <<"jid">>}, #xdata_field{ type = 'text-single', label = translate:translate(Lang, ?T("Hat title")), var = <<"hat_title">>}, #xdata_field{ type = 'text-single', label = translate:translate(Lang, ?T("Hat URI")), required = true, var = <<"hat_uri">>} ]}, {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{ status = executing, xdata = Form})}; {?MUC_HAT_ADD_CMD, complete} when XData /= undefined -> JID = try jid:decode(hd(xmpp_util:get_xdata_values( <<"jid">>, XData))) catch _:_ -> error end, URI = try hd(xmpp_util:get_xdata_values( <<"hat_uri">>, XData)) catch _:_ -> error end, Title = case xmpp_util:get_xdata_values( <<"hat_title">>, XData) of [] -> <<"">>; [T] -> T end, if (JID /= error) and (URI /= error) -> case add_hat(JID, URI, Title, StateData) of {ok, NewStateData} -> store_room(NewStateData), send_update_presence( JID, NewStateData, StateData), {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{status = completed}), NewStateData}; {error, size_limit} -> Txt = ?T("Hats limit exceeded"), {error, xmpp:err_not_allowed(Txt, Lang)} end; true -> {error, xmpp:err_bad_request()} end; {?MUC_HAT_ADD_CMD, complete} -> {error, xmpp:err_bad_request()}; {?MUC_HAT_ADD_CMD, _} -> Txt = ?T("Incorrect value of 'action' attribute"), {error, xmpp:err_bad_request(Txt, Lang)}; {?MUC_HAT_REMOVE_CMD, execute} -> Form = #xdata{ title = translate:translate( Lang, ?T("Remove a hat from a user")), type = form, fields = [#xdata_field{ type = 'jid-single', label = translate:translate(Lang, ?T("Jabber ID")), required = true, var = <<"jid">>}, #xdata_field{ type = 'text-single', label = translate:translate(Lang, ?T("Hat URI")), required = true, var = <<"hat_uri">>} ]}, {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{ status = executing, xdata = Form})}; {?MUC_HAT_REMOVE_CMD, complete} when XData /= undefined -> JID = try jid:decode(hd(xmpp_util:get_xdata_values( <<"jid">>, XData))) catch _:_ -> error end, URI = try hd(xmpp_util:get_xdata_values( <<"hat_uri">>, XData)) catch _:_ -> error end, if (JID /= error) and (URI /= error) -> NewStateData = del_hat(JID, URI, StateData), store_room(NewStateData), send_update_presence( JID, NewStateData, StateData), {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{status = completed}), NewStateData}; true -> {error, xmpp:err_bad_request()} end; {?MUC_HAT_REMOVE_CMD, complete} -> {error, xmpp:err_bad_request()}; {?MUC_HAT_REMOVE_CMD, _} -> Txt = ?T("Incorrect value of 'action' attribute"), {error, xmpp:err_bad_request(Txt, Lang)}; {?MUC_HAT_LIST_CMD, execute} -> Hats = get_all_hats(StateData), Items = lists:map( fun({JID, URI, Title}) -> [#xdata_field{ var = <<"jid">>, values = [jid:encode(JID)]}, #xdata_field{ var = <<"hat_title">>, values = [URI]}, #xdata_field{ var = <<"hat_uri">>, values = [Title]}] end, Hats), Form = #xdata{ title = translate:translate( Lang, ?T("List of users with hats")), type = result, reported = [#xdata_field{ label = translate:translate(Lang, ?T("Jabber ID")), var = <<"jid">>}, #xdata_field{ label = translate:translate(Lang, ?T("Hat title")), var = <<"hat_title">>}, #xdata_field{ label = translate:translate(Lang, ?T("Hat URI")), var = <<"hat_uri">>}], items = Items}, {result, xmpp_util:make_adhoc_response( Request, #adhoc_command{ status = completed, xdata = Form})}; {?MUC_HAT_LIST_CMD, _} -> Txt = ?T("Incorrect value of 'action' attribute"), {error, xmpp:err_bad_request(Txt, Lang)}; _ -> {error, xmpp:err_item_not_found()} end; _ -> {error, xmpp:err_forbidden()} end. -spec add_hat(jid(), binary(), binary(), state()) -> {ok, state()} | {error, size_limit}. add_hat(JID, URI, Title, StateData) -> Hats = StateData#state.hats_users, LJID = jid:remove_resource(jid:tolower(JID)), UserHats = maps:get(LJID, Hats, #{}), UserHats2 = maps:put(URI, Title, UserHats), USize = maps:size(UserHats2), if USize =< ?MAX_HATS_PER_USER -> Hats2 = maps:put(LJID, UserHats2, Hats), Size = maps:size(Hats2), if Size =< ?MAX_HATS_USERS -> {ok, StateData#state{hats_users = Hats2}}; true -> {error, size_limit} end; true -> {error, size_limit} end. -spec del_hat(jid(), binary(), state()) -> state(). del_hat(JID, URI, StateData) -> Hats = StateData#state.hats_users, LJID = jid:remove_resource(jid:tolower(JID)), UserHats = maps:get(LJID, Hats, #{}), UserHats2 = maps:remove(URI, UserHats), Hats2 = case maps:size(UserHats2) of 0 -> maps:remove(LJID, Hats); _ -> maps:put(LJID, UserHats2, Hats) end, StateData#state{hats_users = Hats2}. -spec get_all_hats(state()) -> list({jid(), binary(), binary()}). get_all_hats(StateData) -> lists:flatmap( fun({LJID, H}) -> JID = jid:make(LJID), lists:map(fun({URI, Title}) -> {JID, URI, Title} end, maps:to_list(H)) end, maps:to_list(StateData#state.hats_users)). -spec add_presence_hats(jid(), #presence{}, state()) -> #presence{}. add_presence_hats(JID, Pres, StateData) -> case (StateData#state.config)#config.enable_hats of true -> Hats = StateData#state.hats_users, LJID = jid:remove_resource(jid:tolower(JID)), UserHats = maps:get(LJID, Hats, #{}), case maps:size(UserHats) of 0 -> Pres; _ -> Items = lists:map(fun({URI, Title}) -> #muc_hat{uri = URI, title = Title} end, maps:to_list(UserHats)), xmpp:set_subtag(Pres, #muc_hats{hats = Items}) end; false -> Pres end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Voice request support -spec prepare_request_form(jid(), binary(), binary()) -> message(). prepare_request_form(Requester, Nick, Lang) -> Title = translate:translate(Lang, ?T("Voice request")), Instruction = translate:translate( Lang, ?T("Either approve or decline the voice request.")), Fs = muc_request:encode([{role, participant}, {jid, Requester}, {roomnick, Nick}, {request_allow, false}], Lang), #message{type = normal, sub_els = [#xdata{type = form, title = Title, instructions = [Instruction], fields = Fs}]}. -spec send_voice_request(jid(), binary(), state()) -> ok. send_voice_request(From, Lang, StateData) -> Moderators = search_role(moderator, StateData), FromNick = find_nick_by_jid(From, StateData), lists:foreach( fun({_, User}) -> ejabberd_router:route( xmpp:set_from_to( prepare_request_form(From, FromNick, Lang), StateData#state.jid, User#user.jid)) end, Moderators). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Invitation support -spec check_invitation(jid(), [muc_invite()], binary(), state()) -> ok | {error, stanza_error()}. check_invitation(From, Invitations, Lang, StateData) -> FAffiliation = get_affiliation(From, StateData), CanInvite = (StateData#state.config)#config.allow_user_invites orelse FAffiliation == admin orelse FAffiliation == owner, case CanInvite of true -> case lists:all( fun(#muc_invite{to = #jid{}}) -> true; (_) -> false end, Invitations) of true -> ok; false -> Txt = ?T("No 'to' attribute found in the invitation"), {error, xmpp:err_bad_request(Txt, Lang)} end; false -> Txt = ?T("Invitations are not allowed in this conference"), {error, xmpp:err_not_allowed(Txt, Lang)} end. -spec route_invitation(jid(), message(), muc_invite(), binary(), state()) -> jid(). route_invitation(From, Pkt, Invitation, Lang, StateData) -> #muc_invite{to = JID, reason = Reason} = Invitation, Invite = Invitation#muc_invite{to = undefined, from = From}, Password = case (StateData#state.config)#config.password_protected of true -> (StateData#state.config)#config.password; false -> undefined end, XUser = #muc_user{password = Password, invites = [Invite]}, XConference = #x_conference{jid = jid:make(StateData#state.room, StateData#state.host), reason = Reason}, Body = iolist_to_binary( [io_lib:format( translate:translate( Lang, ?T("~s invites you to the room ~s")), [jid:encode(From), jid:encode({StateData#state.room, StateData#state.host, <<"">>})]), case (StateData#state.config)#config.password_protected of true -> <<", ", (translate:translate( Lang, ?T("the password is")))/binary, " '", ((StateData#state.config)#config.password)/binary, "'">>; _ -> <<"">> end, case Reason of <<"">> -> <<"">>; _ -> <<" (", Reason/binary, ") ">> end]), Msg = #message{from = StateData#state.jid, to = JID, type = normal, body = xmpp:mk_text(Body), sub_els = [XUser, XConference]}, Msg2 = ejabberd_hooks:run_fold(muc_invite, StateData#state.server_host, Msg, [StateData#state.jid, StateData#state.config, From, JID, Reason, Pkt]), ejabberd_router:route(Msg2), JID. %% Handle a message sent to the room by a non-participant. %% If it is a decline, send to the inviter. %% Otherwise, an error message is sent to the sender. -spec handle_roommessage_from_nonparticipant(message(), state(), jid()) -> ok. handle_roommessage_from_nonparticipant(Packet, StateData, From) -> try xmpp:try_subtag(Packet, #muc_user{}) of #muc_user{decline = #muc_decline{to = #jid{} = To} = Decline} = XUser -> NewDecline = Decline#muc_decline{to = undefined, from = From}, NewXUser = XUser#muc_user{decline = NewDecline}, NewPacket = xmpp:set_subtag(Packet, NewXUser), ejabberd_router:route( xmpp:set_from_to(NewPacket, StateData#state.jid, To)); _ -> ErrText = ?T("Only occupants are allowed to send messages " "to the conference"), Err = xmpp:err_not_acceptable(ErrText, xmpp:get_lang(Packet)), ejabberd_router:route_error(Packet, Err) catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Err = xmpp:err_bad_request(Txt, xmpp:get_lang(Packet)), ejabberd_router:route_error(Packet, Err) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Logging add_to_log(Type, Data, StateData) when Type == roomconfig_change_disabledlogging -> mod_muc_log:add_to_log(StateData#state.server_host, roomconfig_change, Data, StateData#state.jid, make_opts(StateData)); add_to_log(Type, Data, StateData) -> case (StateData#state.config)#config.logging of true -> mod_muc_log:add_to_log(StateData#state.server_host, Type, Data, StateData#state.jid, make_opts(StateData)); false -> ok end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Users number checking -spec tab_add_online_user(jid(), state()) -> any(). tab_add_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, ejabberd_hooks:run(join_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:register_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_remove_online_user(jid(), state()) -> any(). tab_remove_online_user(JID, StateData) -> Room = StateData#state.room, Host = StateData#state.host, ServerHost = StateData#state.server_host, ejabberd_hooks:run(leave_room, ServerHost, [ServerHost, Room, Host, JID]), mod_muc:unregister_online_user(ServerHost, jid:tolower(JID), Room, Host). -spec tab_count_user(jid(), state()) -> non_neg_integer(). tab_count_user(JID, StateData) -> ServerHost = StateData#state.server_host, {LUser, LServer, _} = jid:tolower(JID), mod_muc:count_online_rooms_by_user(ServerHost, LUser, LServer). -spec element_size(stanza()) -> non_neg_integer(). element_size(El) -> byte_size(fxml:element_to_binary(xmpp:encode(El, ?NS_CLIENT))). -spec store_room(state()) -> ok. store_room(StateData) -> store_room(StateData, []). store_room(StateData, ChangesHints) -> % Let store persistent rooms or on those backends that have get_subscribed_rooms Mod = gen_mod:db_mod(StateData#state.server_host, mod_muc), HasGSR = erlang:function_exported(Mod, get_subscribed_rooms, 3), case HasGSR of true -> ok; _ -> erlang:put(muc_subscribers, StateData#state.muc_subscribers#muc_subscribers.subscribers) end, ShouldStore = case (StateData#state.config)#config.persistent of true -> true; _ -> case ChangesHints of [] -> false; _ -> HasGSR end end, if ShouldStore -> case erlang:function_exported(Mod, store_changes, 4) of true when ChangesHints /= [] -> mod_muc:store_changes( StateData#state.server_host, StateData#state.host, StateData#state.room, ChangesHints); _ -> store_room_no_checks(StateData, ChangesHints) end; true -> ok end. store_room_no_checks(StateData, ChangesHints) -> mod_muc:store_room(StateData#state.server_host, StateData#state.host, StateData#state.room, make_opts(StateData), ChangesHints). -spec send_subscriptions_change_notifications(jid(), binary(), subscribe|unsubscribe, state()) -> ok. send_subscriptions_change_notifications(From, Nick, Type, State) -> {WJ, WN} = maps:fold( fun(_, #subscriber{jid = JID}, {WithJid, WithNick}) -> case (State#state.config)#config.anonymous == false orelse get_role(JID, State) == moderator orelse get_default_role(get_affiliation(JID, State), State) == moderator of true -> {[JID | WithJid], WithNick}; _ -> {WithJid, [JID | WithNick]} end end, {[], []}, muc_subscribers_get_by_node(?NS_MUCSUB_NODES_SUBSCRIBERS, State#state.muc_subscribers)), if WJ /= [] -> Payload1 = case Type of subscribe -> #muc_subscribe{jid = From, nick = Nick}; _ -> #muc_unsubscribe{jid = From, nick = Nick} end, Packet1 = #message{ sub_els = [#ps_event{ items = #ps_items{ node = ?NS_MUCSUB_NODES_SUBSCRIBERS, items = [#ps_item{ id = p1_rand:get_string(), sub_els = [Payload1]}]}}]}, ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, WJ, Packet1, false); true -> ok end, if WN /= [] -> Payload2 = case Type of subscribe -> #muc_subscribe{nick = Nick}; _ -> #muc_unsubscribe{nick = Nick} end, Packet2 = #message{ sub_els = [#ps_event{ items = #ps_items{ node = ?NS_MUCSUB_NODES_SUBSCRIBERS, items = [#ps_item{ id = p1_rand:get_string(), sub_els = [Payload2]}]}}]}, ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, WN, Packet2, false); true -> ok end. -spec send_wrapped(jid(), jid(), stanza(), binary(), state()) -> ok. send_wrapped(From, To, Packet, Node, State) -> LTo = jid:tolower(To), LBareTo = jid:tolower(jid:remove_resource(To)), IsOffline = case maps:get(LTo, State#state.users, error) of #user{last_presence = undefined} -> true; error -> true; _ -> false end, if IsOffline -> try muc_subscribers_get(LBareTo, State#state.muc_subscribers) of #subscriber{nodes = Nodes, jid = JID} -> case lists:member(Node, Nodes) of true -> MamEnabled = (State#state.config)#config.mam, Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of #stanza_id{id = Id2} -> Id2; _ -> p1_rand:get_string() end, NewPacket = wrap(From, JID, Packet, Node, Id), NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), ejabberd_router:route( xmpp:set_from_to(NewPacket2, State#state.jid, JID)); false -> ok end catch _:{badkey, _} -> ok end; true -> case Packet of #presence{type = unavailable} -> case xmpp:get_subtag(Packet, #muc_user{}) of #muc_user{destroy = Destroy, status_codes = Codes} -> case Destroy /= undefined orelse (lists:member(110,Codes) andalso not lists:member(303, Codes)) of true -> ejabberd_router:route( #presence{from = State#state.jid, to = To, id = p1_rand:get_string(), type = unavailable}); false -> ok end; _ -> false end; _ -> ok end, ejabberd_router:route(xmpp:set_from_to(Packet, From, To)) end. -spec wrap(jid(), undefined | jid(), stanza(), binary(), binary()) -> message(). wrap(From, To, Packet, Node, Id) -> El = xmpp:set_from_to(Packet, From, To), #message{ id = Id, sub_els = [#ps_event{ items = #ps_items{ node = Node, items = [#ps_item{ id = Id, sub_els = [El]}]}}]}. -spec send_wrapped_multiple(jid(), users(), stanza(), binary(), state()) -> ok. send_wrapped_multiple(From, Users, Packet, Node, State) -> {Dir, Wra} = maps:fold( fun(_, #user{jid = To, last_presence = LP}, {Direct, Wrapped} = Res) -> IsOffline = LP == undefined, if IsOffline -> LBareTo = jid:tolower(jid:remove_resource(To)), case muc_subscribers_find(LBareTo, State#state.muc_subscribers) of {ok, #subscriber{nodes = Nodes}} -> case lists:member(Node, Nodes) of true -> {Direct, [To | Wrapped]}; _ -> %% TODO: check that this branch is never called Res end; _ -> Res end; true -> {[To | Direct], Wrapped} end end, {[],[]}, Users), case Dir of [] -> ok; _ -> case Packet of #presence{type = unavailable} -> case xmpp:get_subtag(Packet, #muc_user{}) of #muc_user{destroy = Destroy, status_codes = Codes} -> case Destroy /= undefined orelse (lists:member(110,Codes) andalso not lists:member(303, Codes)) of true -> ejabberd_router_multicast:route_multicast( From, State#state.server_host, Dir, #presence{id = p1_rand:get_string(), type = unavailable}, false); false -> ok end; _ -> false end; _ -> ok end, ejabberd_router_multicast:route_multicast(From, State#state.server_host, Dir, Packet, false) end, case Wra of [] -> ok; _ -> MamEnabled = (State#state.config)#config.mam, Id = case xmpp:get_subtag(Packet, #stanza_id{by = #jid{}}) of #stanza_id{id = Id2} -> Id2; _ -> p1_rand:get_string() end, NewPacket = wrap(From, undefined, Packet, Node, Id), NewPacket2 = xmpp:put_meta(NewPacket, in_muc_mam, MamEnabled), ejabberd_router_multicast:route_multicast(State#state.jid, State#state.server_host, Wra, NewPacket2, true) end. %%%---------------------------------------------------------------------- %%% #muc_subscribers API %%%---------------------------------------------------------------------- -spec muc_subscribers_new() -> #muc_subscribers{}. muc_subscribers_new() -> #muc_subscribers{}. -spec muc_subscribers_get(ljid(), #muc_subscribers{}) -> #subscriber{}. muc_subscribers_get({_, _, _} = LJID, MUCSubscribers) -> maps:get(LJID, MUCSubscribers#muc_subscribers.subscribers). -spec muc_subscribers_find(ljid(), #muc_subscribers{}) -> {ok, #subscriber{}} | error. muc_subscribers_find({_, _, _} = LJID, MUCSubscribers) -> maps:find(LJID, MUCSubscribers#muc_subscribers.subscribers). -spec muc_subscribers_is_key(ljid(), #muc_subscribers{}) -> boolean(). muc_subscribers_is_key({_, _, _} = LJID, MUCSubscribers) -> maps:is_key(LJID, MUCSubscribers#muc_subscribers.subscribers). -spec muc_subscribers_size(#muc_subscribers{}) -> integer(). muc_subscribers_size(MUCSubscribers) -> maps:size(MUCSubscribers#muc_subscribers.subscribers). -spec muc_subscribers_fold(Fun, Acc, #muc_subscribers{}) -> Acc when Fun :: fun((ljid(), #subscriber{}, Acc) -> Acc). muc_subscribers_fold(Fun, Init, MUCSubscribers) -> maps:fold(Fun, Init, MUCSubscribers#muc_subscribers.subscribers). -spec muc_subscribers_get_by_nick(binary(), #muc_subscribers{}) -> [#subscriber{}]. muc_subscribers_get_by_nick(Nick, MUCSubscribers) -> maps:get(Nick, MUCSubscribers#muc_subscribers.subscriber_nicks, []). -spec muc_subscribers_get_by_node(binary(), #muc_subscribers{}) -> subscribers(). muc_subscribers_get_by_node(Node, MUCSubscribers) -> maps:get(Node, MUCSubscribers#muc_subscribers.subscriber_nodes, #{}). -spec muc_subscribers_remove_exn(ljid(), #muc_subscribers{}) -> {#muc_subscribers{}, #subscriber{}}. muc_subscribers_remove_exn({_, _, _} = LJID, MUCSubscribers) -> #muc_subscribers{subscribers = Subs, subscriber_nicks = SubNicks, subscriber_nodes = SubNodes} = MUCSubscribers, Subscriber = maps:get(LJID, Subs), #subscriber{nick = Nick, nodes = Nodes} = Subscriber, NewSubNicks = maps:remove(Nick, SubNicks), NewSubs = maps:remove(LJID, Subs), NewSubNodes = lists:foldl( fun(Node, Acc) -> NodeSubs = maps:get(Node, Acc, #{}), NodeSubs2 = maps:remove(LJID, NodeSubs), maps:put(Node, NodeSubs2, Acc) end, SubNodes, Nodes), {#muc_subscribers{subscribers = NewSubs, subscriber_nicks = NewSubNicks, subscriber_nodes = NewSubNodes}, Subscriber}. -spec muc_subscribers_put(#subscriber{}, #muc_subscribers{}) -> #muc_subscribers{}. muc_subscribers_put(Subscriber, MUCSubscribers) -> #subscriber{jid = JID, nick = Nick, nodes = Nodes} = Subscriber, #muc_subscribers{subscribers = Subs, subscriber_nicks = SubNicks, subscriber_nodes = SubNodes} = MUCSubscribers, LJID = jid:tolower(JID), NewSubs = maps:put(LJID, Subscriber, Subs), NewSubNicks = maps:put(Nick, [LJID], SubNicks), NewSubNodes = lists:foldl( fun(Node, Acc) -> NodeSubs = maps:get(Node, Acc, #{}), NodeSubs2 = maps:put(LJID, Subscriber, NodeSubs), maps:put(Node, NodeSubs2, Acc) end, SubNodes, Nodes), #muc_subscribers{subscribers = NewSubs, subscriber_nicks = NewSubNicks, subscriber_nodes = NewSubNodes}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Detect messange stanzas that don't have meaningful content -spec has_body_or_subject(message()) -> boolean(). has_body_or_subject(#message{body = Body, subject = Subj}) -> Body /= [] orelse Subj /= []. -spec reset_hibernate_timer(state()) -> state(). reset_hibernate_timer(State) -> case State#state.hibernate_timer of hibernating -> ok; _ -> disable_hibernate_timer(State), NewTimer = case {mod_muc_opt:hibernation_timeout(State#state.server_host), maps:size(State#state.users)} of {infinity, _} -> none; {Timeout, 0} -> p1_fsm:send_event_after(Timeout, hibernate); _ -> none end, State#state{hibernate_timer = NewTimer} end. -spec disable_hibernate_timer(state()) -> ok. disable_hibernate_timer(State) -> case State#state.hibernate_timer of Ref when is_reference(Ref) -> p1_fsm:cancel_timer(Ref), ok; _ -> ok end. ejabberd-21.12/src/eldap_pool.erl0000644000232200023220000000646314154362354017254 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : eldap_pool.erl %%% Author : Evgeniy Khramtsov %%% Purpose : LDAP connections pool %%% Created : 12 Nov 2006 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(eldap_pool). -author('xram@jabber.ru'). %% API -export([start_link/7, bind/3, search/2, modify_passwd/3]). -include("logger.hrl"). -ifdef(USE_OLD_PG2). pg_create(PoolName) -> pg2:create(PoolName). pg_join(PoolName, Pid) -> pg2:join(PoolName, Pid). pg_get_closest_pid(Name) -> pg2:get_closest_pid(Name). -else. pg_create(_) -> pg:start_link(). pg_join(PoolName, Pid) -> pg:join(PoolName, Pid). pg_get_closest_pid(Group) -> case pg:get_local_members(Group) of [] -> case pg:get_members(Group) of [] -> {error, {no_process, Group}}; [Pid | _] -> Pid end; [Pid | _] -> Pid end. -endif. %%==================================================================== %% API %%==================================================================== bind(PoolName, DN, Passwd) -> do_request(PoolName, {bind, [DN, Passwd]}). search(PoolName, Opts) -> do_request(PoolName, {search, [Opts]}). modify_passwd(PoolName, DN, Passwd) -> do_request(PoolName, {modify_passwd, [DN, Passwd]}). start_link(Name, Hosts, Backups, Port, Rootdn, Passwd, Opts) -> PoolName = make_id(Name), pg_create(PoolName), lists:foreach(fun (Host) -> ID = list_to_binary(erlang:ref_to_list(make_ref())), case catch eldap:start_link(ID, [Host | Backups], Port, Rootdn, Passwd, Opts) of {ok, Pid} -> pg_join(PoolName, Pid); Err -> ?ERROR_MSG("Err = ~p", [Err]), error end end, Hosts). %%==================================================================== %% Internal functions %%==================================================================== do_request(Name, {F, Args}) -> case pg_get_closest_pid(make_id(Name)) of Pid when is_pid(Pid) -> case catch apply(eldap, F, [Pid | Args]) of {'EXIT', {timeout, _}} -> ?ERROR_MSG("LDAP request failed: timed out", []); {'EXIT', Reason} -> ?ERROR_MSG("LDAP request failed: eldap:~p(~p)~nReason: ~p", [F, Args, Reason]), {error, Reason}; Reply -> Reply end; Err -> Err end. make_id(Name) -> misc:binary_to_atom(<<"eldap_pool_", Name/binary>>). ejabberd-21.12/src/mod_vcard_mnesia.erl0000644000232200023220000002274714154362354020433 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_vcard_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_vcard_mnesia). -behaviour(mod_vcard). %% API -export([init/2, stop/1, import/3, get_vcard/2, set_vcard/4, search/4, search_fields/1, search_reported/1, remove_user/2]). -export([is_search_supported/1]). -export([need_transform/1, transform/1]). -export([mod_opt_type/1, mod_options/1, mod_doc/0]). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_vcard.hrl"). -include("logger.hrl"). -include("translate.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, vcard, [{disc_only_copies, [node()]}, {attributes, record_info(fields, vcard)}]), ejabberd_mnesia:create(?MODULE, vcard_search, [{disc_copies, [node()]}, {attributes, record_info(fields, vcard_search)}, {index, [ luser, lfn, lfamily, lgiven, lmiddle, lnickname, lbday, lctry, llocality, lemail, lorgname, lorgunit ]}]). stop(_Host) -> ok. is_search_supported(_ServerHost) -> true. get_vcard(LUser, LServer) -> US = {LUser, LServer}, Rs = mnesia:dirty_read(vcard, US), {ok, lists:map(fun (R) -> R#vcard.vcard end, Rs)}. set_vcard(LUser, LServer, VCARD, VCardSearch) -> US = {LUser, LServer}, F = fun () -> mnesia:write(#vcard{us = US, vcard = VCARD}), mnesia:write(VCardSearch) end, mnesia:transaction(F). search(LServer, Data, AllowReturnAll, MaxMatch) -> MatchSpec = make_matchspec(LServer, Data), if (MatchSpec == #vcard_search{_ = '_'}) and not AllowReturnAll -> []; true -> case catch mnesia:dirty_select(vcard_search, [{MatchSpec, [], ['$_']}]) of {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; Rs -> Fields = lists:map(fun record_to_item/1, Rs), case MaxMatch of infinity -> Fields; Val -> lists:sublist(Fields, Val) end end end. search_fields(_LServer) -> [{?T("User"), <<"user">>}, {?T("Full Name"), <<"fn">>}, {?T("Name"), <<"first">>}, {?T("Middle Name"), <<"middle">>}, {?T("Family Name"), <<"last">>}, {?T("Nickname"), <<"nick">>}, {?T("Birthday"), <<"bday">>}, {?T("Country"), <<"ctry">>}, {?T("City"), <<"locality">>}, {?T("Email"), <<"email">>}, {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. search_reported(_LServer) -> [{?T("Jabber ID"), <<"jid">>}, {?T("Full Name"), <<"fn">>}, {?T("Name"), <<"first">>}, {?T("Middle Name"), <<"middle">>}, {?T("Family Name"), <<"last">>}, {?T("Nickname"), <<"nick">>}, {?T("Birthday"), <<"bday">>}, {?T("Country"), <<"ctry">>}, {?T("City"), <<"locality">>}, {?T("Email"), <<"email">>}, {?T("Organization Name"), <<"orgname">>}, {?T("Organization Unit"), <<"orgunit">>}]. remove_user(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> mnesia:delete({vcard, US}), mnesia:delete({vcard_search, US}) end, mnesia:transaction(F). import(LServer, <<"vcard">>, [LUser, XML, _TimeStamp]) -> #xmlel{} = El = fxml_stream:parse_element(XML), VCard = #vcard{us = {LUser, LServer}, vcard = El}, mnesia:dirty_write(VCard); import(LServer, <<"vcard_search">>, [User, LUser, FN, LFN, Family, LFamily, Given, LGiven, Middle, LMiddle, Nickname, LNickname, BDay, LBDay, CTRY, LCTRY, Locality, LLocality, EMail, LEMail, OrgName, LOrgName, OrgUnit, LOrgUnit]) -> mnesia:dirty_write( #vcard_search{us = {LUser, LServer}, user = {User, LServer}, luser = LUser, fn = FN, lfn = LFN, family = Family, lfamily = LFamily, given = Given, lgiven = LGiven, middle = Middle, lmiddle = LMiddle, nickname = Nickname, lnickname = LNickname, bday = BDay, lbday = LBDay, ctry = CTRY, lctry = LCTRY, locality = Locality, llocality = LLocality, email = EMail, lemail = LEMail, orgname = OrgName, lorgname = LOrgName, orgunit = OrgUnit, lorgunit = LOrgUnit}). need_transform({vcard, {U, S}, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'vcard' will be converted to binary", []), true; need_transform(R) when element(1, R) == vcard_search -> case element(2, R) of {U, S} when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'vcard_search' will be converted to binary", []), true; _ -> false end; need_transform(_) -> false. transform(#vcard{us = {U, S}, vcard = El} = R) -> R#vcard{us = {iolist_to_binary(U), iolist_to_binary(S)}, vcard = fxml:to_xmlel(El)}; transform(#vcard_search{} = VS) -> [vcard_search | L] = tuple_to_list(VS), NewL = lists:map( fun({U, S}) -> {iolist_to_binary(U), iolist_to_binary(S)}; (Str) -> iolist_to_binary(Str) end, L), list_to_tuple([vcard_search | NewL]). %%%=================================================================== %%% Internal functions %%%=================================================================== make_matchspec(LServer, Data) -> GlobMatch = #vcard_search{_ = '_'}, Match = filter_fields(Data, GlobMatch, LServer), Match. filter_fields([], Match, _LServer) -> Match; filter_fields([{SVar, [Val]} | Ds], Match, LServer) when is_binary(Val) and (Val /= <<"">>) -> LVal = mod_vcard:string2lower(Val), NewMatch = case SVar of <<"user">> -> case mod_vcard_mnesia_opt:search_all_hosts(LServer) of true -> Match#vcard_search{luser = make_val(LVal)}; false -> Host = find_my_host(LServer), Match#vcard_search{us = {make_val(LVal), Host}} end; <<"fn">> -> Match#vcard_search{lfn = make_val(LVal)}; <<"last">> -> Match#vcard_search{lfamily = make_val(LVal)}; <<"first">> -> Match#vcard_search{lgiven = make_val(LVal)}; <<"middle">> -> Match#vcard_search{lmiddle = make_val(LVal)}; <<"nick">> -> Match#vcard_search{lnickname = make_val(LVal)}; <<"bday">> -> Match#vcard_search{lbday = make_val(LVal)}; <<"ctry">> -> Match#vcard_search{lctry = make_val(LVal)}; <<"locality">> -> Match#vcard_search{llocality = make_val(LVal)}; <<"email">> -> Match#vcard_search{lemail = make_val(LVal)}; <<"orgname">> -> Match#vcard_search{lorgname = make_val(LVal)}; <<"orgunit">> -> Match#vcard_search{lorgunit = make_val(LVal)}; _ -> Match end, filter_fields(Ds, NewMatch, LServer); filter_fields([_ | Ds], Match, LServer) -> filter_fields(Ds, Match, LServer). make_val(Val) -> case str:suffix(<<"*">>, Val) of true -> [str:substr(Val, 1, byte_size(Val) - 1)] ++ '_'; _ -> Val end. find_my_host(LServer) -> Parts = str:tokens(LServer, <<".">>), find_my_host(Parts, ejabberd_option:hosts()). find_my_host([], _Hosts) -> ejabberd_config:get_myname(); find_my_host([_ | Tail] = Parts, Hosts) -> Domain = parts_to_string(Parts), case lists:member(Domain, Hosts) of true -> Domain; false -> find_my_host(Tail, Hosts) end. parts_to_string(Parts) -> str:strip(list_to_binary( lists:map(fun (S) -> <> end, Parts)), right, $.). -spec record_to_item(#vcard_search{}) -> [{binary(), binary()}]. record_to_item(R) -> {User, Server} = R#vcard_search.user, [{<<"jid">>, <>}, {<<"fn">>, (R#vcard_search.fn)}, {<<"last">>, (R#vcard_search.family)}, {<<"first">>, (R#vcard_search.given)}, {<<"middle">>, (R#vcard_search.middle)}, {<<"nick">>, (R#vcard_search.nickname)}, {<<"bday">>, (R#vcard_search.bday)}, {<<"ctry">>, (R#vcard_search.ctry)}, {<<"locality">>, (R#vcard_search.locality)}, {<<"email">>, (R#vcard_search.email)}, {<<"orgname">>, (R#vcard_search.orgname)}, {<<"orgunit">>, (R#vcard_search.orgunit)}]. mod_opt_type(search_all_hosts) -> econf:bool(). mod_options(_) -> [{search_all_hosts, true}]. mod_doc() -> #{opts => [{search_all_hosts, #{value => "true | false", desc => ?T("Whether to perform search on all " "virtual hosts or not. The default " "value is 'true'.")}}]}. ejabberd-21.12/src/ejabberd_web_admin.erl0000644000232200023220000021107614154362354020677 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_web_admin.erl %%% Author : Alexey Shchepin %%% Purpose : Administration web interface %%% Created : 9 Apr 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%%% definitions -module(ejabberd_web_admin). -author('alexey@process-one.net'). -export([process/2, list_users/4, list_users_in_diapason/4, pretty_print_xml/1, term_to_id/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("translate.hrl"). -define(INPUTATTRS(Type, Name, Value, Attrs), ?XA(<<"input">>, (Attrs ++ [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}]))). %%%================================== %%%% get_acl_access %% @spec (Path::[string()], Method) -> {HostOfRule, [AccessRule]} %% where Method = 'GET' | 'POST' %% All accounts can access those URLs get_acl_rule([], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"style.css">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"logo.png">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"logo-fill.png">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"favicon.ico">>], _) -> {<<"localhost">>, [all]}; get_acl_rule([<<"additions.js">>], _) -> {<<"localhost">>, [all]}; %% This page only displays vhosts that the user is admin: get_acl_rule([<<"vhosts">>], _) -> {<<"localhost">>, [all]}; %% The pages of a vhost are only accessible if the user is admin of that vhost: get_acl_rule([<<"server">>, VHost | _RPath], Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> {VHost, [configure, webadmin_view]}; get_acl_rule([<<"server">>, VHost | _RPath], 'POST') -> {VHost, [configure]}; %% Default rule: only global admins can access any other random page get_acl_rule(_RPath, Method) when Method =:= 'GET' orelse Method =:= 'HEAD' -> {global, [configure, webadmin_view]}; get_acl_rule(_RPath, 'POST') -> {global, [configure]}. %%%================================== %%%% Menu Items Access get_jid(Auth, HostHTTP, Method) -> case get_auth_admin(Auth, HostHTTP, [], Method) of {ok, {User, Server}} -> jid:make(User, Server); {unauthorized, Error} -> ?ERROR_MSG("Unauthorized ~p: ~p", [Auth, Error]), throw({unauthorized, Auth}) end. get_menu_items(global, cluster, Lang, JID, Level) -> {_Base, _, Items} = make_server_menu([], [], Lang, JID, Level), lists:map(fun ({URI, Name}) -> {<>, Name}; ({URI, Name, _SubMenu}) -> {<>, Name} end, Items); get_menu_items(Host, cluster, Lang, JID, Level) -> {_Base, _, Items} = make_host_menu(Host, [], Lang, JID, Level), lists:map(fun ({URI, Name}) -> {<>, Name}; ({URI, Name, _SubMenu}) -> {<>, Name} end, Items). %% get_menu_items(Host, Node, Lang, JID) -> %% {Base, _, Items} = make_host_node_menu(Host, Node, Lang, JID), %% lists:map( %% fun({URI, Name}) -> %% {Base++URI++"/", Name}; %% ({URI, Name, _SubMenu}) -> %% {Base++URI++"/", Name} %% end, %% Items %% ). is_allowed_path(global, RPath, JID) -> is_allowed_path([], RPath, JID); is_allowed_path(Host, RPath, JID) when is_binary(Host) -> is_allowed_path([<<"server">>, Host], RPath, JID); is_allowed_path(BasePath, {Path, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID); is_allowed_path(BasePath, {Path, _, _}, JID) -> is_allowed_path(BasePath ++ [Path], JID). is_allowed_path([<<"admin">> | Path], JID) -> is_allowed_path(Path, JID); is_allowed_path(Path, JID) -> {HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'), any_rules_allowed(HostOfRule, AccessRule, JID). %%%================================== %%%% process/2 process(Path, #request{raw_path = RawPath} = Request) -> Continue = case Path of [E] -> binary:match(E, <<".">>) /= nomatch; _ -> false end, case Continue orelse binary:at(RawPath, size(RawPath) - 1) == $/ of true -> process2(Path, Request); _ -> {301, [{<<"Location">>, <>}], <<>>} end. process2([<<"server">>, SHost | RPath] = Path, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> Host = jid:nameprep(SHost), case ejabberd_router:is_my_host(Host) of true -> case get_auth_admin(Auth, HostHTTP, Path, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(Host, Request#request{path = RPath, us = {User, Server}}, AJID); {unauthorized, <<"no-auth-provided">>} -> {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], ejabberd_web:make_xhtml([?XCT(<<"h1">>, ?T("Unauthorized"))])}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, IPS = ejabberd_config:may_hide_data(misc:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], ejabberd_web:make_xhtml([?XCT(<<"h1">>, ?T("Unauthorized"))])} end; false -> ejabberd_web:error(not_found) end; process2(RPath, #request{auth = Auth, lang = Lang, host = HostHTTP, method = Method} = Request) -> case get_auth_admin(Auth, HostHTTP, RPath, Method) of {ok, {User, Server}} -> AJID = get_jid(Auth, HostHTTP, Method), process_admin(global, Request#request{path = RPath, us = {User, Server}}, AJID); {unauthorized, <<"no-auth-provided">>} -> {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"ejabberd\"">>}], ejabberd_web:make_xhtml([?XCT(<<"h1">>, ?T("Unauthorized"))])}; {unauthorized, Error} -> {BadUser, _BadPass} = Auth, {IPT, _Port} = Request#request.ip, IPS = ejabberd_config:may_hide_data(misc:ip_to_list(IPT)), ?WARNING_MSG("Access of ~p from ~p failed with error: ~p", [BadUser, IPS, Error]), {401, [{<<"WWW-Authenticate">>, <<"basic realm=\"auth error, retry login " "to ejabberd\"">>}], ejabberd_web:make_xhtml([?XCT(<<"h1">>, ?T("Unauthorized"))])} end. get_auth_admin(Auth, HostHTTP, RPath, Method) -> case Auth of {SJID, Pass} -> {HostOfRule, AccessRule} = get_acl_rule(RPath, Method), try jid:decode(SJID) of #jid{user = <<"">>, server = User} -> case ejabberd_router:is_my_host(HostHTTP) of true -> get_auth_account(HostOfRule, AccessRule, User, HostHTTP, Pass); _ -> {unauthorized, <<"missing-server">>} end; #jid{user = User, server = Server} -> get_auth_account(HostOfRule, AccessRule, User, Server, Pass) catch _:{bad_jid, _} -> {unauthorized, <<"badformed-jid">>} end; invalid -> {unauthorized, <<"no-auth-provided">>}; undefined -> {unauthorized, <<"no-auth-provided">>} end. get_auth_account(HostOfRule, AccessRule, User, Server, Pass) -> case lists:member(Server, ejabberd_config:get_option(hosts)) of true -> get_auth_account2(HostOfRule, AccessRule, User, Server, Pass); false -> {unauthorized, <<"inexistent-host">>} end. get_auth_account2(HostOfRule, AccessRule, User, Server, Pass) -> case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of true -> case any_rules_allowed(HostOfRule, AccessRule, jid:make(User, Server)) of false -> {unauthorized, <<"unprivileged-account">>}; true -> {ok, {User, Server}} end; false -> case ejabberd_auth:user_exists(User, Server) of true -> {unauthorized, <<"bad-password">>}; false -> {unauthorized, <<"inexistent-account">>} end end. %%%================================== %%%% make_xhtml make_xhtml(Els, Host, Lang, JID, Level) -> make_xhtml(Els, Host, cluster, Lang, JID, Level). %% @spec (Els, Host, Node, Lang, JID, Level::integer()) -> {200, [html], xmlelement()} %% where Host = global | string() %% Node = cluster | atom() %% JID = jid() make_xhtml(Els, Host, Node, Lang, JID, Level) -> Base = get_base_path_sum(0, 0, Level), MenuItems = make_navigation(Host, Node, Lang, JID, Level), {200, [html], #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}, {<<"xml:lang">>, Lang}, {<<"lang">>, Lang}]++direction(Lang), children = [#xmlel{name = <<"head">>, attrs = [], children = [?XCT(<<"title">>, ?T("ejabberd Web Admin")), #xmlel{name = <<"meta">>, attrs = [{<<"http-equiv">>, <<"Content-Type">>}, {<<"content">>, <<"text/html; charset=utf-8">>}], children = []}, #xmlel{name = <<"script">>, attrs = [{<<"src">>, <>}, {<<"type">>, <<"text/javascript">>}], children = [?C(<<" ">>)]}, #xmlel{name = <<"link">>, attrs = [{<<"href">>, <>}, {<<"type">>, <<"image/x-icon">>}, {<<"rel">>, <<"shortcut icon">>}], children = []}, #xmlel{name = <<"link">>, attrs = [{<<"href">>, <>}, {<<"type">>, <<"text/css">>}, {<<"rel">>, <<"stylesheet">>}], children = []}]}, ?XE(<<"body">>, [?XAE(<<"div">>, [{<<"id">>, <<"container">>}], [?XAE(<<"div">>, [{<<"id">>, <<"header">>}], [?XE(<<"h1">>, [?ACT(Base, <<"ejabberd Web Admin">>)])]), ?XAE(<<"div">>, [{<<"id">>, <<"navigation">>}], [?XE(<<"ul">>, MenuItems)]), ?XAE(<<"div">>, [{<<"id">>, <<"content">>}], Els), ?XAE(<<"div">>, [{<<"id">>, <<"clearcopyright">>}], [{xmlcdata, <<"">>}])]), ?XAE(<<"div">>, [{<<"id">>, <<"copyrightouter">>}], [?XAE(<<"div">>, [{<<"id">>, <<"copyright">>}], [?XE(<<"p">>, [?AC(<<"https://www.ejabberd.im/">>, <<"ejabberd">>), ?C(<<" ">>), ?C(ejabberd_option:version()), ?C(<<" (c) 2002-2021 ">>), ?AC(<<"https://www.process-one.net/">>, <<"ProcessOne, leader in messaging and push solutions">>)] )])])])]}}. direction(<<"he">>) -> [{<<"dir">>, <<"rtl">>}]; direction(_) -> []. get_base_path(Host, Node, Level) -> SumHost = case Host of global -> 0; _ -> -2 end, SumNode = case Node of cluster -> 0; _ -> -2 end, get_base_path_sum(SumHost, SumNode, Level). get_base_path_sum(SumHost, SumNode, Level) -> iolist_to_binary(lists:duplicate(Level + SumHost + SumNode, "../")). %%%================================== %%%% css & images additions_js() -> case misc:read_js("admin.js") of {ok, JS} -> JS; {error, _} -> <<>> end. css(Host) -> case misc:read_css("admin.css") of {ok, CSS} -> Base = get_base_path(Host, cluster, 0), re:replace(CSS, <<"@BASE@">>, Base, [{return, binary}]); {error, _} -> <<>> end. favicon() -> case misc:read_img("favicon.png") of {ok, ICO} -> ICO; {error, _} -> <<>> end. logo() -> case misc:read_img("admin-logo.png") of {ok, Img} -> Img; {error, _} -> <<>> end. logo_fill() -> case misc:read_img("admin-logo-fill.png") of {ok, Img} -> Img; {error, _} -> <<>> end. %%%================================== %%%% process_admin process_admin(global, #request{path = [], lang = Lang}, AJID) -> make_xhtml((?H1GL((translate:translate(Lang, ?T("Administration"))), <<"">>, <<"Contents">>)) ++ [?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(global, cluster, Lang, AJID, 0)])], global, Lang, AJID, 0); process_admin(Host, #request{path = [], lang = Lang}, AJID) -> make_xhtml([?XCT(<<"h1">>, ?T("Administration")), ?XE(<<"ul">>, [?LI([?ACT(MIU, MIN)]) || {MIU, MIN} <- get_menu_items(Host, cluster, Lang, AJID, 2)])], Host, Lang, AJID, 2); process_admin(Host, #request{path = [<<"style.css">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/css">>}, last_modified(), cache_control_public()], css(Host)}; process_admin(_Host, #request{path = [<<"favicon.ico">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/x-icon">>}, last_modified(), cache_control_public()], favicon()}; process_admin(_Host, #request{path = [<<"logo.png">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/png">>}, last_modified(), cache_control_public()], logo()}; process_admin(_Host, #request{path = [<<"logo-fill.png">>]}, _) -> {200, [{<<"Content-Type">>, <<"image/png">>}, last_modified(), cache_control_public()], logo_fill()}; process_admin(_Host, #request{path = [<<"additions.js">>]}, _) -> {200, [{<<"Content-Type">>, <<"text/javascript">>}, last_modified(), cache_control_public()], additions_js()}; process_admin(global, #request{path = [<<"vhosts">>], lang = Lang}, AJID) -> Res = list_vhosts(Lang, AJID), make_xhtml((?H1GL((translate:translate(Lang, ?T("Virtual Hosts"))), <<"basic/#xmpp-domains">>, ?T("XMPP Domains"))) ++ Res, global, Lang, AJID, 1); process_admin(Host, #request{path = [<<"users">>], q = Query, lang = Lang}, AJID) when is_binary(Host) -> Res = list_users(Host, Query, Lang, fun url_func/1), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"users">>, Diap], lang = Lang}, AJID) when is_binary(Host) -> Res = list_users_in_diapason(Host, Diap, Lang, fun url_func/1), make_xhtml([?XCT(<<"h1">>, ?T("Users"))] ++ Res, Host, Lang, AJID, 4); process_admin(Host, #request{path = [<<"online-users">>], lang = Lang}, AJID) when is_binary(Host) -> Res = list_online_users(Host, Lang), make_xhtml([?XCT(<<"h1">>, ?T("Online Users"))] ++ Res, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"last-activity">>], q = Query, lang = Lang}, AJID) when is_binary(Host) -> ?DEBUG("Query: ~p", [Query]), Month = case lists:keysearch(<<"period">>, 1, Query) of {value, {_, Val}} -> Val; _ -> <<"month">> end, Res = case lists:keysearch(<<"ordinary">>, 1, Query) of {value, {_, _}} -> list_last_activity(Host, Lang, false, Month); _ -> list_last_activity(Host, Lang, true, Month) end, PageH1 = ?H1GL(translate:translate(Lang, ?T("Users Last Activity")), <<"modules/#mod-last">>, <<"mod_last">>), make_xhtml(PageH1 ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?CT(?T("Period: ")), ?XAE(<<"select">>, [{<<"name">>, <<"period">>}], (lists:map(fun ({O, V}) -> Sel = if O == Month -> [{<<"selected">>, <<"selected">>}]; true -> [] end, ?XAC(<<"option">>, (Sel ++ [{<<"value">>, O}]), V) end, [{<<"month">>, translate:translate(Lang, ?T("Last month"))}, {<<"year">>, translate:translate(Lang, ?T("Last year"))}, {<<"all">>, translate:translate(Lang, ?T("All activity"))}]))), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"ordinary">>, ?T("Show Ordinary Table")), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"integral">>, ?T("Show Integral Table"))])] ++ Res, Host, Lang, AJID, 3); process_admin(Host, #request{path = [<<"stats">>], lang = Lang}, AJID) -> Res = get_stats(Host, Lang), PageH1 = ?H1GL(translate:translate(Lang, ?T("Statistics")), <<"modules/#mod-stats">>, <<"mod_stats">>), Level = case Host of global -> 1; _ -> 3 end, make_xhtml(PageH1 ++ Res, Host, Lang, AJID, Level); process_admin(Host, #request{path = [<<"user">>, U], q = Query, lang = Lang}, AJID) -> case ejabberd_auth:user_exists(U, Host) of true -> Res = user_info(U, Host, Query, Lang), make_xhtml(Res, Host, Lang, AJID, 4); false -> make_xhtml([?XCT(<<"h1">>, ?T("Not Found"))], Host, Lang, AJID, 4) end; process_admin(Host, #request{path = [<<"nodes">>], lang = Lang}, AJID) -> Res = get_nodes(Lang), Level = case Host of global -> 1; _ -> 3 end, make_xhtml(Res, Host, Lang, AJID, Level); process_admin(Host, #request{path = [<<"node">>, SNode | NPath], q = Query, lang = Lang}, AJID) -> case search_running_node(SNode) of false -> make_xhtml([?XCT(<<"h1">>, ?T("Node not found"))], Host, Lang, AJID, 2); Node -> Res = get_node(Host, Node, NPath, Query, Lang), Level = case Host of global -> 2 + length(NPath); _ -> 4 + length(NPath) end, make_xhtml(Res, Host, Node, Lang, AJID, Level) end; %%%================================== %%%% process_admin default case process_admin(Host, #request{lang = Lang} = Request, AJID) -> Res = case Host of global -> ejabberd_hooks:run_fold( webadmin_page_main, Host, [], [Request]); _ -> ejabberd_hooks:run_fold( webadmin_page_host, Host, [], [Host, Request]) end, Level = case Host of global -> length(Request#request.path); _ -> 2 + length(Request#request.path) end, case Res of [] -> setelement(1, make_xhtml([?XC(<<"h1">>, <<"Not Found">>)], Host, Lang, AJID, Level), 404); _ -> make_xhtml(Res, Host, Lang, AJID, Level) end. term_to_id(T) -> base64:encode((term_to_binary(T))). %%%================================== %%%% list_vhosts list_vhosts(Lang, JID) -> Hosts = ejabberd_option:hosts(), HostsAllowed = lists:filter(fun (Host) -> any_rules_allowed(Host, [configure, webadmin_view], JID) end, Hosts), list_vhosts2(Lang, HostsAllowed). list_vhosts2(Lang, Hosts) -> SHosts = lists:sort(Hosts), [?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Host")), ?XACT(<<"td">>, [{<<"class">>, <<"alignright">>}], ?T("Registered Users")), ?XACT(<<"td">>, [{<<"class">>, <<"alignright">>}], ?T("Online Users"))])]), ?XE(<<"tbody">>, (lists:map(fun (Host) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(Host)), RegisteredUsers = ejabberd_auth:count_users(Host), ?XE(<<"tr">>, [?XE(<<"td">>, [?AC(<<"../server/", Host/binary, "/">>, Host)]), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?AC(<<"../server/", Host/binary, "/users/">>, pretty_string_int(RegisteredUsers))]), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?AC(<<"../server/", Host/binary, "/online-users/">>, pretty_string_int(OnlineUsers))])]) end, SHosts)))])]. %%%================================== %%%% list_users list_users(Host, Query, Lang, URLFunc) -> Res = list_users_parse_query(Query, Host), Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), FUsers = case length(SUsers) of N when N =< 100 -> [list_given_users(Host, SUsers, <<"../">>, Lang, URLFunc)]; N -> NParts = trunc(math:sqrt(N * 6.17999999999999993783e-1)) + 1, M = trunc(N / NParts) + 1, lists:flatmap(fun (K) -> L = K + M - 1, Last = if L < N -> su_to_list(lists:nth(L, SUsers)); true -> su_to_list(lists:last(SUsers)) end, Name = <<(su_to_list(lists:nth(K, SUsers)))/binary, $\s, 226, 128, 148, $\s, Last/binary>>, [?AC((URLFunc({user_diapason, K, L})), Name), ?BR] end, lists:seq(1, N, M)) end, case Res of %% Parse user creation query and try register: ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], ([?XE(<<"table">>, [?XE(<<"tr">>, [?XC(<<"td">>, <<(translate:translate(Lang, ?T("User")))/binary, ":">>), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"newusername">>, <<"">>)]), ?XE(<<"td">>, [?C(<<" @ ", Host/binary>>)])]), ?XE(<<"tr">>, [?XC(<<"td">>, <<(translate:translate(Lang, ?T("Password")))/binary, ":">>), ?XE(<<"td">>, [?INPUT(<<"password">>, <<"newuserpassword">>, <<"">>)]), ?X(<<"td">>)]), ?XE(<<"tr">>, [?X(<<"td">>), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?INPUTT(<<"submit">>, <<"addnewuser">>, ?T("Add User"))]), ?X(<<"td">>)])]), ?P] ++ FUsers))]. list_users_parse_query(Query, Host) -> case lists:keysearch(<<"addnewuser">>, 1, Query) of {value, _} -> {value, {_, Username}} = lists:keysearch(<<"newusername">>, 1, Query), {value, {_, Password}} = lists:keysearch(<<"newuserpassword">>, 1, Query), try jid:decode(<>) of #jid{user = User, server = Server} -> case ejabberd_auth:try_register(User, Server, Password) of {error, _Reason} -> error; _ -> ok end catch _:{bad_jid, _} -> error end; false -> nothing end. list_users_in_diapason(Host, Diap, Lang, URLFunc) -> Users = ejabberd_auth:get_users(Host), SUsers = lists:sort([{S, U} || {U, S} <- Users]), [S1, S2] = ejabberd_regexp:split(Diap, <<"-">>), N1 = binary_to_integer(S1), N2 = binary_to_integer(S2), Sub = lists:sublist(SUsers, N1, N2 - N1 + 1), [list_given_users(Host, Sub, <<"../../">>, Lang, URLFunc)]. list_given_users(Host, Users, Prefix, Lang, URLFunc) -> ModOffline = get_offlinemsg_module(Host), ?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("User")), ?XACT(<<"td">>, [{<<"class">>, <<"alignright">>}], ?T("Offline Messages")), ?XCT(<<"td">>, ?T("Last Activity"))])]), ?XE(<<"tbody">>, (lists:map(fun (_SU = {Server, User}) -> US = {User, Server}, QueueLenStr = get_offlinemsg_length(ModOffline, User, Server), FQueueLen = [?AC((URLFunc({users_queue, Prefix, User, Server})), QueueLenStr)], FLast = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> translate:translate(Lang, ?T("Never")); {ok, Shift, _Status} -> TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> translate:translate(Lang, ?T("Online")) end, ?XE(<<"tr">>, [?XE(<<"td">>, [?AC((URLFunc({user, Prefix, misc:url_encode(User), Server})), (us_to_list(US)))]), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], FQueueLen), ?XC(<<"td">>, FLast)]) end, Users)))]). get_offlinemsg_length(ModOffline, User, Server) -> case ModOffline of none -> <<"disabled">>; _ -> pretty_string_int(ModOffline:count_offline_messages(User,Server)) end. get_offlinemsg_module(Server) -> case gen_mod:is_loaded(Server, mod_offline) of true -> mod_offline; false -> none end. get_lastactivity_menuitem_list(Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> case mod_last_opt:db_type(Server) of mnesia -> [{<<"last-activity">>, ?T("Last Activity")}]; _ -> [] end; false -> [] end. get_last_info(User, Server) -> case gen_mod:is_loaded(Server, mod_last) of true -> mod_last:get_last_info(User, Server); false -> not_found end. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). su_to_list({Server, User}) -> jid:encode({User, Server, <<"">>}). %%%================================== %%%% get_stats get_stats(global, Lang) -> OnlineUsers = ejabberd_sm:connected_users_number(), RegisteredUsers = lists:foldl(fun (Host, Total) -> ejabberd_auth:count_users(Host) + Total end, 0, ejabberd_option:hosts()), OutS2SNumber = ejabberd_s2s:outgoing_s2s_number(), InS2SNumber = ejabberd_s2s:incoming_s2s_number(), [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Registered Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(RegisteredUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(OnlineUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Outgoing s2s Connections:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(OutS2SNumber)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Incoming s2s Connections:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(InS2SNumber)))])])])]; get_stats(Host, Lang) -> OnlineUsers = length(ejabberd_sm:get_vh_session_list(Host)), RegisteredUsers = ejabberd_auth:count_users(Host), [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Registered Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(RegisteredUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(OnlineUsers)))])])])]. list_online_users(Host, _Lang) -> Users = [{S, U} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Host)], SUsers = lists:usort(Users), lists:flatmap(fun ({_S, U} = SU) -> [?AC(<<"../user/", (misc:url_encode(U))/binary, "/">>, (su_to_list(SU))), ?BR] end, SUsers). user_info(User, Server, Query, Lang) -> LServer = jid:nameprep(Server), US = {jid:nodeprep(User), LServer}, Res = user_parse_query(User, Server, Query), Resources = ejabberd_sm:get_user_resources(User, Server), FResources = case Resources of [] -> [?CT(?T("None"))]; _ -> [?XE(<<"ul">>, (lists:map( fun (R) -> FIP = case ejabberd_sm:get_user_info(User, Server, R) of offline -> <<"">>; Info when is_list(Info) -> Node = proplists:get_value(node, Info), Conn = proplists:get_value(conn, Info), {IP, Port} = proplists:get_value(ip, Info), ConnS = case Conn of c2s -> <<"plain">>; c2s_tls -> <<"tls">>; c2s_compressed -> <<"zlib">>; c2s_compressed_tls -> <<"tls+zlib">>; http_bind -> <<"http-bind">>; websocket -> <<"websocket">>; _ -> <<"unknown">> end, <> end, case direction(Lang) of [{_, <<"rtl">>}] -> ?LI([?C((<>))]); _ -> ?LI([?C((<>))]) end end, lists:sort(Resources))))] end, FPassword = [?INPUT(<<"text">>, <<"password">>, <<"">>), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"chpassword">>, ?T("Change Password"))], UserItems = ejabberd_hooks:run_fold(webadmin_user, LServer, [], [User, Server, Lang]), LastActivity = case ejabberd_sm:get_user_resources(User, Server) of [] -> case get_last_info(User, Server) of not_found -> translate:translate(Lang, ?T("Never")); {ok, Shift, _Status} -> TimeStamp = {Shift div 1000000, Shift rem 1000000, 0}, {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(TimeStamp), (str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second])) end; _ -> translate:translate(Lang, ?T("Online")) end, [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("User ~ts"), [us_to_list(US)])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], ([?XCT(<<"h3">>, ?T("Connected Resources:"))] ++ FResources ++ [?XCT(<<"h3">>, ?T("Password:"))] ++ FPassword ++ [?XCT(<<"h3">>, ?T("Last Activity"))] ++ [?C(LastActivity)] ++ UserItems ++ [?P, ?INPUTTD(<<"submit">>, <<"removeuser">>, ?T("Remove User"))]))]. user_parse_query(User, Server, Query) -> lists:foldl(fun ({Action, _Value}, Acc) when Acc == nothing -> user_parse_query1(Action, User, Server, Query); ({_Action, _Value}, Acc) -> Acc end, nothing, Query). user_parse_query1(<<"password">>, _User, _Server, _Query) -> nothing; user_parse_query1(<<"chpassword">>, User, Server, Query) -> case lists:keysearch(<<"password">>, 1, Query) of {value, {_, Password}} -> ejabberd_auth:set_password(User, Server, Password), ok; _ -> error end; user_parse_query1(<<"removeuser">>, User, Server, _Query) -> ejabberd_auth:remove_user(User, Server), ok; user_parse_query1(Action, User, Server, Query) -> case ejabberd_hooks:run_fold(webadmin_user_parse_query, Server, [], [Action, User, Server, Query]) of [] -> nothing; Res -> Res end. list_last_activity(Host, Lang, Integral, Period) -> TimeStamp = erlang:system_time(second), case Period of <<"all">> -> TS = 0, Days = infinity; <<"year">> -> TS = TimeStamp - 366 * 86400, Days = 366; _ -> TS = TimeStamp - 31 * 86400, Days = 31 end, case catch mnesia:dirty_select(last_activity, [{{last_activity, {'_', Host}, '$1', '_'}, [{'>', '$1', TS}], [{trunc, {'/', {'-', TimeStamp, '$1'}, 86400}}]}]) of {'EXIT', _Reason} -> []; Vals -> Hist = histogram(Vals, Integral), if Hist == [] -> [?CT(?T("No Data"))]; true -> Left = if Days == infinity -> 0; true -> Days - length(Hist) end, Tail = if Integral -> lists:duplicate(Left, lists:last(Hist)); true -> lists:duplicate(Left, 0) end, Max = lists:max(Hist), [?XAE(<<"ol">>, [{<<"id">>, <<"lastactivity">>}, {<<"start">>, <<"0">>}], [?XAE(<<"li">>, [{<<"style">>, <<"width:", (integer_to_binary(trunc(90 * V / Max)))/binary, "%;">>}], [{xmlcdata, pretty_string_int(V)}]) || V <- Hist ++ Tail])] end end. histogram(Values, Integral) -> histogram(lists:sort(Values), Integral, 0, 0, []). histogram([H | T], Integral, Current, Count, Hist) when Current == H -> histogram(T, Integral, Current, Count + 1, Hist); histogram([H | _] = Values, Integral, Current, Count, Hist) when Current < H -> if Integral -> histogram(Values, Integral, Current + 1, Count, [Count | Hist]); true -> histogram(Values, Integral, Current + 1, 0, [Count | Hist]) end; histogram([], _Integral, _Current, Count, Hist) -> if Count > 0 -> lists:reverse([Count | Hist]); true -> lists:reverse(Hist) end. %%%================================== %%%% get_nodes get_nodes(Lang) -> RunningNodes = ejabberd_cluster:get_nodes(), StoppedNodes = ejabberd_cluster:get_known_nodes() -- RunningNodes, FRN = if RunningNodes == [] -> ?CT(?T("None")); true -> ?XE(<<"ul">>, (lists:map(fun (N) -> S = iolist_to_binary(atom_to_list(N)), ?LI([?AC(<<"../node/", S/binary, "/">>, S)]) end, lists:sort(RunningNodes)))) end, FSN = if StoppedNodes == [] -> ?CT(?T("None")); true -> ?XE(<<"ul">>, (lists:map(fun (N) -> S = iolist_to_binary(atom_to_list(N)), ?LI([?C(S)]) end, lists:sort(StoppedNodes)))) end, [?XCT(<<"h1">>, ?T("Nodes")), ?XCT(<<"h3">>, ?T("Running Nodes")), FRN, ?XCT(<<"h3">>, ?T("Stopped Nodes")), FSN]. search_running_node(SNode) -> RunningNodes = ejabberd_cluster:get_nodes(), search_running_node(SNode, RunningNodes). search_running_node(_, []) -> false; search_running_node(SNode, [Node | Nodes]) -> case iolist_to_binary(atom_to_list(Node)) of SNode -> Node; _ -> search_running_node(SNode, Nodes) end. get_node(global, Node, [], Query, Lang) -> Res = node_parse_query(Node, Query), Base = get_base_path(global, Node, 2), MenuItems2 = make_menu_items(global, Node, Base, Lang), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XE(<<"ul">>, ([?LI([?ACT(<<"db/">>, ?T("Database"))]), ?LI([?ACT(<<"backup/">>, ?T("Backup"))]), ?LI([?ACT(<<"stats/">>, ?T("Statistics"))]), ?LI([?ACT(<<"update/">>, ?T("Update"))])] ++ MenuItems2)), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?INPUTT(<<"submit">>, <<"restart">>, ?T("Restart")), ?C(<<" ">>), ?INPUTTD(<<"submit">>, <<"stop">>, ?T("Stop"))])]; get_node(Host, Node, [], _Query, Lang) -> Base = get_base_path(Host, Node, 4), MenuItems2 = make_menu_items(Host, Node, Base, Lang), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Node ~p"), [Node]))), ?XE(<<"ul">>, MenuItems2)]; get_node(global, Node, [<<"db">>], Query, Lang) -> case ejabberd_cluster:call(Node, mnesia, system_info, [tables]) of {badrpc, _Reason} -> [?XCT(<<"h1">>, ?T("RPC Call Error"))]; Tables -> ResS = case node_db_parse_query(Node, Tables, Query) of nothing -> []; ok -> [?XREST(?T("Submitted"))] end, STables = lists:sort(Tables), Rows = lists:map(fun (Table) -> STable = iolist_to_binary(atom_to_list(Table)), TInfo = case ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]) of {badrpc, _} -> []; I -> I end, {Type, Size, Memory} = case {lists:keysearch(storage_type, 1, TInfo), lists:keysearch(size, 1, TInfo), lists:keysearch(memory, 1, TInfo)} of {{value, {storage_type, T}}, {value, {size, S}}, {value, {memory, M}}} -> {T, S, M}; _ -> {unknown, 0, 0} end, MemoryB = Memory*erlang:system_info(wordsize), ?XE(<<"tr">>, [?XE(<<"td">>, [?AC(<<"./", STable/binary, "/">>, STable)]), ?XE(<<"td">>, [db_storage_select(STable, Type, Lang)]), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?AC(<<"./", STable/binary, "/1/">>, (pretty_string_int(Size)))]), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(MemoryB)))]) end, STables), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), [Node])) )] ++ ResS ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XAE(<<"table">>, [], [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Name")), ?XCT(<<"td">>, ?T("Storage Type")), ?XACT(<<"td">>, [{<<"class">>, <<"alignright">>}], ?T("Elements")), ?XACT(<<"td">>, [{<<"class">>, <<"alignright">>}], ?T("Memory"))])]), ?XE(<<"tbody">>, (Rows ++ [?XE(<<"tr">>, [?XAE(<<"td">>, [{<<"colspan">>, <<"4">>}, {<<"class">>, <<"alignright">>}], [?INPUTT(<<"submit">>, <<"submit">>, ?T("Submit"))])])]))])])] end; get_node(global, Node, [<<"db">>, TableName], _Query, Lang) -> make_table_view(Node, TableName, Lang); get_node(global, Node, [<<"db">>, TableName, PageNumber], _Query, Lang) -> make_table_elements_view(Node, TableName, Lang, binary_to_integer(PageNumber)); get_node(global, Node, [<<"backup">>], Query, Lang) -> HomeDirRaw = case {os:getenv("HOME"), os:type()} of {EnvHome, _} when is_list(EnvHome) -> list_to_binary(EnvHome); {false, {win32, _Osname}} -> <<"C:/">>; {false, _} -> <<"/tmp/">> end, HomeDir = filename:nativename(HomeDirRaw), ResS = case node_backup_parse_query(Node, Query) of nothing -> []; ok -> [?XREST(?T("Submitted"))]; {error, Error} -> [?XRES(<<(translate:translate(Lang, ?T("Error")))/binary, ": ", ((str:format("~p", [Error])))/binary>>)] end, [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Backup of ~p"), [Node])))] ++ ResS ++ [?XCT(<<"p">>, ?T("Please note that these options will " "only backup the builtin Mnesia database. " "If you are using the ODBC module, you " "also need to backup your SQL database " "separately.")), ?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Store binary backup:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"storepath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"store">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore binary backup immediately:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"restorepath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"restore">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore binary backup after next ejabberd " "restart (requires less memory):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"fallbackpath">>, (filename:join(HomeDir, "ejabberd.backup")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"fallback">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Store plain text backup:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"dumppath">>, (filename:join(HomeDir, "ejabberd.dump")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"dump">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Restore plain text backup immediately:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"loadpath">>, (filename:join(HomeDir, "ejabberd.dump")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"load">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import users data from a PIEFXIS file " "(XEP-0227):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_piefxis_filepath">>, (filename:join(HomeDir, "users.xml")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_piefxis_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Export data of all users in the server " "to PIEFXIS files (XEP-0227):")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_piefxis_dirpath">>, HomeDir)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_piefxis_dir">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XE(<<"td">>, [?CT(?T("Export data of users in a host to PIEFXIS " "files (XEP-0227):")), ?C(<<" ">>), make_select_host(Lang, <<"export_piefxis_host_dirhost">>)]), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_piefxis_host_dirpath">>, HomeDir)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_piefxis_host_dir">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XE(<<"td">>, [?CT(?T("Export all tables as SQL queries " "to a file:")), ?C(<<" ">>), make_select_host(Lang, <<"export_sql_filehost">>)]), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"export_sql_filepath">>, (filename:join(HomeDir, "db.sql")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"export_sql_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import user data from jabberd14 spool " "file:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_filepath">>, (filename:join(HomeDir, "user1.xml")))]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_file">>, ?T("OK"))])]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Import users data from jabberd14 spool " "directory:")), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"import_dirpath">>, <<"/var/spool/jabber/">>)]), ?XE(<<"td">>, [?INPUTT(<<"submit">>, <<"import_dir">>, ?T("OK"))])])])])])]; get_node(global, Node, [<<"stats">>], _Query, Lang) -> UpTime = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]), UpTimeS = (str:format("~.3f", [element(1, UpTime) / 1000])), UpTimeDate = uptime_date(Node), CPUTime = ejabberd_cluster:call(Node, erlang, statistics, [runtime]), CPUTimeS = (str:format("~.3f", [element(1, CPUTime) / 1000])), OnlineUsers = ejabberd_sm:connected_users_number(), TransactionsCommitted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_commits]), TransactionsAborted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_failures]), TransactionsRestarted = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_restarts]), TransactionsLogged = ejabberd_cluster:call(Node, mnesia, system_info, [transaction_log_writes]), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Statistics of ~p"), [Node]))), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Uptime:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], UpTimeS)]), ?XE(<<"tr">>, [?X(<<"td">>), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], UpTimeDate)]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("CPU Time:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], CPUTimeS)]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Online Users:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(OnlineUsers)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Committed:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsCommitted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Aborted:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsAborted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Restarted:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsRestarted)))]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Transactions Logged:")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(TransactionsLogged)))])])])]; get_node(global, Node, [<<"update">>], Query, Lang) -> ejabberd_cluster:call(Node, code, purge, [ejabberd_update]), Res = node_update_parse_query(Node, Query), ejabberd_cluster:call(Node, code, load_file, [ejabberd_update]), {ok, _Dir, UpdatedBeams, Script, LowLevelScript, Check} = ejabberd_cluster:call(Node, ejabberd_update, update_info, []), Mods = case UpdatedBeams of [] -> ?CT(?T("None")); _ -> BeamsLis = lists:map(fun (Beam) -> BeamString = iolist_to_binary(atom_to_list(Beam)), ?LI([?INPUT(<<"checkbox">>, <<"selected">>, BeamString), ?C(BeamString)]) end, UpdatedBeams), SelectButtons = [?BR, ?INPUTATTRS(<<"button">>, <<"selectall">>, ?T("Select All"), [{<<"onClick">>, <<"selectAll()">>}]), ?C(<<" ">>), ?INPUTATTRS(<<"button">>, <<"unselectall">>, ?T("Unselect All"), [{<<"onClick">>, <<"unSelectAll()">>}])], ?XAE(<<"ul">>, [{<<"class">>, <<"nolistyle">>}], (BeamsLis ++ SelectButtons)) end, FmtScript = (?XC(<<"pre">>, (str:format("~p", [Script])))), FmtLowLevelScript = (?XC(<<"pre">>, (str:format("~p", [LowLevelScript])))), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Update ~p"), [Node])))] ++ case Res of ok -> [?XREST(?T("Submitted"))]; {error, ErrorText} -> [?XREST(<<"Error: ", ErrorText/binary>>)]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], [?XCT(<<"h2">>, ?T("Update plan")), ?XCT(<<"h3">>, ?T("Modified modules")), Mods, ?XCT(<<"h3">>, ?T("Update script")), FmtScript, ?XCT(<<"h3">>, ?T("Low level update script")), FmtLowLevelScript, ?XCT(<<"h3">>, ?T("Script check")), ?XC(<<"pre">>, (misc:atom_to_binary(Check))), ?BR, ?INPUTT(<<"submit">>, <<"update">>, ?T("Update"))])]; get_node(Host, Node, NPath, Query, Lang) -> Res = case Host of global -> ejabberd_hooks:run_fold(webadmin_page_node, Host, [], [Node, NPath, Query, Lang]); _ -> ejabberd_hooks:run_fold(webadmin_page_hostnode, Host, [], [Host, Node, NPath, Query, Lang]) end, case Res of [] -> [?XC(<<"h1">>, <<"Not Found">>)]; _ -> Res end. uptime_date(Node) -> Localtime = ejabberd_cluster:call(Node, erlang, localtime, []), Now = calendar:datetime_to_gregorian_seconds(Localtime), {Wall, _} = ejabberd_cluster:call(Node, erlang, statistics, [wall_clock]), LastRestart = Now - (Wall div 1000), {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:gregorian_seconds_to_datetime(LastRestart), str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w", [Year, Month, Day, Hour, Minute, Second]). %%%================================== %%%% node parse node_parse_query(Node, Query) -> case lists:keysearch(<<"restart">>, 1, Query) of {value, _} -> case ejabberd_cluster:call(Node, init, restart, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> case lists:keysearch(<<"stop">>, 1, Query) of {value, _} -> case ejabberd_cluster:call(Node, init, stop, []) of {badrpc, _Reason} -> error; _ -> ok end; _ -> nothing end end. make_select_host(Lang, Name) -> ?XAE(<<"select">>, [{<<"name">>, Name}], (lists:map(fun (Host) -> ?XACT(<<"option">>, ([{<<"value">>, Host}]), Host) end, ejabberd_config:get_option(hosts)))). db_storage_select(ID, Opt, Lang) -> ?XAE(<<"select">>, [{<<"name">>, <<"table", ID/binary>>}], (lists:map(fun ({O, Desc}) -> Sel = if O == Opt -> [{<<"selected">>, <<"selected">>}]; true -> [] end, ?XACT(<<"option">>, (Sel ++ [{<<"value">>, iolist_to_binary(atom_to_list(O))}]), Desc) end, [{ram_copies, ?T("RAM copy")}, {disc_copies, ?T("RAM and disc copy")}, {disc_only_copies, ?T("Disc only copy")}, {unknown, ?T("Remote copy")}, {delete_content, ?T("Delete content")}, {delete_table, ?T("Delete table")}]))). node_db_parse_query(_Node, _Tables, [{nokey, <<>>}]) -> nothing; node_db_parse_query(Node, Tables, Query) -> lists:foreach(fun (Table) -> STable = iolist_to_binary(atom_to_list(Table)), case lists:keysearch(<<"table", STable/binary>>, 1, Query) of {value, {_, SType}} -> Type = case SType of <<"unknown">> -> unknown; <<"ram_copies">> -> ram_copies; <<"disc_copies">> -> disc_copies; <<"disc_only_copies">> -> disc_only_copies; <<"delete_content">> -> delete_content; <<"delete_table">> -> delete_table; _ -> false end, if Type == false -> ok; Type == delete_content -> mnesia:clear_table(Table); Type == delete_table -> mnesia:delete_table(Table); Type == unknown -> mnesia:del_table_copy(Table, Node); true -> case mnesia:add_table_copy(Table, Node, Type) of {aborted, _} -> mnesia:change_table_copy_type(Table, Node, Type); _ -> ok end end; _ -> ok end end, Tables), ok. node_backup_parse_query(_Node, [{nokey, <<>>}]) -> nothing; node_backup_parse_query(Node, Query) -> lists:foldl(fun (Action, nothing) -> case lists:keysearch(Action, 1, Query) of {value, _} -> case lists:keysearch(<>, 1, Query) of {value, {_, Path}} -> Res = case Action of <<"store">> -> ejabberd_cluster:call(Node, mnesia, backup, [binary_to_list(Path)]); <<"restore">> -> ejabberd_cluster:call(Node, ejabberd_admin, restore, [Path]); <<"fallback">> -> ejabberd_cluster:call(Node, mnesia, install_fallback, [binary_to_list(Path)]); <<"dump">> -> ejabberd_cluster:call(Node, ejabberd_admin, dump_to_textfile, [Path]); <<"load">> -> ejabberd_cluster:call(Node, mnesia, load_textfile, [binary_to_list(Path)]); <<"import_piefxis_file">> -> ejabberd_cluster:call(Node, ejabberd_piefxis, import_file, [Path]); <<"export_piefxis_dir">> -> ejabberd_cluster:call(Node, ejabberd_piefxis, export_server, [Path]); <<"export_piefxis_host_dir">> -> {value, {_, Host}} = lists:keysearch(<>, 1, Query), ejabberd_cluster:call(Node, ejabberd_piefxis, export_host, [Path, Host]); <<"export_sql_file">> -> {value, {_, Host}} = lists:keysearch(<>, 1, Query), ejabberd_cluster:call(Node, ejd2sql, export, [Host, Path]); <<"import_file">> -> ejabberd_cluster:call(Node, ejabberd_admin, import_file, [Path]); <<"import_dir">> -> ejabberd_cluster:call(Node, ejabberd_admin, import_dir, [Path]) end, case Res of {error, Reason} -> {error, Reason}; {badrpc, Reason} -> {badrpc, Reason}; _ -> ok end; OtherError -> {error, OtherError} end; _ -> nothing end; (_Action, Res) -> Res end, nothing, [<<"store">>, <<"restore">>, <<"fallback">>, <<"dump">>, <<"load">>, <<"import_file">>, <<"import_dir">>, <<"import_piefxis_file">>, <<"export_piefxis_dir">>, <<"export_piefxis_host_dir">>, <<"export_sql_file">>]). node_update_parse_query(Node, Query) -> case lists:keysearch(<<"update">>, 1, Query) of {value, _} -> ModulesToUpdateStrings = proplists:get_all_values(<<"selected">>, Query), ModulesToUpdate = [misc:binary_to_atom(M) || M <- ModulesToUpdateStrings], case ejabberd_cluster:call(Node, ejabberd_update, update, [ModulesToUpdate]) of {ok, _} -> ok; {error, Error} -> ?ERROR_MSG("~p~n", [Error]), {error, (str:format("~p", [Error]))}; {badrpc, Error} -> ?ERROR_MSG("Bad RPC: ~p~n", [Error]), {error, <<"Bad RPC: ", ((str:format("~p", [Error])))/binary>>} end; _ -> nothing end. pretty_print_xml(El) -> list_to_binary(pretty_print_xml(El, <<"">>)). pretty_print_xml({xmlcdata, CData}, Prefix) -> IsBlankCData = lists:all( fun($\f) -> true; ($\r) -> true; ($\n) -> true; ($\t) -> true; ($\v) -> true; ($ ) -> true; (_) -> false end, binary_to_list(CData)), if IsBlankCData -> []; true -> [Prefix, CData, $\n] end; pretty_print_xml(#xmlel{name = Name, attrs = Attrs, children = Els}, Prefix) -> [Prefix, $<, Name, case Attrs of [] -> []; [{Attr, Val} | RestAttrs] -> AttrPrefix = [Prefix, str:copies(<<" ">>, byte_size(Name) + 2)], [$\s, Attr, $=, $', fxml:crypt(Val) | [$', lists:map(fun ({Attr1, Val1}) -> [$\n, AttrPrefix, Attr1, $=, $', fxml:crypt(Val1), $'] end, RestAttrs)]] end, if Els == [] -> <<"/>\n">>; true -> OnlyCData = lists:all(fun ({xmlcdata, _}) -> true; (#xmlel{}) -> false end, Els), if OnlyCData -> [$>, fxml:get_cdata(Els), $<, $/, Name, $>, $\n]; true -> [$>, $\n, lists:map(fun (E) -> pretty_print_xml(E, [Prefix, <<" ">>]) end, Els), Prefix, $<, $/, Name, $>, $\n] end end]. url_func({user_diapason, From, To}) -> <<(integer_to_binary(From))/binary, "-", (integer_to_binary(To))/binary, "/">>; url_func({users_queue, Prefix, User, _Server}) -> <>; url_func({user, Prefix, User, _Server}) -> <>. last_modified() -> {<<"Last-Modified">>, <<"Mon, 25 Feb 2008 13:23:30 GMT">>}. cache_control_public() -> {<<"Cache-Control">>, <<"public">>}. %% Transform 1234567890 into "1,234,567,890" pretty_string_int(Integer) when is_integer(Integer) -> pretty_string_int(integer_to_binary(Integer)); pretty_string_int(String) when is_binary(String) -> {_, Result} = lists:foldl(fun (NewNumber, {3, Result}) -> {1, <>}; (NewNumber, {CountAcc, Result}) -> {CountAcc + 1, <>} end, {0, <<"">>}, lists:reverse(binary_to_list(String))), Result. %%%================================== %%%% mnesia table view make_table_view(Node, STable, Lang) -> Table = misc:binary_to_atom(STable), TInfo = ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]), {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), {value, {size, Size}} = lists:keysearch(size, 1, TInfo), {value, {memory, Memory}} = lists:keysearch(memory, 1, TInfo), MemoryB = Memory*erlang:system_info(wordsize), TableInfo = str:format("~p", [TInfo]), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), [Node]))), ?XAE(<<"table">>, [], [?XE(<<"tbody">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Name")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], STable )]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Node")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], misc:atom_to_binary(Node) )]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Storage Type")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], misc:atom_to_binary(Type) )]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Elements")), ?XAE(<<"td">>, [{<<"class">>, <<"alignright">>}], [?AC(<<"1/">>, (pretty_string_int(Size)))]) ]), ?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Memory")), ?XAC(<<"td">>, [{<<"class">>, <<"alignright">>}], (pretty_string_int(MemoryB)) )]) ])]), ?XC(<<"pre">>, TableInfo)]. make_table_elements_view(Node, STable, Lang, PageNumber) -> Table = misc:binary_to_atom(STable), TInfo = ejabberd_cluster:call(Node, mnesia, table_info, [Table, all]), {value, {storage_type, Type}} = lists:keysearch(storage_type, 1, TInfo), {value, {size, Size}} = lists:keysearch(size, 1, TInfo), PageSize = 500, TableContentErl = get_table_content(Node, Table, Type, PageNumber, PageSize), TableContent = str:format("~p", [TableContentErl]), PagesLinks = build_elements_pages_list(Size, PageNumber, PageSize), [?XC(<<"h1">>, (str:translate_and_format(Lang, ?T("Database Tables at ~p"), [Node]))), ?P, ?AC(<<"../">>, STable), ?P ] ++ PagesLinks ++ [?XC(<<"pre">>, TableContent)]. build_elements_pages_list(Size, PageNumber, PageSize) -> PagesNumber = calculate_pages_number(Size, PageSize), PagesSeq = lists:seq(1, PagesNumber), PagesList = [?AC(<<"../", (integer_to_binary(N))/binary, "/">>, <<(integer_to_binary(N))/binary, " ">>) || N <- PagesSeq], lists:keyreplace( [?C(<<(integer_to_binary(PageNumber))/binary, " ">>)], 4, PagesList, ?C(<<" [", (integer_to_binary(PageNumber))/binary, "] ">>)). calculate_pages_number(Size, PageSize) -> Remainer = case Size rem PageSize of 0 -> 0; _ -> 1 end, case (Size div PageSize) + Remainer of 1 -> 0; Res -> Res end. get_table_content(Node, Table, _Type, PageNumber, PageSize) -> Keys1 = lists:sort(ejabberd_cluster:call(Node, mnesia, dirty_all_keys, [Table])), FirstKeyPos = 1 - PageSize + PageNumber*PageSize, Keys = lists:sublist(Keys1, FirstKeyPos, PageSize), Res = [ejabberd_cluster:call(Node, mnesia, dirty_read, [Table, Key]) || Key <- Keys], lists:flatten(Res). %%%================================== %%%% navigation menu %% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> [LI] make_navigation(Host, Node, Lang, JID, Level) -> Menu = make_navigation_menu(Host, Node, Lang, JID, Level), make_menu_items(Lang, Menu). %% @spec (Host, Node, Lang, JID::jid(), Level::integer()) -> Menu %% where Host = global | string() %% Node = cluster | string() %% Lang = string() %% Menu = {URL, Title} | {URL, Title, [Menu]} %% URL = string() %% Title = string() make_navigation_menu(Host, Node, Lang, JID, Level) -> HostNodeMenu = make_host_node_menu(Host, Node, Lang, JID, Level), HostMenu = make_host_menu(Host, HostNodeMenu, Lang, JID, Level), NodeMenu = make_node_menu(Host, Node, Lang, Level), make_server_menu(HostMenu, NodeMenu, Lang, JID, Level). %% @spec (Host, Node, Base, Lang) -> [LI] make_menu_items(global, cluster, Base, Lang) -> HookItems = get_menu_items_hook(server, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(global, Node, Base, Lang) -> HookItems = get_menu_items_hook({node, Node}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(Host, cluster, Base, Lang) -> HookItems = get_menu_items_hook({host, Host}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}); make_menu_items(Host, Node, Base, Lang) -> HookItems = get_menu_items_hook({hostnode, Host, Node}, Lang), make_menu_items(Lang, {Base, <<"">>, HookItems}). make_host_node_menu(global, _, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_node_menu(_, cluster, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_node_menu(Host, Node, Lang, JID, Level) -> HostNodeBase = get_base_path(Host, Node, Level), HostNodeFixed = get_menu_items_hook({hostnode, Host, Node}, Lang), HostNodeFixed2 = [Tuple || Tuple <- HostNodeFixed, is_allowed_path(Host, Tuple, JID)], {HostNodeBase, iolist_to_binary(atom_to_list(Node)), HostNodeFixed2}. make_host_menu(global, _HostNodeMenu, _Lang, _JID, _Level) -> {<<"">>, <<"">>, []}; make_host_menu(Host, HostNodeMenu, Lang, JID, Level) -> HostBase = get_base_path(Host, cluster, Level), HostFixed = [{<<"users">>, ?T("Users")}, {<<"online-users">>, ?T("Online Users")}] ++ get_lastactivity_menuitem_list(Host) ++ [{<<"nodes">>, ?T("Nodes"), HostNodeMenu}, {<<"stats">>, ?T("Statistics")}] ++ get_menu_items_hook({host, Host}, Lang), HostFixed2 = [Tuple || Tuple <- HostFixed, is_allowed_path(Host, Tuple, JID)], {HostBase, Host, HostFixed2}. make_node_menu(_Host, cluster, _Lang, _Level) -> {<<"">>, <<"">>, []}; make_node_menu(global, Node, Lang, Level) -> NodeBase = get_base_path(global, Node, Level), NodeFixed = [{<<"db">>, ?T("Database")}, {<<"backup">>, ?T("Backup")}, {<<"stats">>, ?T("Statistics")}, {<<"update">>, ?T("Update")}] ++ get_menu_items_hook({node, Node}, Lang), {NodeBase, iolist_to_binary(atom_to_list(Node)), NodeFixed}; make_node_menu(_Host, _Node, _Lang, _Level) -> {<<"">>, <<"">>, []}. make_server_menu(HostMenu, NodeMenu, Lang, JID, Level) -> Base = get_base_path(global, cluster, Level), Fixed = [{<<"vhosts">>, ?T("Virtual Hosts"), HostMenu}, {<<"nodes">>, ?T("Nodes"), NodeMenu}, {<<"stats">>, ?T("Statistics")}] ++ get_menu_items_hook(server, Lang), Fixed2 = [Tuple || Tuple <- Fixed, is_allowed_path(global, Tuple, JID)], {Base, <<"">>, Fixed2}. get_menu_items_hook({hostnode, Host, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_hostnode, Host, [], [Host, Node, Lang]); get_menu_items_hook({host, Host}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_host, Host, [], [Host, Lang]); get_menu_items_hook({node, Node}, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_node, [], [Node, Lang]); get_menu_items_hook(server, Lang) -> ejabberd_hooks:run_fold(webadmin_menu_main, [], [Lang]). %% @spec (Lang::string(), Menu) -> [LI] %% where Menu = {MURI::string(), MName::string(), Items::[Item]} %% Item = {IURI::string(), IName::string()} | {IURI::string(), IName::string(), Menu} make_menu_items(Lang, Menu) -> lists:reverse(make_menu_items2(Lang, 1, Menu)). make_menu_items2(Lang, Deep, {MURI, MName, _} = Menu) -> Res = case MName of <<"">> -> []; _ -> [make_menu_item(header, Deep, MURI, MName, Lang)] end, make_menu_items2(Lang, Deep, Menu, Res). make_menu_items2(_, _Deep, {_, _, []}, Res) -> Res; make_menu_items2(Lang, Deep, {MURI, MName, [Item | Items]}, Res) -> Res2 = case Item of {IURI, IName} -> [make_menu_item(item, Deep, <>, IName, Lang) | Res]; {IURI, IName, SubMenu} -> ResTemp = [make_menu_item(item, Deep, <>, IName, Lang) | Res], ResSubMenu = make_menu_items2(Lang, Deep + 1, SubMenu), ResSubMenu ++ ResTemp end, make_menu_items2(Lang, Deep, {MURI, MName, Items}, Res2). make_menu_item(header, 1, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navhead">>}], [?AC(URI, Name)])]); make_menu_item(header, 2, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navheadsub">>}], [?AC(URI, Name)])]); make_menu_item(header, 3, URI, Name, _Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navheadsubsub">>}], [?AC(URI, Name)])]); make_menu_item(item, 1, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitem">>}], [?ACT(URI, Name)])]); make_menu_item(item, 2, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitemsub">>}], [?ACT(URI, Name)])]); make_menu_item(item, 3, URI, Name, Lang) -> ?LI([?XAE(<<"div">>, [{<<"id">>, <<"navitemsubsub">>}], [?ACT(URI, Name)])]). any_rules_allowed(Host, Access, Entity) -> lists:any( fun(Rule) -> allow == acl:match_rule(Host, Rule, Entity) end, Access). %%% vim: set foldmethod=marker foldmarker=%%%%,%%%=: ejabberd-21.12/src/mod_proxy65_stream.erl0000644000232200023220000002205514154362354020677 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_proxy65_stream.erl %%% Author : Evgeniy Khramtsov %%% Purpose : Bytestream process. %%% Created : 12 Oct 2006 by Evgeniy Khramtsov %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_proxy65_stream). -author('xram@jabber.ru'). -behaviour(p1_fsm). -behaviour(ejabberd_listener). %% gen_fsm callbacks. -export([init/1, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3]). %% gen_fsm states. -export([accepting/2, wait_for_init/2, wait_for_auth/2, wait_for_request/2, wait_for_activation/2, stream_established/2]). -export([start/3, stop/1, start_link/3, activate/2, relay/3, accept/1, listen_options/0]). -include("mod_proxy65.hrl"). -include("logger.hrl"). -define(WAIT_TIMEOUT, 60000). -record(state, {socket :: inet:socket(), timer = make_ref() :: reference(), sha1 = <<"">> :: binary(), host = <<"">> :: binary(), auth_type = anonymous :: plain | anonymous, shaper = none :: ejabberd_shaper:shaper()}). %% Unused callbacks handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. %%------------------------------- start(gen_tcp, Socket, Opts) -> Host = proplists:get_value(server_host, Opts), p1_fsm:start(?MODULE, [Socket, Host], []). start_link(gen_tcp, Socket, Opts) -> Host = proplists:get_value(server_host, Opts), p1_fsm:start_link(?MODULE, [Socket, Host], []). init([Socket, Host]) -> process_flag(trap_exit, true), AuthType = mod_proxy65_opt:auth_type(Host), Shaper = mod_proxy65_opt:shaper(Host), RecvBuf = mod_proxy65_opt:recbuf(Host), SendBuf = mod_proxy65_opt:sndbuf(Host), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), inet:setopts(Socket, [{recbuf, RecvBuf}, {sndbuf, SendBuf}]), {ok, accepting, #state{host = Host, auth_type = AuthType, socket = Socket, shaper = Shaper, timer = TRef}}. terminate(_Reason, StateName, #state{sha1 = SHA1}) -> Mod = gen_mod:ram_db_mod(global, mod_proxy65), Mod:unregister_stream(SHA1), if StateName == stream_established -> ?INFO_MSG("(~w) Bytestream terminated", [self()]); true -> ok end. %%%------------------------------ %%% API. %%%------------------------------ accept(StreamPid) -> p1_fsm:send_event(StreamPid, accept). stop(StreamPid) -> StreamPid ! stop. activate({P1, J1}, {P2, J2}) -> case catch {p1_fsm:sync_send_all_state_event(P1, get_socket), p1_fsm:sync_send_all_state_event(P2, get_socket)} of {S1, S2} when is_port(S1), is_port(S2) -> P1 ! {activate, P2, S2, J1, J2}, P2 ! {activate, P1, S1, J1, J2}, JID1 = jid:encode(J1), JID2 = jid:encode(J2), ?INFO_MSG("(~w:~w) Activated bytestream for ~ts " "-> ~ts", [P1, P2, JID1, JID2]), ok; _ -> error end. %%%----------------------- %%% States %%%----------------------- accepting(accept, State) -> inet:setopts(State#state.socket, [{active, true}]), {next_state, wait_for_init, State}. wait_for_init(Packet, #state{socket = Socket, auth_type = AuthType} = StateData) -> case mod_proxy65_lib:unpack_init_message(Packet) of {ok, AuthMethods} -> Method = select_auth_method(AuthType, AuthMethods), gen_tcp:send(Socket, mod_proxy65_lib:make_init_reply(Method)), case Method of ?AUTH_ANONYMOUS -> {next_state, wait_for_request, StateData}; ?AUTH_PLAIN -> {next_state, wait_for_auth, StateData}; ?AUTH_NO_METHODS -> {stop, normal, StateData} end; error -> {stop, normal, StateData} end. wait_for_auth(Packet, #state{socket = Socket, host = Host} = StateData) -> case mod_proxy65_lib:unpack_auth_request(Packet) of {User, Pass} -> Result = ejabberd_auth:check_password(User, <<"">>, Host, Pass), gen_tcp:send(Socket, mod_proxy65_lib:make_auth_reply(Result)), case Result of true -> {next_state, wait_for_request, StateData}; false -> {stop, normal, StateData} end; _ -> {stop, normal, StateData} end. wait_for_request(Packet, #state{socket = Socket} = StateData) -> Request = mod_proxy65_lib:unpack_request(Packet), case Request of #s5_request{sha1 = SHA1, cmd = connect} -> Mod = gen_mod:ram_db_mod(global, mod_proxy65), case Mod:register_stream(SHA1, self()) of ok -> inet:setopts(Socket, [{active, false}]), gen_tcp:send(Socket, mod_proxy65_lib:make_reply(Request)), {next_state, wait_for_activation, StateData#state{sha1 = SHA1}}; _ -> Err = mod_proxy65_lib:make_error_reply(Request), gen_tcp:send(Socket, Err), {stop, normal, StateData} end; #s5_request{cmd = udp} -> Err = mod_proxy65_lib:make_error_reply(Request, ?ERR_COMMAND_NOT_SUPPORTED), gen_tcp:send(Socket, Err), {stop, normal, StateData}; _ -> {stop, normal, StateData} end. wait_for_activation(_Data, StateData) -> {next_state, wait_for_activation, StateData}. stream_established(_Data, StateData) -> {next_state, stream_established, StateData}. %%%----------------------- %%% Callbacks processing %%%----------------------- %% SOCKS5 packets. handle_info({tcp, _S, Data}, StateName, StateData) when StateName /= wait_for_activation -> misc:cancel_timer(StateData#state.timer), TRef = erlang:send_after(?WAIT_TIMEOUT, self(), stop), p1_fsm:send_event(self(), Data), {next_state, StateName, StateData#state{timer = TRef}}; %% Activation message. handle_info({activate, PeerPid, PeerSocket, IJid, TJid}, wait_for_activation, StateData) -> erlang:monitor(process, PeerPid), misc:cancel_timer(StateData#state.timer), MySocket = StateData#state.socket, Shaper = StateData#state.shaper, Host = StateData#state.host, MaxRate = find_maxrate(Shaper, IJid, TJid, Host), spawn_link(?MODULE, relay, [MySocket, PeerSocket, MaxRate]), {next_state, stream_established, StateData}; %% Socket closed handle_info({tcp_closed, _Socket}, _StateName, StateData) -> {stop, normal, StateData}; handle_info({tcp_error, _Socket, _Reason}, _StateName, StateData) -> {stop, normal, StateData}; %% Got stop message. handle_info(stop, _StateName, StateData) -> {stop, normal, StateData}; %% Either linked process or peer process died. handle_info({'EXIT', _, _}, _StateName, StateData) -> {stop, normal, StateData}; handle_info({'DOWN', _, _, _, _}, _StateName, StateData) -> {stop, normal, StateData}; %% Packets of no interest handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. %% Socket request. handle_sync_event(get_socket, _From, wait_for_activation, StateData) -> Socket = StateData#state.socket, {reply, Socket, wait_for_activation, StateData}; handle_sync_event(_Event, _From, StateName, StateData) -> {reply, error, StateName, StateData}. %%%------------------------------------------------- %%% Relay Process. %%%------------------------------------------------- relay(MySocket, PeerSocket, Shaper) -> case gen_tcp:recv(MySocket, 0) of {ok, Data} -> case gen_tcp:send(PeerSocket, Data) of ok -> {NewShaper, Pause} = ejabberd_shaper:update(Shaper, byte_size(Data)), if Pause > 0 -> timer:sleep(Pause); true -> pass end, relay(MySocket, PeerSocket, NewShaper); {error, _} = Err -> Err end; {error, _} = Err -> Err end. %%%------------------------ %%% Auxiliary functions %%%------------------------ select_auth_method(plain, AuthMethods) -> case lists:member(?AUTH_PLAIN, AuthMethods) of true -> ?AUTH_PLAIN; false -> ?AUTH_NO_METHODS end; select_auth_method(anonymous, AuthMethods) -> case lists:member(?AUTH_ANONYMOUS, AuthMethods) of true -> ?AUTH_ANONYMOUS; false -> ?AUTH_NO_METHODS end. %% Obviously, we must use shaper with maximum rate. find_maxrate(Shaper, JID1, JID2, Host) -> R1 = ejabberd_shaper:match(Host, Shaper, JID1), R2 = ejabberd_shaper:match(Host, Shaper, JID2), R = case ejabberd_shaper:get_max_rate(R1) >= ejabberd_shaper:get_max_rate(R2) of true -> R1; false -> R2 end, ejabberd_shaper:new(R). listen_options() -> []. ejabberd-21.12/src/mod_muc_sup.erl0000644000232200023220000000547514154362354017452 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% Created : 4 Jul 2019 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_sup). -behaviour(supervisor). %% API -export([start/1, start_link/1, procname/1]). %% Supervisor callbacks -export([init/1]). %%%=================================================================== %%% API functions %%%=================================================================== start(Host) -> Spec = #{id => procname(Host), start => {?MODULE, start_link, [Host]}, restart => permanent, shutdown => infinity, type => supervisor, modules => [?MODULE]}, supervisor:start_child(ejabberd_gen_mod_sup, Spec). start_link(Host) -> Proc = procname(Host), supervisor:start_link({local, Proc}, ?MODULE, [Host]). -spec procname(binary()) -> atom(). procname(Host) -> gen_mod:get_module_proc(Host, ?MODULE). %%%=================================================================== %%% Supervisor callbacks %%%=================================================================== init([Host]) -> Cores = erlang:system_info(logical_processors), Specs = lists:foldl( fun(I, Acc) -> [#{id => mod_muc:procname(Host, I), start => {mod_muc, start_link, [Host, I]}, restart => permanent, shutdown => timer:minutes(1), type => worker, modules => [mod_muc]}|Acc] end, [room_sup_spec(Host)], lists:seq(1, Cores)), {ok, {{one_for_one, 10*Cores, 1}, Specs}}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec room_sup_spec(binary()) -> supervisor:child_spec(). room_sup_spec(Host) -> Name = mod_muc_room:supervisor(Host), #{id => Name, start => {ejabberd_tmp_sup, start_link, [Name, mod_muc_room]}, restart => permanent, shutdown => infinity, type => supervisor, modules => [ejabberd_tmp_sup]}. ejabberd-21.12/src/ejabberd_router_sql.erl0000644000232200023220000001016614154362354021146 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 28 Mar 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_router_sql). -behaviour(ejabberd_router). %% API -export([init/0, register_route/5, unregister_route/3, find_routes/1, get_all_routes/0]). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("ejabberd_router.hrl"). -include("ejabberd_stacktrace.hrl"). %%%=================================================================== %%% API %%%=================================================================== init() -> Node = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL 'route' table...", []), case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from route where node=%(Node)s")) of {updated, _} -> ok; Err -> ?ERROR_MSG("Failed to clean 'route' table: ~p", [Err]), Err end. register_route(Domain, ServerHost, LocalHint, _, Pid) -> PidS = misc:encode_pid(Pid), LocalHintS = enc_local_hint(LocalHint), Node = erlang:atom_to_binary(node(Pid), latin1), case ?SQL_UPSERT(ejabberd_config:get_myname(), "route", ["!domain=%(Domain)s", "!server_host=%(ServerHost)s", "!node=%(Node)s", "!pid=%(PidS)s", "local_hint=%(LocalHintS)s"]) of ok -> ok; _ -> {error, db_failure} end. unregister_route(Domain, _, Pid) -> PidS = misc:encode_pid(Pid), Node = erlang:atom_to_binary(node(Pid), latin1), case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("delete from route where domain=%(Domain)s " "and pid=%(PidS)s and node=%(Node)s")) of {updated, _} -> ok; _ -> {error, db_failure} end. find_routes(Domain) -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("select @(server_host)s, @(node)s, @(pid)s, @(local_hint)s " "from route where domain=%(Domain)s")) of {selected, Rows} -> {ok, lists:flatmap( fun(Row) -> row_to_route(Domain, Row) end, Rows)}; _ -> {error, db_failure} end. get_all_routes() -> case ejabberd_sql:sql_query( ejabberd_config:get_myname(), ?SQL("select @(domain)s from route where domain <> server_host")) of {selected, Domains} -> {ok, [Domain || {Domain} <- Domains]}; _ -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== enc_local_hint(undefined) -> <<"">>; enc_local_hint(LocalHint) -> misc:term_to_expr(LocalHint). dec_local_hint(<<"">>) -> undefined; dec_local_hint(S) -> ejabberd_sql:decode_term(S). row_to_route(Domain, {ServerHost, NodeS, PidS, LocalHintS} = Row) -> try [#route{domain = Domain, server_host = ServerHost, pid = misc:decode_pid(PidS, NodeS), local_hint = dec_local_hint(LocalHintS)}] catch _:{bad_node, _} -> []; ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to decode row from 'route' table:~n" "** Row = ~p~n" "** Domain = ~ts~n" "** ~ts", [Row, Domain, misc:format_exception(2, Class, Reason, StackTrace)]), [] end. ejabberd-21.12/src/mod_mix_pam_mnesia.erl0000644000232200023220000000636714154362354020766 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov %%% Created : 4 Dec 2018 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix_pam_mnesia). -behaviour(mod_mix_pam). %% API -export([init/2, add_channel/3, get_channel/2, get_channels/1, del_channel/2, del_channels/1, use_cache/1]). -record(mix_pam, {user_channel :: {binary(), binary(), binary(), binary()}, user :: {binary(), binary()}, id :: binary()}). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> case ejabberd_mnesia:create(?MODULE, mix_pam, [{disc_only_copies, [node()]}, {attributes, record_info(fields, mix_pam)}, {index, [user]}]) of {atomic, _} -> ok; _ -> {error, db_failure} end. use_cache(Host) -> case mnesia:table_info(mix_pam, storage_type) of disc_only_copies -> mod_mix_pam_opt:use_cache(Host); _ -> false end. add_channel(User, Channel, ID) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), mnesia:dirty_write(#mix_pam{user_channel = {LUser, LServer, Chan, Service}, user = {LUser, LServer}, id = ID}). get_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), case mnesia:dirty_read(mix_pam, {LUser, LServer, Chan, Service}) of [#mix_pam{id = ID}] -> {ok, ID}; [] -> {error, notfound} end. get_channels(User) -> {LUser, LServer, _} = jid:tolower(User), Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), {ok, lists:map( fun(#mix_pam{user_channel = {_, _, Chan, Service}, id = ID}) -> {jid:make(Chan, Service), ID} end, Ret)}. del_channel(User, Channel) -> {LUser, LServer, _} = jid:tolower(User), {Chan, Service, _} = jid:tolower(Channel), mnesia:dirty_delete(mix_pam, {LUser, LServer, Chan, Service}). del_channels(User) -> {LUser, LServer, _} = jid:tolower(User), Ret = mnesia:dirty_index_read(mix_pam, {LUser, LServer}, #mix_pam.user), lists:foreach(fun mnesia:dirty_delete_object/1, Ret). %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_mqtt_ws.erl0000644000232200023220000001440214154362354017463 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -module(mod_mqtt_ws). -ifndef(GEN_SERVER). -define(GEN_SERVER, gen_server). -endif. -behaviour(?GEN_SERVER). %% API -export([socket_handoff/3]). -export([start/1, start_link/1]). -export([peername/1, setopts/2, send/2, close/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("logger.hrl"). -define(SEND_TIMEOUT, timer:seconds(15)). -record(state, {socket :: socket(), ws_pid :: pid(), mqtt_session :: undefined | pid()}). -type peername() :: {inet:ip_address(), inet:port_number()}. -type socket() :: {http_ws, pid(), peername()}. -export_type([socket/0]). %%%=================================================================== %%% API %%%=================================================================== socket_handoff(LocalPath, Request, Opts) -> ejabberd_websocket:socket_handoff( LocalPath, Request, Opts, ?MODULE, fun get_human_html_xmlel/0). start({#ws{http_opts = Opts}, _} = WS) -> ?GEN_SERVER:start(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). start_link({#ws{http_opts = Opts}, _} = WS) -> ?GEN_SERVER:start_link(?MODULE, [WS], ejabberd_config:fsm_limit_opts(Opts)). -spec peername(socket()) -> {ok, peername()}. peername({http_ws, _, IP}) -> {ok, IP}. -spec setopts(socket(), list()) -> ok. setopts(_WSock, _Opts) -> ok. -spec send(socket(), iodata()) -> ok | {error, timeout | einval}. send({http_ws, Pid, _}, Data) -> try ?GEN_SERVER:call(Pid, {send, Data}, ?SEND_TIMEOUT) catch exit:{timeout, {?GEN_SERVER, _, _}} -> {error, timeout}; exit:{_, {?GEN_SERVER, _, _}} -> {error, einval} end. -spec close(socket()) -> ok. close({http_ws, Pid, _}) -> ?GEN_SERVER:cast(Pid, close). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([{#ws{ip = IP, http_opts = ListenOpts}, WsPid}]) -> Socket = {http_ws, self(), IP}, case mod_mqtt_session:start(?MODULE, Socket, ListenOpts) of {ok, Pid} -> erlang:monitor(process, Pid), erlang:monitor(process, WsPid), mod_mqtt_session:accept(Pid), State = #state{socket = Socket, ws_pid = WsPid, mqtt_session = Pid}, {ok, State}; {error, Reason} -> {stop, Reason}; ignore -> ignore end. handle_call({send, Data}, _From, #state{ws_pid = WsPid} = State) -> WsPid ! {data, Data}, {reply, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(close, State) -> {stop, normal, State#state{mqtt_session = undefined}}; handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. handle_info(closed, State) -> {stop, normal, State}; handle_info({received, Data}, State) -> State#state.mqtt_session ! {tcp, State#state.socket, Data}, {noreply, State}; handle_info({'DOWN', _, process, Pid, _}, State) when Pid == State#state.mqtt_session orelse Pid == State#state.ws_pid -> {stop, normal, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> if State#state.mqtt_session /= undefined -> State#state.mqtt_session ! {tcp_closed, State#state.socket}; true -> ok end. code_change(_OldVsn, State, _Extra) -> {ok, State}. format_status(_Opt, Status) -> Status. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_human_html_xmlel() -> xmlel(). get_human_html_xmlel() -> Heading = <<"ejabberd mod_mqtt">>, #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], children = [#xmlel{name = <<"head">>, attrs = [], children = [#xmlel{name = <<"title">>, attrs = [], children = [{xmlcdata, Heading}]}]}, #xmlel{name = <<"body">>, attrs = [], children = [#xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, Heading}]}, #xmlel{name = <<"p">>, attrs = [], children = [{xmlcdata, <<"An implementation of ">>}, #xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"http://tools.ietf.org/html/rfc6455">>}], children = [{xmlcdata, <<"WebSocket protocol">>}]}]}, #xmlel{name = <<"p">>, attrs = [], children = [{xmlcdata, <<"This web page is only informative. To " "use WebSocket connection you need an MQTT " "client that supports it.">>}]}]}]}. ejabberd-21.12/src/node_pep.erl0000644000232200023220000001776314154362354016734 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : node_pep.erl %%% Author : Christophe Romain %%% Purpose : Standard PubSub PEP plugin %%% Created : 1 Dec 2007 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc The module {@module} is the pep PubSub plugin. %%%

PubSub plugin nodes are using the {@link gen_pubsub_node} behaviour.

-module(node_pep). -behaviour(gen_pubsub_node). -author('christophe.romain@process-one.net'). -include("pubsub.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/2, remove_extra_items/3, remove_expired_items/2, 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_last_items/3, get_only_item/2, get_item/2, set_item/1, get_item_name/3, node_to_path/1, path_to_node/1, depends/3]). depends(_Host, _ServerHost, _Opts) -> [{mod_caps, hard}]. init(Host, ServerHost, Opts) -> node_flat:init(Host, ServerHost, Opts), ok. terminate(Host, ServerHost) -> node_flat:terminate(Host, ServerHost), ok. options() -> [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, false}, {purge_offline, false}, {persist_items, true}, {max_items, 1}, {subscribe, true}, {access_model, presence}, {roster_groups_allowed, []}, {publish_model, publishers}, {notification_type, headline}, {max_payload_size, ?MAX_PAYLOAD_SIZE}, {send_last_published_item, on_sub_and_presence}, {deliver_notifications, true}, {presence_based_delivery, true}, {itemreply, none}]. features() -> [<<"create-nodes">>, <<"auto-create">>, <<"auto-subscribe">>, <<"delete-nodes">>, <<"delete-items">>, <<"filtered-notifications">>, <<"modify-affiliations">>, <<"outcast-affiliation">>, <<"persistent-items">>, <<"publish">>, <<"publish-options">>, <<"purge-nodes">>, <<"retract-items">>, <<"retrieve-affiliations">>, <<"retrieve-items">>, <<"retrieve-subscriptions">>, <<"subscribe">>]. create_node_permission(Host, ServerHost, _Node, _ParentNode, Owner, Access) -> LOwner = jid:tolower(Owner), {User, Server, _Resource} = LOwner, Allowed = case LOwner of {<<"">>, Host, <<"">>} -> true; % pubsub service always allowed _ -> case acl:match_rule(ServerHost, Access, LOwner) of allow -> case Host of {User, Server, _} -> true; _ -> false end; _ -> false end end, {result, Allowed}. create_node(Nidx, Owner) -> node_flat:create_node(Nidx, Owner). delete_node(Nodes) -> node_flat:delete_node(Nodes). subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> case node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId) of {error, Error} -> {error, Error}; {result, _} -> {result, default} end. publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts) -> node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload, PubOpts). remove_extra_items(Nidx, MaxItems) -> node_flat:remove_extra_items(Nidx, MaxItems). remove_extra_items(Nidx, MaxItems, ItemIds) -> node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). remove_expired_items(Nidx, Seconds) -> node_flat:remove_expired_items(Nidx, Seconds). delete_item(Nidx, Publisher, PublishModel, ItemId) -> node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). purge_node(Nidx, Owner) -> node_flat:purge_node(Nidx, Owner). get_entity_affiliations(Host, Owner) -> {_, D, _} = SubKey = jid:tolower(Owner), SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun (#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> case NodeTree:get_node(N) of #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, A} | Acc]; _ -> Acc end end, [], States), {result, Reply}. get_node_affiliations(Nidx) -> node_flat:get_node_affiliations(Nidx). get_affiliation(Nidx, Owner) -> node_flat:get_affiliation(Nidx, Owner). set_affiliation(Nidx, Owner, Affiliation) -> node_flat:set_affiliation(Nidx, Owner, Affiliation). get_entity_subscriptions(Host, Owner) -> {U, D, _} = SubKey = jid:tolower(Owner), GenKey = jid:remove_resource(SubKey), States = case SubKey of GenKey -> mnesia:match_object(#pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); _ -> mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}) ++ mnesia:match_object(#pubsub_state{stateid = {SubKey, '_'}, _ = '_'}) end, NodeTree = mod_pubsub:tree(Host), Reply = lists:foldl(fun (#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> case NodeTree:get_node(N) of #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> lists:foldl(fun ({subscribed, SubId}, Acc2) -> [{Node, subscribed, SubId, J} | Acc2]; ({pending, _SubId}, Acc2) -> [{Node, pending, J} | Acc2]; (S, Acc2) -> [{Node, S, J} | Acc2] end, Acc, Ss); _ -> Acc end end, [], States), {result, Reply}. get_node_subscriptions(Nidx) -> node_flat:get_node_subscriptions(Nidx). get_subscriptions(Nidx, Owner) -> node_flat:get_subscriptions(Nidx, Owner). set_subscriptions(Nidx, Owner, Subscription, SubId) -> node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). get_pending_nodes(Host, Owner) -> node_flat:get_pending_nodes(Host, Owner). get_states(Nidx) -> node_flat:get_states(Nidx). get_state(Nidx, JID) -> node_flat:get_state(Nidx, JID). set_state(State) -> node_flat:set_state(State). get_items(Nidx, From, RSM) -> node_flat:get_items(Nidx, From, RSM). get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> node_flat:get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM). get_last_items(Nidx, From, Count) -> node_flat:get_last_items(Nidx, From, Count). get_only_item(Nidx, From) -> node_flat:get_only_item(Nidx, From). get_item(Nidx, ItemId) -> node_flat:get_item(Nidx, ItemId). get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> node_flat:get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId). set_item(Item) -> node_flat:set_item(Item). get_item_name(Host, Node, Id) -> node_flat:get_item_name(Host, Node, Id). node_to_path(Node) -> node_flat:node_to_path(Node). path_to_node(Path) -> node_flat:path_to_node(Path). ejabberd-21.12/src/mod_pres_counter_opt.erl0000644000232200023220000000110214154362354021350 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_pres_counter_opt). -export([count/1]). -export([interval/1]). -spec count(gen_mod:opts() | global | binary()) -> pos_integer(). count(Opts) when is_map(Opts) -> gen_mod:get_opt(count, Opts); count(Host) -> gen_mod:get_module_opt(Host, mod_pres_counter, count). -spec interval(gen_mod:opts() | global | binary()) -> pos_integer(). interval(Opts) when is_map(Opts) -> gen_mod:get_opt(interval, Opts); interval(Host) -> gen_mod:get_module_opt(Host, mod_pres_counter, interval). ejabberd-21.12/src/ejabberd_auth_sql.erl0000644000232200023220000002471114154362354020570 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_sql.erl %%% Author : Alexey Shchepin %%% Purpose : Authentication via ODBC %%% Created : 12 Dec 2004 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_sql). -author('alexey@process-one.net'). -behaviour(ejabberd_auth). -export([start/1, stop/1, set_password/3, try_register/3, get_users/2, count_users/2, get_password/2, remove_user/2, store_type/1, plain_password_required/1, export/1, which_users_exists/2]). -include_lib("xmpp/include/scram.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("ejabberd_auth.hrl"). -define(SALT_LENGTH, 16). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(_Host) -> ok. stop(_Host) -> ok. plain_password_required(Server) -> store_type(Server) == scram. store_type(Server) -> ejabberd_auth:password_format(Server). set_password(User, Server, Password) -> F = fun() -> case Password of #scram{hash = Hash, storedkey = SK, serverkey = SEK, salt = Salt, iterationcount = IC} -> SK2 = scram_hash_encode(Hash, SK), set_password_scram_t( User, Server, SK2, SEK, Salt, IC); _ -> set_password_t(User, Server, Password) end end, case ejabberd_sql:sql_transaction(Server, F) of {atomic, _} -> {cache, {ok, Password}}; {aborted, _} -> {nocache, {error, db_failure}} end. try_register(User, Server, Password) -> Res = case Password of #scram{hash = Hash, storedkey = SK, serverkey = SEK, salt = Salt, iterationcount = IC} -> SK2 = scram_hash_encode(Hash, SK), add_user_scram( Server, User, SK2, SEK, Salt, IC); _ -> add_user(Server, User, Password) end, case Res of {updated, 1} -> {cache, {ok, Password}}; _ -> {nocache, {error, exists}} end. get_users(Server, Opts) -> case list_users(Server, Opts) of {selected, Res} -> [{U, Server} || {U} <- Res]; _ -> [] end. count_users(Server, Opts) -> case users_number(Server, Opts) of {selected, [{Res}]} -> Res; _Other -> 0 end. get_password(User, Server) -> case get_password_scram(Server, User) of {selected, [{Password, <<>>, <<>>, 0}]} -> {cache, {ok, Password}}; {selected, [{StoredKey, ServerKey, Salt, IterationCount}]} -> {Hash, SK} = case StoredKey of <<"sha256:", Rest/binary>> -> {sha256, Rest}; <<"sha512:", Rest/binary>> -> {sha512, Rest}; Other -> {sha, Other} end, {cache, {ok, #scram{storedkey = SK, serverkey = ServerKey, salt = Salt, hash = Hash, iterationcount = IterationCount}}}; {selected, []} -> {cache, error}; _ -> {nocache, error} end. remove_user(User, Server) -> case del_user(Server, User) of {updated, _} -> ok; _ -> {error, db_failure} end. -define(BATCH_SIZE, 1000). scram_hash_encode(Hash, StoreKey) -> case Hash of sha -> StoreKey; sha256 -> <<"sha256:", StoreKey/binary>>; sha512 -> <<"sha512:", StoreKey/binary>> end. set_password_scram_t(LUser, LServer, StoredKey, ServerKey, Salt, IterationCount) -> ?SQL_UPSERT_T( "users", ["!username=%(LUser)s", "!server_host=%(LServer)s", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", "iterationcount=%(IterationCount)d"]). set_password_t(LUser, LServer, Password) -> ?SQL_UPSERT_T( "users", ["!username=%(LUser)s", "!server_host=%(LServer)s", "password=%(Password)s"]). get_password_scram(LServer, LUser) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(password)s, @(serverkey)s, @(salt)s, @(iterationcount)d" " from users" " where username=%(LUser)s and %(LServer)H")). add_user_scram(LServer, LUser, StoredKey, ServerKey, Salt, IterationCount) -> ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "users", ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", "iterationcount=%(IterationCount)d"])). add_user(LServer, LUser, Password) -> ejabberd_sql:sql_query( LServer, ?SQL_INSERT( "users", ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(Password)s"])). del_user(LServer, LUser) -> ejabberd_sql:sql_query( LServer, ?SQL("delete from users where username=%(LUser)s and %(LServer)H")). list_users(LServer, []) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users where %(LServer)H")); list_users(LServer, [{from, Start}, {to, End}]) when is_integer(Start) and is_integer(End) -> list_users(LServer, [{limit, End - Start + 1}, {offset, Start - 1}]); list_users(LServer, [{prefix, Prefix}, {from, Start}, {to, End}]) when is_binary(Prefix) and is_integer(Start) and is_integer(End) -> list_users(LServer, [{prefix, Prefix}, {limit, End - Start + 1}, {offset, Start - 1}]); list_users(LServer, [{limit, Limit}, {offset, Offset}]) when is_integer(Limit) and is_integer(Offset) -> ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users " "where %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")); list_users(LServer, [{prefix, Prefix}, {limit, Limit}, {offset, Offset}]) when is_binary(Prefix) and is_integer(Limit) and is_integer(Offset) -> SPrefix = ejabberd_sql:escape_like_arg(Prefix), SPrefix2 = <>, ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H " "order by username " "limit %(Limit)d offset %(Offset)d")). users_number(LServer) -> ejabberd_sql:sql_query( LServer, fun(pgsql, _) -> case ejabberd_option:pgsql_users_number_estimate(LServer) of true -> ejabberd_sql:sql_query_t( ?SQL("select @(reltuples :: bigint)d from pg_class" " where oid = 'users'::regclass::oid")); _ -> ejabberd_sql:sql_query_t( ?SQL("select @(count(*))d from users where %(LServer)H")) end; (_Type, _) -> ejabberd_sql:sql_query_t( ?SQL("select @(count(*))d from users where %(LServer)H")) end). users_number(LServer, [{prefix, Prefix}]) when is_binary(Prefix) -> SPrefix = ejabberd_sql:escape_like_arg(Prefix), SPrefix2 = <>, ejabberd_sql:sql_query( LServer, ?SQL("select @(count(*))d from users " "where username like %(SPrefix2)s %ESCAPE and %(LServer)H")); users_number(LServer, []) -> users_number(LServer). which_users_exists(LServer, LUsers) when length(LUsers) =< 100 -> try ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s from users where username in %(LUsers)ls")) of {selected, Matching} -> [U || {U} <- Matching]; {error, _} = E -> E catch _:B -> {error, B} end; which_users_exists(LServer, LUsers) -> {First, Rest} = lists:split(100, LUsers), case which_users_exists(LServer, First) of {error, _} = E -> E; V -> case which_users_exists(LServer, Rest) of {error, _} = E2 -> E2; V2 -> V ++ V2 end end. export(_Server) -> [{passwd, fun(Host, #passwd{us = {LUser, LServer}, password = Password}) when LServer == Host, is_binary(Password) -> [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "users", ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(Password)s"])]; (Host, {passwd, {LUser, LServer}, {scram, StoredKey1, ServerKey, Salt, IterationCount}}) when LServer == Host -> Hash = sha, StoredKey = scram_hash_encode(Hash, StoredKey1), [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "users", ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", "iterationcount=%(IterationCount)d"])]; (Host, #passwd{us = {LUser, LServer}, password = #scram{} = Scram}) when LServer == Host -> StoredKey = scram_hash_encode(Scram#scram.hash, Scram#scram.storedkey), ServerKey = Scram#scram.serverkey, Salt = Scram#scram.salt, IterationCount = Scram#scram.iterationcount, [?SQL("delete from users where username=%(LUser)s and %(LServer)H;"), ?SQL_INSERT( "users", ["username=%(LUser)s", "server_host=%(LServer)s", "password=%(StoredKey)s", "serverkey=%(ServerKey)s", "salt=%(Salt)s", "iterationcount=%(IterationCount)d"])]; (_Host, _R) -> [] end}]. ejabberd-21.12/src/ejabberd_config.erl0000644000232200023220000005466614154362354020231 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_config.erl %%% Author : Alexey Shchepin %%% Purpose : Load config file %%% Created : 14 Dec 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_config). %% API -export([get_option/1]). -export([load/0, reload/0, format_error/1, path/0]). -export([env_binary_to_list/2]). -export([get_myname/0, get_uri/0, get_copyright/0]). -export([get_shared_key/0, get_node_start/0]). -export([fsm_limit_opts/1]). -export([codec_options/0]). -export([version/0]). -export([default_db/2, default_db/3, default_ram_db/2, default_ram_db/3]). -export([beams/1, validators/1, globals/0, may_hide_data/1]). -export([dump/0, dump/1, convert_to_yaml/1, convert_to_yaml/2]). -export([callback_modules/1]). %% Deprecated functions -export([get_option/2, set_option/2]). -export([get_version/0, get_myhosts/0]). -export([get_mylang/0, get_lang/1]). -deprecated([{get_option, 2}, {set_option, 2}, {get_version, 0}, {get_myhosts, 0}, {get_mylang, 0}, {get_lang, 1}]). -include("logger.hrl"). -include("ejabberd_stacktrace.hrl"). -type option() :: atom() | {atom(), global | binary()}. -type error_reason() :: {merge_conflict, atom(), binary()} | {old_config, file:filename_all(), term()} | {write_file, file:filename_all(), term()} | {exception, term(), term(), term()}. -type error_return() :: {error, econf:error_reason(), term()} | {error, error_reason()}. -type host_config() :: #{{atom(), binary() | global} => term()}. -callback opt_type(atom()) -> econf:validator(). -callback options() -> [atom() | {atom(), term()}]. -callback globals() -> [atom()]. -callback doc() -> any(). -optional_callbacks([globals/0]). %%%=================================================================== %%% API %%%=================================================================== -spec load() -> ok | error_return(). load() -> load(path()). -spec load(file:filename_all()) -> ok | error_return(). load(Path) -> ConfigFile = unicode:characters_to_binary(Path), UnixTime = erlang:monotonic_time(second), ?INFO_MSG("Loading configuration from ~ts", [ConfigFile]), _ = ets:new(ejabberd_options, [named_table, public, {read_concurrency, true}]), case load_file(ConfigFile) of ok -> set_shared_key(), set_node_start(UnixTime), ?INFO_MSG("Configuration loaded successfully", []); Err -> Err end. -spec reload() -> ok | error_return(). reload() -> ejabberd_systemd:reloading(), ConfigFile = path(), ?INFO_MSG("Reloading configuration from ~ts", [ConfigFile]), OldHosts = get_myhosts(), Res = case load_file(ConfigFile) of ok -> NewHosts = get_myhosts(), AddHosts = NewHosts -- OldHosts, DelHosts = OldHosts -- NewHosts, lists:foreach( fun(Host) -> ejabberd_hooks:run(host_up, [Host]) end, AddHosts), lists:foreach( fun(Host) -> ejabberd_hooks:run(host_down, [Host]) end, DelHosts), ejabberd_hooks:run(config_reloaded, []), delete_host_options(DelHosts), ?INFO_MSG("Configuration reloaded successfully", []); Err -> ?ERROR_MSG("Configuration reload aborted: ~ts", [format_error(Err)]), Err end, ejabberd_systemd:ready(), Res. -spec dump() -> ok | error_return(). dump() -> dump(stdout). -spec dump(stdout | file:filename_all()) -> ok | error_return(). dump(Output) -> Y = get_option(yaml_config), dump(Y, Output). -spec dump(term(), stdout | file:filename_all()) -> ok | error_return(). dump(Y, Output) -> Data = fast_yaml:encode(Y), case Output of stdout -> io:format("~ts~n", [Data]); FileName -> try ok = filelib:ensure_dir(FileName), ok = file:write_file(FileName, Data) catch _:{badmatch, {error, Reason}} -> {error, {write_file, FileName, Reason}} end end. -spec get_option(option(), term()) -> term(). get_option(Opt, Default) -> try get_option(Opt) catch _:badarg -> Default end. -spec get_option(option()) -> term(). get_option(Opt) when is_atom(Opt) -> get_option({Opt, global}); get_option({O, Host} = Opt) -> Tab = case get_tmp_config() of undefined -> ejabberd_options; T -> T end, try ets:lookup_element(Tab, Opt, 2) catch ?EX_RULE(error, badarg, St) when Host /= global -> StackTrace = ?EX_STACK(St), Val = get_option({O, global}), ?DEBUG("Option '~ts' is not defined for virtual host '~ts'. " "This is a bug, please report it with the following " "stacktrace included:~n** ~ts", [O, Host, misc:format_exception(2, error, badarg, StackTrace)]), Val end. -spec set_option(option(), term()) -> ok. set_option(Opt, Val) when is_atom(Opt) -> set_option({Opt, global}, Val); set_option(Opt, Val) -> Tab = case get_tmp_config() of undefined -> ejabberd_options; T -> T end, ets:insert(Tab, {Opt, Val}), ok. -spec get_version() -> binary(). get_version() -> get_option(version). -spec get_myhosts() -> [binary(), ...]. get_myhosts() -> get_option(hosts). -spec get_myname() -> binary(). get_myname() -> get_option(host). -spec get_mylang() -> binary(). get_mylang() -> get_lang(global). -spec get_lang(global | binary()) -> binary(). get_lang(Host) -> get_option({language, Host}). -spec get_uri() -> binary(). get_uri() -> <<"http://www.process-one.net/en/ejabberd/">>. -spec get_copyright() -> binary(). get_copyright() -> <<"Copyright (c) ProcessOne">>. -spec get_shared_key() -> binary(). get_shared_key() -> get_option(shared_key). -spec get_node_start() -> integer(). get_node_start() -> get_option(node_start). -spec fsm_limit_opts([proplists:property()]) -> [{max_queue, pos_integer()}]. fsm_limit_opts(Opts) -> case lists:keyfind(max_fsm_queue, 1, Opts) of {_, I} when is_integer(I), I>0 -> [{max_queue, I}]; false -> case get_option(max_fsm_queue) of undefined -> []; N -> [{max_queue, N}] end end. -spec codec_options() -> [xmpp:decode_option()]. codec_options() -> case get_option(validate_stream) of true -> []; false -> [ignore_els] end. %% Do not use this function in runtime: %% It's slow and doesn't read 'version' option from the config. %% Use ejabberd_option:version() instead. -spec version() -> binary(). version() -> case application:get_env(ejabberd, custom_vsn) of {ok, Vsn0} when is_list(Vsn0) -> list_to_binary(Vsn0); {ok, Vsn1} when is_binary(Vsn1) -> Vsn1; _ -> case application:get_key(ejabberd, vsn) of undefined -> <<"">>; {ok, Vsn} -> list_to_binary(Vsn) end end. -spec default_db(binary() | global, module()) -> atom(). default_db(Host, Module) -> default_db(default_db, Host, Module, mnesia). -spec default_db(binary() | global, module(), atom()) -> atom(). default_db(Host, Module, Default) -> default_db(default_db, Host, Module, Default). -spec default_ram_db(binary() | global, module()) -> atom(). default_ram_db(Host, Module) -> default_db(default_ram_db, Host, Module, mnesia). -spec default_ram_db(binary() | global, module(), atom()) -> atom(). default_ram_db(Host, Module, Default) -> default_db(default_ram_db, Host, Module, Default). -spec default_db(default_db | default_ram_db, binary() | global, module(), atom()) -> atom(). default_db(Opt, Host, Mod, Default) -> Type = get_option({Opt, Host}), DBMod = list_to_atom(atom_to_list(Mod) ++ "_" ++ atom_to_list(Type)), case code:ensure_loaded(DBMod) of {module, _} -> Type; {error, _} -> ?WARNING_MSG("Module ~ts doesn't support database '~ts' " "defined in option '~ts', using " "'~ts' as fallback", [Mod, Type, Opt, Default]), Default end. -spec beams(local | external | all) -> [module()]. beams(local) -> {ok, Mods} = application:get_key(ejabberd, modules), Mods; beams(external) -> ExtMods = [Name || {Name, _Details} <- ext_mod:installed()], lists:foreach( fun(ExtMod) -> ExtModPath = ext_mod:module_ebin_dir(ExtMod), case lists:member(ExtModPath, code:get_path()) of true -> ok; false -> code:add_patha(ExtModPath) end end, ExtMods), case application:get_env(ejabberd, external_beams) of {ok, Path} -> case lists:member(Path, code:get_path()) of true -> ok; false -> code:add_patha(Path) end, Beams = filelib:wildcard(filename:join(Path, "*\.beam")), CustMods = [list_to_atom(filename:rootname(filename:basename(Beam))) || Beam <- Beams], CustMods ++ ExtMods; _ -> ExtMods end; beams(all) -> beams(local) ++ beams(external). -spec may_hide_data(term()) -> term(). may_hide_data(Data) -> case get_option(hide_sensitive_log_data) of false -> Data; true -> "hidden_by_ejabberd" end. %% Some Erlang apps expects env parameters to be list and not binary. %% For example, Mnesia is not able to start if mnesia dir is passed as a binary. %% However, binary is most common on Elixir, so it is easy to make a setup mistake. -spec env_binary_to_list(atom(), atom()) -> {ok, any()} | undefined. env_binary_to_list(Application, Parameter) -> %% Application need to be loaded to allow setting parameters application:load(Application), case application:get_env(Application, Parameter) of {ok, Val} when is_binary(Val) -> BVal = binary_to_list(Val), application:set_env(Application, Parameter, BVal), {ok, BVal}; Other -> Other end. -spec validators([atom()]) -> {econf:validators(), [atom()]}. validators(Disallowed) -> Modules = callback_modules(all), Validators = lists:foldl( fun(M, Vs) -> maps:merge(Vs, validators(M, Disallowed)) end, #{}, Modules), Required = lists:flatmap( fun(M) -> [O || O <- M:options(), is_atom(O)] end, Modules), {Validators, Required}. -spec convert_to_yaml(file:filename()) -> ok | error_return(). convert_to_yaml(File) -> convert_to_yaml(File, stdout). -spec convert_to_yaml(file:filename(), stdout | file:filename()) -> ok | error_return(). convert_to_yaml(File, Output) -> case read_erlang_file(File, []) of {ok, Y} -> dump(Y, Output); Err -> Err end. -spec format_error(error_return()) -> string(). format_error({error, Reason, Ctx}) -> econf:format_error(Reason, Ctx); format_error({error, {merge_conflict, Opt, Host}}) -> lists:flatten( io_lib:format( "Cannot merge value of option '~ts' defined in append_host_config " "for virtual host ~ts: only options of type list or map are allowed " "in append_host_config. Hint: specify the option in host_config", [Opt, Host])); format_error({error, {old_config, Path, Reason}}) -> lists:flatten( io_lib:format( "Failed to read configuration from '~ts': ~ts~ts", [Path, case Reason of {_, _, _} -> "at line "; _ -> "" end, file:format_error(Reason)])); format_error({error, {write_file, Path, Reason}}) -> lists:flatten( io_lib:format( "Failed to write to '~ts': ~ts", [Path, file:format_error(Reason)])); format_error({error, {exception, Class, Reason, St}}) -> lists:flatten( io_lib:format( "Exception occurred during configuration processing. " "This is most likely due to faulty/incompatible validator in " "third-party code. If you are not running any third-party " "code, please report the bug with ejabberd configuration " "file attached and the following stacktrace included:~n** ~ts", [misc:format_exception(2, Class, Reason, St)])). %%%=================================================================== %%% Internal functions %%%=================================================================== -spec path() -> binary(). path() -> unicode:characters_to_binary( case get_env_config() of {ok, Path} -> Path; undefined -> case os:getenv("EJABBERD_CONFIG_PATH") of false -> "ejabberd.yml"; Path -> Path end end). -spec get_env_config() -> {ok, string()} | undefined. get_env_config() -> %% First case: the filename can be specified with: erl -config "/path/to/ejabberd.yml". case application:get_env(ejabberd, config) of R = {ok, _Path} -> R; undefined -> %% Second case for embbeding ejabberd in another app, for example for Elixir: %% config :ejabberd, %% file: "config/ejabberd.yml" application:get_env(ejabberd, file) end. -spec create_tmp_config() -> ok. create_tmp_config() -> T = ets:new(options, [private]), put(ejabberd_options, T), ok. -spec get_tmp_config() -> ets:tid() | undefined. get_tmp_config() -> get(ejabberd_options). -spec delete_tmp_config() -> ok. delete_tmp_config() -> case get_tmp_config() of undefined -> ok; T -> erase(ejabberd_options), ets:delete(T), ok end. -spec callback_modules(local | external | all) -> [module()]. callback_modules(local) -> [ejabberd_options]; callback_modules(external) -> lists:filter( fun(M) -> case code:ensure_loaded(M) of {module, _} -> erlang:function_exported(M, options, 0) andalso erlang:function_exported(M, opt_type, 1); {error, _} -> false end end, beams(external)); callback_modules(all) -> callback_modules(local) ++ callback_modules(external). -spec validators(module(), [atom()]) -> econf:validators(). validators(Mod, Disallowed) -> maps:from_list( lists:filtermap( fun(O) -> case lists:member(O, Disallowed) of true -> false; false -> {true, try {O, Mod:opt_type(O)} catch _:_ -> {O, ejabberd_options:opt_type(O)} end} end end, proplists:get_keys(Mod:options()))). read_file(File) -> read_file(File, [replace_macros, include_files, include_modules_configs]). read_file(File, Opts) -> {Opts1, Opts2} = proplists:split(Opts, [replace_macros, include_files]), Ret = case filename:extension(File) of Ex when Ex == <<".yml">> orelse Ex == <<".yaml">> -> Files = case proplists:get_bool(include_modules_configs, Opts2) of true -> ext_mod:modules_configs(); false -> [] end, lists:foreach( fun(F) -> ?INFO_MSG("Loading third-party configuration from ~ts", [F]) end, Files), read_yaml_files([File|Files], lists:flatten(Opts1)); _ -> read_erlang_file(File, lists:flatten(Opts1)) end, case Ret of {ok, Y} -> validate(Y); Err -> Err end. read_yaml_files(Files, Opts) -> ParseOpts = [plain_as_atom | lists:flatten(Opts)], lists:foldl( fun(File, {ok, Y1}) -> case econf:parse(File, #{'_' => econf:any()}, ParseOpts) of {ok, Y2} -> {ok, Y1 ++ Y2}; Err -> Err end; (_, Err) -> Err end, {ok, []}, Files). read_erlang_file(File, _) -> case ejabberd_old_config:read_file(File) of {ok, Y} -> econf:replace_macros(Y); Err -> Err end. -spec validate(term()) -> {ok, [{atom(), term()}]} | error_return(). validate(Y1) -> case pre_validate(Y1) of {ok, Y2} -> set_loglevel(proplists:get_value(loglevel, Y2, info)), case ejabberd_config_transformer:map_reduce(Y2) of {ok, Y3} -> Hosts = proplists:get_value(hosts, Y3), Version = proplists:get_value(version, Y3, version()), create_tmp_config(), set_option(hosts, Hosts), set_option(host, hd(Hosts)), set_option(version, Version), set_option(yaml_config, Y3), {Validators, Required} = validators([]), Validator = econf:options(Validators, [{required, Required}, unique]), econf:validate(Validator, Y3); Err -> Err end; Err -> Err end. -spec pre_validate(term()) -> {ok, [{atom(), term()}]} | error_return(). pre_validate(Y1) -> econf:validate( econf:and_then( econf:options( #{hosts => ejabberd_options:opt_type(hosts), loglevel => ejabberd_options:opt_type(loglevel), version => ejabberd_options:opt_type(version), '_' => econf:any()}, [{required, [hosts]}]), fun econf:group_dups/1), Y1). -spec load_file(binary()) -> ok | error_return(). load_file(File) -> try case read_file(File) of {ok, Terms} -> case set_host_config(Terms) of {ok, Map} -> T = get_tmp_config(), Hosts = get_myhosts(), apply_defaults(T, Hosts, Map), case validate_modules(Hosts) of {ok, ModOpts} -> ets:insert(T, ModOpts), set_option(host, hd(Hosts)), commit(), set_fqdn(); Err -> abort(Err) end; Err -> abort(Err) end; Err -> abort(Err) end catch ?EX_RULE(Class, Reason, St) -> {error, {exception, Class, Reason, ?EX_STACK(St)}} end. -spec commit() -> ok. commit() -> T = get_tmp_config(), NewOpts = ets:tab2list(T), ets:insert(ejabberd_options, NewOpts), delete_tmp_config(). -spec abort(error_return()) -> error_return(). abort(Err) -> delete_tmp_config(), try ets:lookup_element(ejabberd_options, {loglevel, global}, 2) of Level -> set_loglevel(Level) catch _:badarg -> ok end, Err. -spec set_host_config([{atom(), term()}]) -> {ok, host_config()} | error_return(). set_host_config(Opts) -> Map1 = lists:foldl( fun({Opt, Val}, M) when Opt /= host_config, Opt /= append_host_config -> maps:put({Opt, global}, Val, M); (_, M) -> M end, #{}, Opts), HostOpts = proplists:get_value(host_config, Opts, []), AppendHostOpts = proplists:get_value(append_host_config, Opts, []), Map2 = lists:foldl( fun({Host, Opts1}, M1) -> lists:foldl( fun({Opt, Val}, M2) -> maps:put({Opt, Host}, Val, M2) end, M1, Opts1) end, Map1, HostOpts), Map3 = lists:foldl( fun(_, {error, _} = Err) -> Err; ({Host, Opts1}, M1) -> lists:foldl( fun(_, {error, _} = Err) -> Err; ({Opt, L1}, M2) when is_list(L1) -> L2 = try maps:get({Opt, Host}, M2) catch _:{badkey, _} -> maps:get({Opt, global}, M2, []) end, L3 = L2 ++ L1, maps:put({Opt, Host}, L3, M2); ({Opt, _}, _) -> {error, {merge_conflict, Opt, Host}} end, M1, Opts1) end, Map2, AppendHostOpts), case Map3 of {error, _} -> Map3; _ -> {ok, Map3} end. -spec apply_defaults(ets:tid(), [binary()], host_config()) -> ok. apply_defaults(Tab, Hosts, Map) -> Defaults1 = defaults(), apply_defaults(Tab, global, Map, Defaults1), {_, Defaults2} = proplists:split(Defaults1, globals()), lists:foreach( fun(Host) -> set_option(host, Host), apply_defaults(Tab, Host, Map, Defaults2) end, Hosts). -spec apply_defaults(ets:tid(), global | binary(), host_config(), [atom() | {atom(), term()}]) -> ok. apply_defaults(Tab, Host, Map, Defaults) -> lists:foreach( fun({Opt, Default}) -> try maps:get({Opt, Host}, Map) of Val -> ets:insert(Tab, {{Opt, Host}, Val}) catch _:{badkey, _} when Host == global -> Default1 = compute_default(Default, Host), ets:insert(Tab, {{Opt, Host}, Default1}); _:{badkey, _} -> try maps:get({Opt, global}, Map) of V -> ets:insert(Tab, {{Opt, Host}, V}) catch _:{badkey, _} -> Default1 = compute_default(Default, Host), ets:insert(Tab, {{Opt, Host}, Default1}) end end; (Opt) when Host == global -> Val = maps:get({Opt, Host}, Map), ets:insert(Tab, {{Opt, Host}, Val}); (_) -> ok end, Defaults). -spec defaults() -> [atom() | {atom(), term()}]. defaults() -> lists:foldl( fun(Mod, Acc) -> lists:foldl( fun({Opt, Val}, Acc1) -> lists:keystore(Opt, 1, Acc1, {Opt, Val}); (Opt, Acc1) -> case lists:member(Opt, Acc1) of true -> Acc1; false -> [Opt|Acc1] end end, Acc, Mod:options()) end, ejabberd_options:options(), callback_modules(external)). -spec globals() -> [atom()]. globals() -> lists:usort( lists:flatmap( fun(Mod) -> case erlang:function_exported(Mod, globals, 0) of true -> Mod:globals(); false -> [] end end, callback_modules(all))). %% The module validator depends on virtual host, so we have to %% validate modules in this separate function. -spec validate_modules([binary()]) -> {ok, list()} | error_return(). validate_modules(Hosts) -> lists:foldl( fun(Host, {ok, Acc}) -> set_option(host, Host), ModOpts = get_option({modules, Host}), case gen_mod:validate(Host, ModOpts) of {ok, ModOpts1} -> {ok, [{{modules, Host}, ModOpts1}|Acc]}; Err -> Err end; (_, Err) -> Err end, {ok, []}, Hosts). -spec delete_host_options([binary()]) -> ok. delete_host_options(Hosts) -> lists:foreach( fun(Host) -> ets:match_delete(ejabberd_options, {{'_', Host}, '_'}) end, Hosts). -spec compute_default(fun((global | binary()) -> T) | T, global | binary()) -> T. compute_default(F, Host) when is_function(F, 1) -> F(Host); compute_default(Val, _) -> Val. -spec set_fqdn() -> ok. set_fqdn() -> FQDNs = get_option(fqdn), xmpp:set_config([{fqdn, FQDNs}]). -spec set_shared_key() -> ok. set_shared_key() -> Key = case erlang:get_cookie() of nocookie -> str:sha(p1_rand:get_string()); Cookie -> str:sha(erlang:atom_to_binary(Cookie, latin1)) end, set_option(shared_key, Key). -spec set_node_start(integer()) -> ok. set_node_start(UnixTime) -> set_option(node_start, UnixTime). -spec set_loglevel(logger:level()) -> ok. set_loglevel(Level) -> ejabberd_logger:set(Level). ejabberd-21.12/src/mod_push_sql.erl0000644000232200023220000001752514154362354017634 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_push_sql.erl %%% Author : Evgeniy Khramtsov %%% Purpose : %%% Created : 26 Oct 2017 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_push_sql). -behaviour(mod_push). %% API -export([init/2, store_session/6, lookup_session/4, lookup_session/3, lookup_sessions/3, lookup_sessions/2, lookup_sessions/1, delete_session/3, delete_old_sessions/2, export/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -include("mod_push.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. store_session(LUser, LServer, NowTS, PushJID, Node, XData) -> XML = encode_xdata(XData), TS = misc:now_to_usec(NowTS), PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), MaxSessions = ejabberd_sm:get_max_user_sessions(LUser, LServer), enforce_max_sessions(LUser, LServer, MaxSessions), case ?SQL_UPSERT(LServer, "push_session", ["!username=%(LUser)s", "!server_host=%(LServer)s", "timestamp=%(TS)d", "!service=%(Service)s", "!node=%(Node)s", "xml=%(XML)s"]) of ok -> {ok, {NowTS, PushLJID, Node, XData}}; _Err -> {error, db_failure} end. lookup_session(LUser, LServer, PushJID, Node) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s from push_session " "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s " "and node=%(Node)s")) of {selected, [{TS, XML}]} -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), {ok, {NowTS, PushLJID, Node, XData}}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. lookup_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( LServer, ?SQL("select @(service)s, @(node)s, @(xml)s " "from push_session where username=%(LUser)s and %(LServer)H " "and timestamp=%(TS)d")) of {selected, [{Service, Node, XML}]} -> PushLJID = jid:tolower(jid:decode(Service)), XData = decode_xdata(XML, LUser, LServer), {ok, {NowTS, PushLJID, Node, XData}}; {selected, []} -> {error, notfound}; _Err -> {error, db_failure} end. lookup_sessions(LUser, LServer, PushJID) -> PushLJID = jid:tolower(PushJID), Service = jid:encode(PushLJID), case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s, @(node)s from push_session " "where username=%(LUser)s and %(LServer)H " "and service=%(Service)s")) of {selected, Rows} -> {ok, lists:map( fun({TS, XML, Node}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), {NowTS, PushLJID, Node, XData} end, Rows)}; _Err -> {error, db_failure} end. lookup_sessions(LUser, LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(timestamp)d, @(xml)s, @(node)s, @(service)s " "from push_session " "where username=%(LUser)s and %(LServer)H")) of {selected, Rows} -> {ok, lists:map( fun({TS, XML, Node, Service}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), PushLJID = jid:tolower(jid:decode(Service)), {NowTS, PushLJID,Node, XData} end, Rows)}; _Err -> {error, db_failure} end. lookup_sessions(LServer) -> case ejabberd_sql:sql_query( LServer, ?SQL("select @(username)s, @(timestamp)d, @(xml)s, " "@(node)s, @(service)s from push_session " "where %(LServer)H")) of {selected, Rows} -> {ok, lists:map( fun({LUser, TS, XML, Node, Service}) -> NowTS = misc:usec_to_now(TS), XData = decode_xdata(XML, LUser, LServer), PushLJID = jid:tolower(jid:decode(Service)), {NowTS, PushLJID, Node, XData} end, Rows)}; _Err -> {error, db_failure} end. delete_session(LUser, LServer, NowTS) -> TS = misc:now_to_usec(NowTS), case ejabberd_sql:sql_query( LServer, ?SQL("delete from push_session where " "username=%(LUser)s and %(LServer)H and timestamp=%(TS)d")) of {updated, _} -> ok; _Err -> {error, db_failure} end. delete_old_sessions(LServer, Time) -> TS = misc:now_to_usec(Time), case ejabberd_sql:sql_query( LServer, ?SQL("delete from push_session where timestamp<%(TS)d " "and %(LServer)H")) of {updated, _} -> ok; _Err -> {error, db_failure} end. export(_Server) -> [{push_session, fun(Host, #push_session{us = {LUser, LServer}, timestamp = NowTS, service = PushLJID, node = Node, xml = XData}) when LServer == Host -> TS = misc:now_to_usec(NowTS), Service = jid:encode(PushLJID), XML = encode_xdata(XData), [?SQL("delete from push_session where " "username=%(LUser)s and %(LServer)H and " "timestamp=%(TS)d and " "service=%(Service)s and node=%(Node)s and " "xml=%(XML)s;"), ?SQL_INSERT( "push_session", ["username=%(LUser)s", "server_host=%(LServer)s", "timestamp=%(TS)d", "service=%(Service)s", "node=%(Node)s", "xml=%(XML)s"])]; (_Host, _R) -> [] end}]. %%%=================================================================== %%% Internal functions %%%=================================================================== enforce_max_sessions(_LUser, _LServer, infinity) -> ok; enforce_max_sessions(LUser, LServer, MaxSessions) -> case lookup_sessions(LUser, LServer) of {ok, Sessions} when length(Sessions) >= MaxSessions -> ?INFO_MSG("Disabling old push session(s) of ~ts@~ts", [LUser, LServer]), Sessions1 = lists:sort(fun({TS1, _, _, _}, {TS2, _, _, _}) -> TS1 >= TS2 end, Sessions), OldSessions = lists:nthtail(MaxSessions - 1, Sessions1), lists:foreach(fun({TS, _, _, _}) -> delete_session(LUser, LServer, TS) end, OldSessions); _ -> ok end. decode_xdata(<<>>, _LUser, _LServer) -> undefined; decode_xdata(XML, LUser, LServer) -> case fxml_stream:parse_element(XML) of #xmlel{} = El -> try xmpp:decode(El) catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts " "from table 'push_session': ~ts", [XML, LUser, LServer, xmpp:format_error(Why)]), undefined end; Err -> ?ERROR_MSG("Failed to decode ~ts for user ~ts@~ts from " "table 'push_session': ~p", [XML, LUser, LServer, Err]), undefined end. encode_xdata(undefined) -> <<>>; encode_xdata(XData) -> fxml:element_to_binary(xmpp:encode(XData)). ejabberd-21.12/src/mod_bosh.erl0000644000232200023220000003436114154362354016726 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_bosh.erl %%% Author : Evgeniy Khramtsov %%% Purpose : This module acts as a bridge to ejabberd_bosh which implements %%% the real stuff, this is to handle the new pluggable architecture %%% for extending ejabberd's http service. %%% Created : 20 Jul 2011 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_bosh). -author('steve@zeank.in-berlin.de'). %%-define(ejabberd_debug, true). -behaviour(gen_mod). -export([start_link/0]). -export([start/2, stop/1, reload/3, process/2, open_session/2, close_session/1, find_session/1, clean_cache/1]). -export([depends/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -include("logger.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_http.hrl"). -include("bosh.hrl"). -include("translate.hrl"). -callback init() -> any(). -callback open_session(binary(), pid()) -> ok | {error, any()}. -callback close_session(binary()) -> ok | {error, any()}. -callback find_session(binary()) -> {ok, pid()} | {error, any()}. -callback use_cache() -> boolean(). -callback cache_nodes() -> [node()]. -optional_callbacks([use_cache/0, cache_nodes/0]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). process([], #request{method = 'POST', data = <<>>}) -> ?DEBUG("Bad Request: no data", []), {400, ?HEADER(?CT_XML), #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}}; process([], #request{method = 'POST', data = Data, ip = IP, headers = Hdrs}) -> ?DEBUG("Incoming data: ~p", [Data]), Type = get_type(Hdrs), ejabberd_bosh:process_request(Data, IP, Type); process([], #request{method = 'GET', data = <<>>}) -> {200, ?HEADER(?CT_XML), get_human_html_xmlel()}; process([], #request{method = 'OPTIONS', data = <<>>}) -> {200, ?OPTIONS_HEADER, []}; process(_Path, _Request) -> ?DEBUG("Bad Request: ~p", [_Request]), {400, ?HEADER(?CT_XML), #xmlel{name = <<"h1">>, attrs = [], children = [{xmlcdata, <<"400 Bad Request">>}]}}. -spec open_session(binary(), pid()) -> ok | {error, any()}. open_session(SID, Pid) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), case Mod:open_session(SID, Pid) of ok -> delete_cache(Mod, SID); {error, _} = Err -> Err end. -spec close_session(binary()) -> ok. close_session(SID) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), Mod:close_session(SID), delete_cache(Mod, SID). -spec find_session(binary()) -> {ok, pid()} | error. find_session(SID) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), case use_cache(Mod) of true -> ets_cache:lookup( ?BOSH_CACHE, SID, fun() -> case Mod:find_session(SID) of {ok, Pid} -> {ok, Pid}; {error, _} -> error end end); false -> case Mod:find_session(SID) of {ok, Pid} -> {ok, Pid}; {error, _} -> error end end. start(Host, _Opts) -> Mod = gen_mod:ram_db_mod(Host, ?MODULE), init_cache(Host, Mod), Mod:init(), clean_cache(), TmpSup = gen_mod:get_module_proc(Host, ?MODULE), TmpSupSpec = {TmpSup, {ejabberd_tmp_sup, start_link, [TmpSup, ejabberd_bosh]}, permanent, infinity, supervisor, [ejabberd_tmp_sup]}, supervisor:start_child(ejabberd_gen_mod_sup, TmpSupSpec). stop(Host) -> TmpSup = gen_mod:get_module_proc(Host, ?MODULE), supervisor:terminate_child(ejabberd_gen_mod_sup, TmpSup), supervisor:delete_child(ejabberd_gen_mod_sup, TmpSup). reload(Host, _NewOpts, _OldOpts) -> Mod = gen_mod:ram_db_mod(global, ?MODULE), init_cache(Host, Mod), Mod:init(), ok. %%%=================================================================== %%% Internal functions %%%=================================================================== get_type(Hdrs) -> try {_, S} = lists:keyfind('Content-Type', 1, Hdrs), [T|_] = str:tokens(S, <<";">>), [_, <<"json">>] = str:tokens(T, <<"/">>), json catch _:_ -> xml end. depends(_Host, _Opts) -> []. mod_opt_type(json) -> econf:and_then( econf:bool(), fun(false) -> false; (true) -> ejabberd:start_app(jiffy), true end); mod_opt_type(max_concat) -> econf:pos_int(unlimited); mod_opt_type(max_inactivity) -> econf:timeout(second); mod_opt_type(max_pause) -> econf:timeout(second); mod_opt_type(prebind) -> econf:bool(); mod_opt_type(queue_type) -> econf:queue_type(); mod_opt_type(ram_db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). -spec mod_options(binary()) -> [{json, boolean()} | {atom(), term()}]. mod_options(Host) -> [{json, false}, {max_concat, unlimited}, {max_inactivity, timer:seconds(30)}, {max_pause, timer:seconds(120)}, {prebind, false}, {ram_db_type, ejabberd_config:default_ram_db(Host, ?MODULE)}, {queue_type, ejabberd_option:queue_type(Host)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module implements XMPP over BOSH as defined in " "https://xmpp.org/extensions/xep-0124.html[XEP-0124] and " "https://xmpp.org/extensions/xep-0206.html[XEP-0206]. BOSH " "stands for Bidirectional-streams Over Synchronous HTTP. " "It makes it possible to simulate long lived connections " "required by XMPP over the HTTP protocol. In practice, " "this module makes it possible to use XMPP in a browser without " "Websocket support and more generally to have a way to use " "XMPP while having to get through an HTTP proxy."), opts => [{json, #{value => "true | false", desc => ?T("This option has no effect.")}}, {max_concat, #{value => "pos_integer() | infinity", desc => ?T("This option limits the number of stanzas that the server " "will send in a single bosh request. " "The default value is 'unlimited'.")}}, {max_inactivity, #{value => "timeout()", desc => ?T("The option defines the maximum inactivity period. " "The default value is '30' seconds.")}}, {max_pause, #{value => "pos_integer()", desc => ?T("Indicate the maximum length of a temporary session pause " "(in seconds) that a client can request. " "The default value is '120'.")}}, {prebind, #{value => "true | false", desc => ?T("If enabled, the client can create the session without " "going through authentication. Basically, it creates a " "new session with anonymous authentication. " "The default value is 'false'.")}}, {queue_type, #{value => "ram | file", desc => ?T("Same as top-level _`queue_type`_ option, but applied to this module only.")}}, {ram_db_type, #{value => "mnesia | sql | redis", desc => ?T("Same as _`default_ram_db`_ but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => ["listen:", " -", " port: 5222", " module: ejabberd_c2s", " -", " port: 5443", " module: ejabberd_http", " request_handlers:", " /bosh: mod_bosh", "", "modules:", " mod_bosh: {}"]}. %%%---------------------------------------------------------------------- %%% Cache stuff %%%---------------------------------------------------------------------- -spec init_cache(binary(), module()) -> ok. init_cache(Host, Mod) -> case use_cache(Mod, Host) of true -> ets_cache:new(?BOSH_CACHE, cache_opts(Host)); false -> ets_cache:delete(?BOSH_CACHE) end. -spec use_cache(module()) -> boolean(). use_cache(Mod) -> use_cache(Mod, global). -spec use_cache(module(), global | binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 0) of true -> Mod:use_cache(); false -> mod_bosh_opt:use_cache(Host) end. -spec cache_nodes(module()) -> [node()]. cache_nodes(Mod) -> case erlang:function_exported(Mod, cache_nodes, 0) of true -> Mod:cache_nodes(); false -> ejabberd_cluster:get_nodes() end. -spec delete_cache(module(), binary()) -> ok. delete_cache(Mod, SID) -> case use_cache(Mod) of true -> ets_cache:delete(?BOSH_CACHE, SID, cache_nodes(Mod)); false -> ok end. -spec cache_opts(binary()) -> [proplists:property()]. cache_opts(Host) -> MaxSize = mod_bosh_opt:cache_size(Host), CacheMissed = mod_bosh_opt:cache_missed(Host), LifeTime = mod_bosh_opt:cache_life_time(Host), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec clean_cache(node()) -> non_neg_integer(). clean_cache(Node) -> ets_cache:filter( ?BOSH_CACHE, fun(_, error) -> false; (_, {ok, Pid}) -> node(Pid) /= Node end). -spec clean_cache() -> ok. clean_cache() -> ejabberd_cluster:eval_everywhere(?MODULE, clean_cache, [node()]). %%%---------------------------------------------------------------------- %%% Help Web Page %%%---------------------------------------------------------------------- get_human_html_xmlel() -> Heading = <<"ejabberd ", (iolist_to_binary(atom_to_list(?MODULE)))/binary>>, #xmlel{name = <<"html">>, attrs = [{<<"xmlns">>, <<"http://www.w3.org/1999/xhtml">>}], children = [#xmlel{name = <<"head">>, children = [#xmlel{name = <<"title">>, children = [{xmlcdata, Heading}]}, #xmlel{name = <<"style">>, children = [{xmlcdata, get_style_cdata()}]}]}, #xmlel{name = <<"body">>, children = [#xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"container">>}], children = get_container_children(Heading)}]}]}. get_container_children(Heading) -> [#xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"section">>}], children = [#xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"block">>}], children = [#xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}], children = [#xmlel{name = <<"img">>, attrs = [{<<"height">>, <<"32">>}, {<<"src">>, get_image_src()}]}]}]}]}, #xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"white section">>}], children = [#xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"block">>}], children = [#xmlel{name = <<"h1">>, children = [{xmlcdata, Heading}]}, #xmlel{name = <<"p">>, children = [{xmlcdata, <<"An implementation of ">>}, #xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"http://xmpp.org/extensions/xep-0206.html">>}], children = [{xmlcdata, <<"XMPP over BOSH (XEP-0206)">>}]}]}, #xmlel{name = <<"p">>, children = [{xmlcdata, <<"This web page is only informative. To " "use HTTP-Bind you need a Jabber/XMPP " "client that supports it.">>}]}]}]}, #xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"section">>}], children = [#xmlel{name = <<"div">>, attrs = [{<<"class">>, <<"block">>}], children = [#xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"https://www.ejabberd.im">>}, {<<"title">>, <<"ejabberd XMPP server">>}], children = [{xmlcdata, <<"ejabberd">>}]}, {xmlcdata, <<" is maintained by ">>}, #xmlel{name = <<"a">>, attrs = [{<<"href">>, <<"https://www.process-one.net">>}, {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], children = [{xmlcdata, <<"ProcessOne">>}]} ]}]} ]. get_style_cdata() -> case misc:read_css("bosh.css") of {ok, Data} -> Data; {error, _} -> <<>> end. get_image_src() -> case misc:read_img("bosh-logo.png") of {ok, Img} -> B64Img = base64:encode(Img), <<"data:image/png;base64,", B64Img/binary>>; {error, _} -> <<>> end. ejabberd-21.12/src/ejabberd_oauth_rest.erl0000644000232200023220000001254414154362354021126 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_oauth_rest.erl %%% Author : Alexey Shchepin %%% Purpose : OAUTH2 REST backend %%% Created : 26 Jul 2016 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%------------------------------------------------------------------- -module(ejabberd_oauth_rest). -behaviour(ejabberd_oauth). -export([init/0, store/1, lookup/1, clean/1, lookup_client/1, store_client/1]). -include("ejabberd_oauth.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/jid.hrl"). init() -> rest:start(ejabberd_config:get_myname()), ok. store(R) -> Path = path(<<"store">>), %% Retry 2 times, with a backoff of 500millisec {User, Server} = R#oauth_token.us, SJID = jid:encode({User, Server, <<"">>}), case rest:with_retry( post, [ejabberd_config:get_myname(), Path, [], {[{<<"token">>, R#oauth_token.token}, {<<"user">>, SJID}, {<<"scope">>, R#oauth_token.scope}, {<<"expire">>, R#oauth_token.expire} ]}], 2, 500) of {ok, Code, _} when Code == 200 orelse Code == 201 -> ok; Err -> ?ERROR_MSG("Failed to store oauth record ~p: ~p", [R, Err]), {error, db_failure} end. lookup(Token) -> Path = path(<<"lookup">>), case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], {[{<<"token">>, Token}]}], 2, 500) of {ok, 200, {Data}} -> SJID = proplists:get_value(<<"user">>, Data, <<>>), JID = jid:decode(SJID), US = {JID#jid.luser, JID#jid.lserver}, Scope = proplists:get_value(<<"scope">>, Data, []), Expire = proplists:get_value(<<"expire">>, Data, 0), {ok, #oauth_token{token = Token, us = US, scope = Scope, expire = Expire}}; {ok, 404, _Resp} -> error; Other -> ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]), case ejabberd_option:oauth_cache_rest_failure_life_time() of infinity -> error; Time -> {cache_with_timeout, error, Time} end end. clean(_TS) -> ok. path(Path) -> Base = ejabberd_option:ext_api_path_oauth(), <>. store_client(#oauth_client{client_id = ClientID, client_name = ClientName, grant_type = GrantType, options = Options} = R) -> Path = path(<<"store_client">>), SGrantType = case GrantType of password -> <<"password">>; implicit -> <<"implicit">> end, SOptions = misc:term_to_base64(Options), %% Retry 2 times, with a backoff of 500millisec case rest:with_retry( post, [ejabberd_config:get_myname(), Path, [], {[{<<"client_id">>, ClientID}, {<<"client_name">>, ClientName}, {<<"grant_type">>, SGrantType}, {<<"options">>, SOptions} ]}], 2, 500) of {ok, Code, _} when Code == 200 orelse Code == 201 -> ok; Err -> ?ERROR_MSG("Failed to store oauth record ~p: ~p", [R, Err]), {error, db_failure} end. lookup_client(ClientID) -> Path = path(<<"lookup_client">>), case rest:with_retry(post, [ejabberd_config:get_myname(), Path, [], {[{<<"client_id">>, ClientID}]}], 2, 500) of {ok, 200, {Data}} -> ClientName = proplists:get_value(<<"client_name">>, Data, <<>>), SGrantType = proplists:get_value(<<"grant_type">>, Data, <<>>), GrantType = case SGrantType of <<"password">> -> password; <<"implicit">> -> implicit end, SOptions = proplists:get_value(<<"options">>, Data, <<>>), case misc:base64_to_term(SOptions) of {term, Options} -> {ok, #oauth_client{client_id = ClientID, client_name = ClientName, grant_type = GrantType, options = Options}}; _ -> error end; {ok, 404, _Resp} -> error; Other -> ?ERROR_MSG("Unexpected response for oauth lookup: ~p", [Other]), error end. ejabberd-21.12/src/mod_pres_counter.erl0000644000232200023220000001177014154362354020502 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_pres_counter.erl %%% Author : Ahmed Omar %%% Purpose : Presence subscription flood prevention %%% Created : 23 Sep 2010 by Ahmed Omar %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_pres_counter). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, check_packet/4, mod_opt_type/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include("translate.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -record(pres_counter, {dir, start, count, logged = false}). start(Host, _Opts) -> ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, check_packet, 25), ok. stop(Host) -> ejabberd_hooks:delete(privacy_check_packet, Host, ?MODULE, check_packet, 25), ok. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. -spec check_packet(allow | deny, ejabberd_c2s:state() | jid(), stanza(), in | out) -> allow | deny. check_packet(Acc, #{jid := JID}, Packet, Dir) -> check_packet(Acc, JID, Packet, Dir); check_packet(_, #jid{lserver = LServer}, #presence{from = From, to = To, type = Type}, Dir) -> IsSubscription = case Type of subscribe -> true; subscribed -> true; unsubscribe -> true; unsubscribed -> true; _ -> false end, if IsSubscription -> JID = case Dir of in -> To; out -> From end, update(LServer, JID, Dir); true -> allow end; check_packet(Acc, _, _, _) -> Acc. update(Server, JID, Dir) -> StormCount = mod_pres_counter_opt:count(Server), TimeInterval = mod_pres_counter_opt:interval(Server), TimeStamp = erlang:system_time(millisecond), case read(Dir) of undefined -> write(Dir, #pres_counter{dir = Dir, start = TimeStamp, count = 1}), allow; #pres_counter{start = TimeStart, count = Count, logged = Logged} = R -> if TimeStamp - TimeStart > TimeInterval -> write(Dir, R#pres_counter{start = TimeStamp, count = 1}), allow; (Count =:= StormCount) and Logged -> {stop, deny}; Count =:= StormCount -> write(Dir, R#pres_counter{logged = true}), case Dir of in -> ?WARNING_MSG("User ~ts is being flooded, ignoring received " "presence subscriptions", [jid:encode(JID)]); out -> IP = ejabberd_sm:get_user_ip(JID#jid.luser, JID#jid.lserver, JID#jid.lresource), ?WARNING_MSG("Flooder detected: ~ts, on IP: ~ts ignoring " "sent presence subscriptions~n", [jid:encode(JID), misc:ip_to_list(IP)]) end, {stop, deny}; true -> write(Dir, R#pres_counter{start = TimeStamp, count = Count + 1}), allow end end. read(K) -> get({pres_counter, K}). write(K, V) -> put({pres_counter, K}, V). mod_opt_type(count) -> econf:pos_int(); mod_opt_type(interval) -> econf:timeout(second). mod_options(_) -> [{count, 5}, {interval, timer:seconds(60)}]. mod_doc() -> #{desc => ?T("This module detects flood/spam in presence " "subscriptions traffic. If a user sends or receives " "more of those stanzas in a given time interval, " "the exceeding stanzas are silently dropped, and a " "warning is logged."), opts => [{count, #{value => ?T("Number"), desc => ?T("The number of subscription presence stanzas " "(subscribe, unsubscribe, subscribed, unsubscribed) " "allowed for any direction (input or output) per time " "defined in 'interval' option. Please note that two " "users subscribing to each other usually generate 4 " "stanzas, so the recommended value is '4' or more. " "The default value is '5'.")}}, {interval, #{value => "timeout()", desc => ?T("The time interval. The default value is '1' minute.")}}], example => ["modules:", " ...", " mod_pres_counter:", " count: 5", " interval: 30 secs", " ..."]}. ejabberd-21.12/src/mod_mix.erl0000644000232200023220000006347214154362354016575 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_mix.erl %%% Author : Evgeny Khramtsov %%% Created : 2 Mar 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2018 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(mod_mix). -behaviour(gen_mod). -behaviour(gen_server). -protocol({xep, 369, '0.14.1'}). %% API -export([route/1]). %% gen_mod callbacks -export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). -export([mod_doc/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, format_status/2]). %% Hooks -export([process_disco_info/1, process_disco_items/1, process_mix_core/1, process_mam_query/1, process_pubsub_query/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("translate.hrl"). -include("ejabberd_stacktrace.hrl"). -callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. -callback set_channel(binary(), binary(), binary(), jid:jid(), boolean(), binary()) -> ok | {error, db_failure}. -callback get_channels(binary(), binary()) -> {ok, [binary()]} | {error, db_failure}. -callback get_channel(binary(), binary(), binary()) -> {ok, {jid(), boolean(), binary()}} | {error, notfound | db_failure}. -callback set_participant(binary(), binary(), binary(), jid(), binary(), binary()) -> ok | {error, db_failure}. -callback get_participant(binary(), binary(), binary(), jid()) -> {ok, {binary(), binary()}} | {error, notfound | db_failure}. -record(state, {hosts :: [binary()], server_host :: binary()}). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). depends(_Host, _Opts) -> [{mod_mam, hard}]. mod_opt_type(access_create) -> econf:acl(); mod_opt_type(name) -> econf:binary(); mod_opt_type(host) -> econf:host(); mod_opt_type(hosts) -> econf:hosts(); mod_opt_type(db_type) -> econf:db_type(?MODULE). mod_options(Host) -> [{access_create, all}, {host, <<"mix.", Host/binary>>}, {hosts, []}, {name, ?T("Channels")}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}]. mod_doc() -> #{desc => [?T("This module is an experimental implementation of " "https://xmpp.org/extensions/xep-0369.html" "[XEP-0369: Mediated Information eXchange (MIX)]. " "MIX support was added in ejabberd 16.03 as an " "experimental feature, updated in 19.02, and is not " "yet ready to use in production. It's asserted that " "the MIX protocol is going to replace the MUC protocol " "in the future (see _`mod_muc`_)."), "", ?T("To learn more about how to use that feature, you can refer to " "our tutorial: https://docs.ejabberd.im/tutorials/mix-010/" "[Getting started with XEP-0369: Mediated Information " "eXchange (MIX) v0.1]."), "", ?T("The module depends on _`mod_mam`_.")], opts => [{access_create, #{value => ?T("AccessName"), desc => ?T("An access rule to control MIX channels creations. " "The default value is 'all'.")}}, {host, #{desc => ?T("Deprecated. Use 'hosts' instead.")}}, {hosts, #{value => ?T("[Host, ...]"), desc => ?T("This option defines the Jabber IDs of the service. " "If the 'hosts' option is not specified, the only Jabber ID will " "be the hostname of the virtual host with the prefix \"mix.\". " "The keyword '@HOST@' is replaced with the real virtual host name.")}}, {name, #{value => ?T("Name"), desc => ?T("A name of the service in the Service Discovery. " "This will only be displayed by special XMPP clients. " "The default value is 'Channels'.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}]}. -spec route(stanza()) -> ok. route(#iq{} = IQ) -> ejabberd_router:process_iq(IQ); route(#message{type = groupchat, id = ID, lang = Lang, to = #jid{luser = <<_, _/binary>>}} = Msg) -> case ID of <<>> -> Txt = ?T("Attribute 'id' is mandatory for MIX messages"), Err = xmpp:err_bad_request(Txt, Lang), ejabberd_router:route_error(Msg, Err); _ -> process_mix_message(Msg) end; route(Pkt) -> ?DEBUG("Dropping packet:~n~ts", [xmpp:pp(Pkt)]). -spec process_disco_info(iq()) -> iq(). process_disco_info(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To, from = _From, lang = Lang, sub_els = [#disco_info{node = <<>>}]} = IQ) -> ServerHost = ejabberd_router:host_of_route(To#jid.lserver), X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], [ServerHost, ?MODULE, <<"">>, Lang]), Name = mod_mix_opt:name(ServerHost), Identity = #identity{category = <<"conference">>, type = <<"mix">>, name = translate:translate(Lang, Name)}, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MIX_CORE_0, ?NS_MIX_CORE_SEARCHABLE_0, ?NS_MIX_CORE_CREATE_CHANNEL_0], xmpp:make_iq_result( IQ, #disco_info{features = Features, identities = [Identity], xdata = X}); process_disco_info(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, sub_els = [#disco_info{node = Node}]} = IQ) when Node == <<"mix">>; Node == <<>> -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> Identity = #identity{category = <<"conference">>, type = <<"mix">>}, Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MIX_CORE_0, ?NS_MAM_2], xmpp:make_iq_result( IQ, #disco_info{node = Node, features = Features, identities = [Identity]}); {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; process_disco_info(#iq{type = get, sub_els = [#disco_info{node = Node}]} = IQ) -> xmpp:make_iq_result(IQ, #disco_info{node = Node, features = [?NS_DISCO_INFO]}); process_disco_info(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). -spec process_disco_items(iq()) -> iq(). process_disco_items(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_disco_items(#iq{type = get, to = #jid{luser = <<>>} = To, sub_els = [#disco_items{node = <<>>}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channels(ServerHost, Host) of {ok, Channels} -> Items = [#disco_item{jid = jid:make(Channel, Host)} || Channel <- Channels], xmpp:make_iq_result(IQ, #disco_items{items = Items}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; process_disco_items(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, sub_els = [#disco_items{node = Node}]} = IQ) when Node == <<"mix">>; Node == <<>> -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> BTo = jid:remove_resource(To), Items = [#disco_item{jid = BTo, node = N} || N <- known_nodes()], xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; process_disco_items(#iq{type = get, sub_els = [#disco_items{node = Node}]} = IQ) -> xmpp:make_iq_result(IQ, #disco_items{node = Node}); process_disco_items(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). -spec process_mix_core(iq()) -> iq(). process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, sub_els = [#mix_create{}]} = IQ) -> process_mix_create(IQ); process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, sub_els = [#mix_destroy{}]} = IQ) -> process_mix_destroy(IQ); process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, sub_els = [#mix_join{}]} = IQ) -> process_mix_join(IQ); process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, sub_els = [#mix_leave{}]} = IQ) -> process_mix_leave(IQ); process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, sub_els = [#mix_setnick{}]} = IQ) -> process_mix_setnick(IQ); process_mix_core(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). process_pubsub_query(#iq{type = get, sub_els = [#pubsub{items = #ps_items{node = Node}}]} = IQ) when Node == ?NS_MIX_NODES_PARTICIPANTS -> process_participants_list(IQ); process_pubsub_query(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). process_mam_query(#iq{from = From, to = To, type = T, sub_els = [#mam_query{}]} = IQ) when T == get; T == set -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> BFrom = jid:remove_resource(From), case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, _} -> mod_mam:process_iq(ServerHost, IQ, mix); {error, notfound} -> xmpp:make_error(IQ, not_joined_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; process_mam_query(IQ) -> xmpp:make_error(IQ, unsupported_error(IQ)). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), MyHosts = gen_mod:get_opt_hosts(Opts), case Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)) of ok -> lists:foreach( fun(MyHost) -> ejabberd_router:register_route( MyHost, Host, {apply, ?MODULE, route}), register_iq_handlers(MyHost) end, MyHosts), {ok, #state{hosts = MyHosts, server_host = Host}}; {error, db_failure} -> {stop, db_failure} end. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Request, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Request]), {noreply, State}. handle_info({route, Packet}, State) -> try route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> lists:foreach( fun(MyHost) -> unregister_iq_handlers(MyHost), ejabberd_router:unregister_route(MyHost) end, State#state.hosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. format_status(_Opt, Status) -> Status. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_mix_create(iq()) -> iq(). process_mix_create(#iq{to = To, from = From, sub_els = [#mix_create{channel = Chan}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), Creator = jid:remove_resource(From), Chan1 = case Chan of <<>> -> p1_rand:get_string(); _ -> Chan end, Ret = case Mod:get_channel(ServerHost, Chan1, Host) of {ok, {#jid{luser = U, lserver = S}, _, _}} -> case {From#jid.luser, From#jid.lserver} of {U, S} -> ok; _ -> {error, conflict} end; {error, notfound} -> Key = xmpp_util:hex(p1_rand:bytes(20)), Mod:set_channel(ServerHost, Chan1, Host, Creator, Chan == <<>>, Key); {error, db_failure} = Err -> Err end, case Ret of ok -> xmpp:make_iq_result(IQ, #mix_create{channel = Chan1}); {error, conflict} -> xmpp:make_error(IQ, channel_exists_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_mix_destroy(iq()) -> iq(). process_mix_destroy(#iq{to = To, from = #jid{luser = U, lserver = S}, sub_els = [#mix_destroy{channel = Chan}]} = IQ) -> Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, {#jid{luser = U, lserver = S}, _, _}} -> case Mod:del_channel(ServerHost, Chan, Host) of ok -> xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {ok, _} -> xmpp:make_error(IQ, ownership_error(IQ)); {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_mix_join(iq()) -> iq(). process_mix_join(#iq{to = To, from = From, sub_els = [#mix_join{} = JoinReq]} = IQ) -> Chan = To#jid.luser, Host = To#jid.lserver, ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, {_, _, Key}} -> ID = make_id(From, Key), Nick = JoinReq#mix_join.nick, BFrom = jid:remove_resource(From), Nodes = filter_nodes(JoinReq#mix_join.subscribe), try ok = Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick), ok = Mod:subscribe(ServerHost, Chan, Host, BFrom, Nodes), notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), xmpp:make_iq_result(IQ, #mix_join{id = ID, subscribe = Nodes, nick = Nick}) catch _:{badmatch, {error, db_failure}} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_mix_leave(iq()) -> iq(). process_mix_leave(#iq{to = To, from = From, sub_els = [#mix_leave{}]} = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), BFrom = jid:remove_resource(From), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, {ID, _}} -> try ok = Mod:unsubscribe(ServerHost, Chan, Host, BFrom), ok = Mod:del_participant(ServerHost, Chan, Host, BFrom), notify_participant_left(Mod, ServerHost, To, ID), xmpp:make_iq_result(IQ, #mix_leave{}) catch _:{badmatch, {error, db_failure}} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_iq_result(IQ, #mix_leave{}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_iq_result(IQ, #mix_leave{}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_mix_setnick(iq()) -> iq(). process_mix_setnick(#iq{to = To, from = From, sub_els = [#mix_setnick{nick = Nick}]} = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), BFrom = jid:remove_resource(From), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, {_, Nick}} -> xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick}); {ok, {ID, _}} -> case Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick) of ok -> notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick}); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, not_joined_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec process_mix_message(message()) -> ok. process_mix_message(#message{from = From, to = To, id = SubmissionID} = Msg) -> {Chan, Host, _} = jid:tolower(To), {FUser, FServer, _} = jid:tolower(From), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> BFrom = jid:remove_resource(From), case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, {StableID, Nick}} -> MamID = mod_mam:make_id(), Msg1 = xmpp:set_subtag( Msg#message{from = jid:replace_resource(To, StableID), to = undefined, id = integer_to_binary(MamID)}, #mix{jid = BFrom, nick = Nick}), Msg2 = xmpp:put_meta(Msg1, stanza_id, MamID), case ejabberd_hooks:run_fold( store_mam_message, ServerHost, Msg2, [Chan, Host, BFrom, Nick, groupchat, recv]) of #message{} -> multicast(Mod, ServerHost, Chan, Host, ?NS_MIX_NODES_MESSAGES, fun(#jid{luser = U, lserver = S}) when U == FUser, S == FServer -> xmpp:set_subtag( Msg1, #mix{jid = BFrom, nick = Nick, submission_id = SubmissionID}); (_) -> Msg1 end); _ -> ok end; {error, notfound} -> ejabberd_router:route_error(Msg, not_joined_error(Msg)); {error, db_failure} -> ejabberd_router:route_error(Msg, db_error(Msg)) end; {error, notfound} -> ejabberd_router:route_error(Msg, no_channel_error(Msg)); {error, db_failure} -> ejabberd_router:route_error(Msg, db_error(Msg)) end. -spec process_participants_list(iq()) -> iq(). process_participants_list(#iq{from = From, to = To} = IQ) -> {Chan, Host, _} = jid:tolower(To), ServerHost = ejabberd_router:host_of_route(Host), Mod = gen_mod:db_mod(ServerHost, ?MODULE), case Mod:get_channel(ServerHost, Chan, Host) of {ok, _} -> BFrom = jid:remove_resource(From), case Mod:get_participant(ServerHost, Chan, Host, BFrom) of {ok, _} -> case Mod:get_participants(ServerHost, Chan, Host) of {ok, Participants} -> Items = items_of_participants(Participants), Pubsub = #pubsub{ items = #ps_items{ node = ?NS_MIX_NODES_PARTICIPANTS, items = Items}}, xmpp:make_iq_result(IQ, Pubsub); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, not_joined_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end; {error, notfound} -> xmpp:make_error(IQ, no_channel_error(IQ)); {error, db_failure} -> xmpp:make_error(IQ, db_error(IQ)) end. -spec items_of_participants([{jid(), binary(), binary()}]) -> [ps_item()]. items_of_participants(Participants) -> lists:map( fun({JID, ID, Nick}) -> Participant = #mix_participant{jid = JID, nick = Nick}, #ps_item{id = ID, sub_els = [xmpp:encode(Participant)]} end, Participants). -spec known_nodes() -> [binary()]. known_nodes() -> [?NS_MIX_NODES_MESSAGES, ?NS_MIX_NODES_PARTICIPANTS]. -spec filter_nodes([binary()]) -> [binary()]. filter_nodes(Nodes) -> lists:filter( fun(Node) -> lists:member(Node, Nodes) end, known_nodes()). -spec multicast(module(), binary(), binary(), binary(), binary(), fun((jid()) -> message())) -> ok. multicast(Mod, LServer, Chan, Service, Node, F) -> case Mod:get_subscribed(LServer, Chan, Service, Node) of {ok, Subscribers} -> lists:foreach( fun(To) -> Msg = xmpp:set_to(F(To), To), ejabberd_router:route(Msg) end, Subscribers); {error, db_failure} -> ok end. -spec notify_participant_joined(module(), binary(), jid(), jid(), binary(), binary()) -> ok. notify_participant_joined(Mod, LServer, To, From, ID, Nick) -> {Chan, Host, _} = jid:tolower(To), Participant = #mix_participant{jid = jid:remove_resource(From), nick = Nick}, Item = #ps_item{id = ID, sub_els = [xmpp:encode(Participant)]}, Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, items = [Item]}, Event = #ps_event{items = Items}, Msg = #message{from = jid:remove_resource(To), id = p1_rand:get_string(), sub_els = [Event]}, multicast(Mod, LServer, Chan, Host, ?NS_MIX_NODES_PARTICIPANTS, fun(_) -> Msg end). -spec notify_participant_left(module(), binary(), jid(), binary()) -> ok. notify_participant_left(Mod, LServer, To, ID) -> {Chan, Host, _} = jid:tolower(To), Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, retract = ID}, Event = #ps_event{items = Items}, Msg = #message{from = jid:remove_resource(To), id = p1_rand:get_string(), sub_els = [Event]}, multicast(Mod, LServer, Chan, Host, ?NS_MIX_NODES_PARTICIPANTS, fun(_) -> Msg end). -spec make_id(jid(), binary()) -> binary(). make_id(JID, Key) -> Data = jid:encode(jid:tolower(jid:remove_resource(JID))), xmpp_util:hex(misc:crypto_hmac(sha256, Data, Key, 10)). %%%=================================================================== %%% Error generators %%%=================================================================== -spec db_error(stanza()) -> stanza_error(). db_error(Pkt) -> Txt = ?T("Database failure"), xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). -spec channel_exists_error(stanza()) -> stanza_error(). channel_exists_error(Pkt) -> Txt = ?T("Channel already exists"), xmpp:err_conflict(Txt, xmpp:get_lang(Pkt)). -spec no_channel_error(stanza()) -> stanza_error(). no_channel_error(Pkt) -> Txt = ?T("Channel does not exist"), xmpp:err_item_not_found(Txt, xmpp:get_lang(Pkt)). -spec not_joined_error(stanza()) -> stanza_error(). not_joined_error(Pkt) -> Txt = ?T("You are not joined to the channel"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). -spec unsupported_error(stanza()) -> stanza_error(). unsupported_error(Pkt) -> Txt = ?T("No module is handling this query"), xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). -spec ownership_error(stanza()) -> stanza_error(). ownership_error(Pkt) -> Txt = ?T("Owner privileges required"), xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). %%%=================================================================== %%% IQ handlers %%%=================================================================== -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0, ?MODULE, process_mix_core), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, ?MODULE, process_disco_info), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, ?MODULE, process_disco_items), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0, ?MODULE, process_mix_core), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB, ?MODULE, process_pubsub_query), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, ?MODULE, process_mam_query). -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). ejabberd-21.12/src/ejabberd_sql_pt.erl0000644000232200023220000010146014154362354020247 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_sql_pt.erl %%% Author : Alexey Shchepin %%% Description : Parse transform for SQL queries %%% Created : 20 Jan 2016 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_sql_pt). %% API -export([parse_transform/2, format_error/1]). -include("ejabberd_sql.hrl"). -record(state, {loc, 'query' = [], params = [], param_pos = 0, args = [], res = [], res_vars = [], res_pos = 0, server_host_used = false, used_vars = [], use_new_schema, need_array_pass = false}). -define(QUERY_RECORD, "sql_query"). -define(ESCAPE_RECORD, "sql_escape"). -define(ESCAPE_VAR, "__SQLEscape"). -define(MOD, sql__module_). -ifdef(NEW_SQL_SCHEMA). -define(USE_NEW_SCHEMA, true). -else. -define(USE_NEW_SCHEMA, false). -endif. %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: %% Description: %%-------------------------------------------------------------------- parse_transform(AST, _Options) -> put(warnings, []), NewAST = top_transform(AST), NewAST ++ get(warnings). format_error(no_server_host) -> "server_host field is not used". %%==================================================================== %% Internal functions %%==================================================================== transform(Form) -> case erl_syntax:type(Form) of application -> case erl_syntax_lib:analyze_application(Form) of {?SQL_MARK, 1} -> case erl_syntax:application_arguments(Form) of [Arg] -> case erl_syntax:type(Arg) of string -> transform_sql(Arg); _ -> throw({error, erl_syntax:get_pos(Form), "?SQL argument must be " "a constant string"}) end; _ -> throw({error, erl_syntax:get_pos(Form), "wrong number of ?SQL args"}) end; {?SQL_UPSERT_MARK, 2} -> case erl_syntax:application_arguments(Form) of [TableArg, FieldsArg] -> case {erl_syntax:type(TableArg), erl_syntax:is_proper_list(FieldsArg)}of {string, true} -> transform_upsert(Form, TableArg, FieldsArg); _ -> throw({error, erl_syntax:get_pos(Form), "?SQL_UPSERT arguments must be " "a constant string and a list"}) end; _ -> throw({error, erl_syntax:get_pos(Form), "wrong number of ?SQL_UPSERT args"}) end; {?SQL_INSERT_MARK, 2} -> case erl_syntax:application_arguments(Form) of [TableArg, FieldsArg] -> case {erl_syntax:type(TableArg), erl_syntax:is_proper_list(FieldsArg)}of {string, true} -> transform_insert(Form, TableArg, FieldsArg); _ -> throw({error, erl_syntax:get_pos(Form), "?SQL_INSERT arguments must be " "a constant string and a list"}) end; _ -> throw({error, erl_syntax:get_pos(Form), "wrong number of ?SQL_INSERT args"}) end; _ -> Form end; attribute -> case erl_syntax:atom_value(erl_syntax:attribute_name(Form)) of module -> case erl_syntax:attribute_arguments(Form) of [M | _] -> Module = erl_syntax:atom_value(M), put(?MOD, Module), Form; _ -> Form end; _ -> Form end; _ -> Form end. top_transform(Forms) when is_list(Forms) -> lists:map( fun(Form) -> try Form2 = erl_syntax_lib:map(fun transform/1, Form), Form3 = erl_syntax:revert(Form2), Form3 catch throw:{error, Line, Error} -> {error, {Line, erl_parse, Error}} end end, Forms). transform_sql(Arg) -> S = erl_syntax:string_value(Arg), Pos = erl_syntax:get_pos(Arg), ParseRes = parse(S, Pos, true), ParseResOld = parse(S, Pos, false), case ParseRes#state.server_host_used of {true, _SHVar} -> ok; false -> add_warning( Pos, no_server_host), [] end, case ParseRes#state.need_array_pass of true -> {PR1, PR2} = perform_array_pass(ParseRes), {PRO1, PRO2} = perform_array_pass(ParseResOld), set_pos(make_schema_check( erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PR2)]), erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(PR1)])]), erl_syntax:list([erl_syntax:tuple([erl_syntax:atom(pgsql), make_sql_query(PRO2)]), erl_syntax:tuple([erl_syntax:atom(any), make_sql_query(PRO1)])])), Pos); false -> set_pos( make_schema_check( make_sql_query(ParseRes), make_sql_query(ParseResOld) ), Pos) end. transform_upsert(Form, TableArg, FieldsArg) -> Table = erl_syntax:string_value(TableArg), ParseRes = parse_upsert( erl_syntax:list_elements(FieldsArg)), Pos = erl_syntax:get_pos(Form), case lists:keymember( "server_host", 1, ParseRes) of true -> ok; false -> add_warning(Pos, no_server_host) end, ParseResOld = filter_upsert_sh(Table, ParseRes), set_pos( make_schema_check( make_sql_upsert(Table, ParseRes, Pos), make_sql_upsert(Table, ParseResOld, Pos) ), Pos). transform_insert(Form, TableArg, FieldsArg) -> Table = erl_syntax:string_value(TableArg), ParseRes = parse_insert( erl_syntax:list_elements(FieldsArg)), Pos = erl_syntax:get_pos(Form), case lists:keymember( "server_host", 1, ParseRes) of true -> ok; false -> add_warning(Pos, no_server_host) end, ParseResOld = filter_upsert_sh(Table, ParseRes), set_pos( make_schema_check( make_sql_insert(Table, ParseRes), make_sql_insert(Table, ParseResOld) ), Pos). parse(S, Loc, UseNewSchema) -> parse1(S, [], #state{loc = Loc, use_new_schema = UseNewSchema}). parse(S, ParamPos, Loc, UseNewSchema) -> parse1(S, [], #state{loc = Loc, param_pos = ParamPos, use_new_schema = UseNewSchema}). parse1([], Acc, State) -> State1 = append_string(lists:reverse(Acc), State), State1#state{'query' = lists:reverse(State1#state.'query'), params = lists:reverse(State1#state.params), args = lists:reverse(State1#state.args), res = lists:reverse(State1#state.res), res_vars = lists:reverse(State1#state.res_vars) }; parse1([$@, $( | S], Acc, State) -> State1 = append_string(lists:reverse(Acc), State), {Name, Type, S1, State2} = parse_name(S, false, State1), Var = "__V" ++ integer_to_list(State2#state.res_pos), EVar = erl_syntax:variable(Var), Convert = case Type of integer -> erl_syntax:application( erl_syntax:atom(binary_to_integer), [EVar]); string -> EVar; boolean -> erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(to_bool), [EVar]) end, State3 = append_string(Name, State2), State4 = State3#state{res_pos = State3#state.res_pos + 1, res = [Convert | State3#state.res], res_vars = [EVar | State3#state.res_vars]}, parse1(S1, [], State4); parse1([$%, $( | S], Acc, State) -> State1 = append_string(lists:reverse(Acc), State), {Name, Type, S1, State2} = parse_name(S, true, State1), Var = State2#state.param_pos, State4 = case Type of host -> State3 = State2#state{server_host_used = {true, Name}, used_vars = [Name | State2#state.used_vars]}, case State#state.use_new_schema of true -> Convert = erl_syntax:application( erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(string)), [erl_syntax:variable(Name)]), State3#state{'query' = [{var, Var}, {str, "server_host="} | State3#state.'query'], args = [Convert | State3#state.args], params = [Var | State3#state.params], param_pos = State3#state.param_pos + 1}; false -> append_string("0=0", State3) end; {list, InternalType} -> Convert = erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(to_list), [erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(InternalType)), erl_syntax:variable(Name)]), IT2 = case InternalType of string -> in_array_string; _ -> InternalType end, ConvertArr = erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(to_array), [erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(IT2)), erl_syntax:variable(Name)]), State2#state{'query' = [[{var, Var}] | State2#state.'query'], need_array_pass = true, args = [[Convert, ConvertArr] | State2#state.args], params = [Var | State2#state.params], param_pos = State2#state.param_pos + 1, used_vars = [Name | State2#state.used_vars]}; _ -> Convert = erl_syntax:application( erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(Type)), [erl_syntax:variable(Name)]), State2#state{'query' = [{var, Var} | State2#state.'query'], args = [Convert | State2#state.args], params = [Var | State2#state.params], param_pos = State2#state.param_pos + 1, used_vars = [Name | State2#state.used_vars]} end, parse1(S1, [], State4); parse1("%ESCAPE" ++ S, Acc, State) -> State1 = append_string(lists:reverse(Acc), State), Convert = erl_syntax:application( erl_syntax:record_access( erl_syntax:variable(?ESCAPE_VAR), erl_syntax:atom(?ESCAPE_RECORD), erl_syntax:atom(like_escape)), []), Var = State1#state.param_pos, State2 = State1#state{'query' = [{var, Var} | State1#state.'query'], args = [Convert | State1#state.args], params = [Var | State1#state.params], param_pos = State1#state.param_pos + 1}, parse1(S, [], State2); parse1([C | S], Acc, State) -> parse1(S, [C | Acc], State). append_string([], State) -> State; append_string(S, State) -> State#state{query = [{str, S} | State#state.query]}. parse_name(S, IsArg, State) -> parse_name(S, [], 0, IsArg, State). parse_name([], _Acc, _Depth, _IsArg, State) -> throw({error, State#state.loc, "expected ')', found end of string"}); parse_name([$), $l, T | S], Acc, 0, true, State) -> Type = case T of $d -> {list, integer}; $s -> {list, string}; $b -> {list, boolean}; _ -> throw({error, State#state.loc, ["unknown type specifier 'l", T, "'"]}) end, {lists:reverse(Acc), Type, S, State}; parse_name([$), $l, T | _], _Acc, 0, false, State) -> throw({error, State#state.loc, ["list type 'l", T, "' is not allowed for outputs"]}); parse_name([$), T | S], Acc, 0, IsArg, State) -> Type = case T of $d -> integer; $s -> string; $b -> boolean; $H when IsArg -> host; _ -> throw({error, State#state.loc, ["unknown type specifier '", T, "'"]}) end, {lists:reverse(Acc), Type, S, State}; parse_name([$)], _Acc, 0, _IsArg, State) -> throw({error, State#state.loc, "expected type specifier, found end of string"}); parse_name([$( = C | S], Acc, Depth, IsArg, State) -> parse_name(S, [C | Acc], Depth + 1, IsArg, State); parse_name([$) = C | S], Acc, Depth, IsArg, State) -> parse_name(S, [C | Acc], Depth - 1, IsArg, State); parse_name([C | S], Acc, Depth, IsArg, State) -> parse_name(S, [C | Acc], Depth, IsArg, State). make_var(V) -> Var = "__V" ++ integer_to_list(V), erl_syntax:variable(Var). perform_array_pass(State) -> {NQ, PQ, Rest} = lists:foldl( fun([{var, _} = Var], {N, P, {str, Str} = Prev}) -> Str2 = re:replace(Str, "(^|\s+)in\s*$", " = any(", [{return, list}]), {[Var, Prev | N], [{str, ")"}, Var, {str, Str2} | P], none}; ([{var, _}], _) -> throw({error, State#state.loc, ["List variable not following 'in' operator"]}); (Other, {N, P, none}) -> {N, P, Other}; (Other, {N, P, Prev}) -> {[Prev | N], [Prev | P], Other} end, {[], [], none}, State#state.query), {NQ2, PQ2} = case Rest of none -> {NQ, PQ}; _ -> {[Rest | NQ], [Rest | PQ]} end, {NA, PA} = lists:foldl( fun([V1, V2], {N, P}) -> {[V1 | N], [V2 | P]}; (Other, {N, P}) -> {[Other | N], [Other | P]} end, {[], []}, State#state.args), {State#state{query = lists:reverse(NQ2), args = lists:reverse(NA), need_array_pass = false}, State#state{query = lists:reverse(PQ2), args = lists:reverse(PA), need_array_pass = false}}. make_sql_query(State) -> Hash = erlang:phash2(State#state{loc = undefined, use_new_schema = true}), SHash = <<"Q", (integer_to_binary(Hash))/binary>>, Query = pack_query(State#state.'query'), EQuery = lists:map( fun({str, S}) -> erl_syntax:binary( [erl_syntax:binary_field( erl_syntax:string(S))]); ({var, V}) -> make_var(V) end, Query), erl_syntax:record_expr( erl_syntax:atom(?QUERY_RECORD), [erl_syntax:record_field( erl_syntax:atom(hash), %erl_syntax:abstract(SHash) erl_syntax:binary( [erl_syntax:binary_field( erl_syntax:string(binary_to_list(SHash)))])), erl_syntax:record_field( erl_syntax:atom(args), erl_syntax:fun_expr( [erl_syntax:clause( [erl_syntax:variable(?ESCAPE_VAR)], none, [erl_syntax:list(State#state.args)] )])), erl_syntax:record_field( erl_syntax:atom(format_query), erl_syntax:fun_expr( [erl_syntax:clause( [erl_syntax:list(lists:map(fun make_var/1, State#state.params))], none, [erl_syntax:list(EQuery)] )])), erl_syntax:record_field( erl_syntax:atom(format_res), erl_syntax:fun_expr( [erl_syntax:clause( [erl_syntax:list(State#state.res_vars)], none, [erl_syntax:tuple(State#state.res)] )])), erl_syntax:record_field( erl_syntax:atom(loc), erl_syntax:abstract({get(?MOD), State#state.loc})) ]). pack_query([]) -> []; pack_query([{str, S1}, {str, S2} | Rest]) -> pack_query([{str, S1 ++ S2} | Rest]); pack_query([X | Rest]) -> [X | pack_query(Rest)]. parse_upsert(Fields) -> {Fs, _} = lists:foldr( fun(F, {Acc, Param}) -> case erl_syntax:type(F) of string -> V = erl_syntax:string_value(F), {_, _, State} = Res = parse_upsert_field( V, Param, erl_syntax:get_pos(F)), {[Res | Acc], State#state.param_pos}; _ -> throw({error, erl_syntax:get_pos(F), "?SQL_UPSERT field must be " "a constant string"}) end end, {[], 0}, Fields), Fs. %% key | {Update} parse_upsert_field([$! | S], ParamPos, Loc) -> {Name, ParseState} = parse_upsert_field1(S, [], ParamPos, Loc), {Name, key, ParseState}; parse_upsert_field([$- | S], ParamPos, Loc) -> {Name, ParseState} = parse_upsert_field1(S, [], ParamPos, Loc), {Name, {false}, ParseState}; parse_upsert_field(S, ParamPos, Loc) -> {Name, ParseState} = parse_upsert_field1(S, [], ParamPos, Loc), {Name, {true}, ParseState}. parse_upsert_field1([], _Acc, _ParamPos, Loc) -> throw({error, Loc, "?SQL_UPSERT fields must have the " "following form: \"[!-]name=value\""}); parse_upsert_field1([$= | S], Acc, ParamPos, Loc) -> {lists:reverse(Acc), parse(S, ParamPos, Loc, true)}; parse_upsert_field1([C | S], Acc, ParamPos, Loc) -> parse_upsert_field1(S, [C | Acc], ParamPos, Loc). make_sql_upsert(Table, ParseRes, Pos) -> check_upsert(ParseRes, Pos), HasInsertOnlyFields = lists:any( fun({_, {false}, _}) -> true; (_) -> false end, ParseRes), MySqlReplace = case HasInsertOnlyFields of false -> [erl_syntax:clause( [erl_syntax:atom(mysql), erl_syntax:underscore()], [], [make_sql_upsert_mysql(Table, ParseRes), erl_syntax:atom(ok)])]; _ -> [] end, erl_syntax:fun_expr( [erl_syntax:clause( [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], [erl_syntax:infix_expr( erl_syntax:variable("__Version"), erl_syntax:operator('>='), erl_syntax:integer(90500))], [make_sql_upsert_pgsql905(Table, ParseRes), erl_syntax:atom(ok)]), erl_syntax:clause( [erl_syntax:atom(pgsql), erl_syntax:variable("__Version")], [erl_syntax:infix_expr( erl_syntax:variable("__Version"), erl_syntax:operator('>='), erl_syntax:integer(90100))], [make_sql_upsert_pgsql901(Table, ParseRes), erl_syntax:atom(ok)])] ++ MySqlReplace ++ [erl_syntax:clause( [erl_syntax:underscore(), erl_syntax:underscore()], none, [make_sql_upsert_generic(Table, ParseRes)]) ]). make_sql_upsert_generic(Table, ParseRes) -> Update = make_sql_query(make_sql_upsert_update(Table, ParseRes)), Insert = make_sql_query(make_sql_upsert_insert(Table, ParseRes)), InsertBranch = erl_syntax:case_expr( erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), [Insert]), [erl_syntax:clause( [erl_syntax:abstract({updated, 1})], none, [erl_syntax:atom(ok)]), erl_syntax:clause( [erl_syntax:variable("__UpdateRes")], none, [erl_syntax:variable("__UpdateRes")])]), erl_syntax:case_expr( erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), [Update]), [erl_syntax:clause( [erl_syntax:abstract({updated, 1})], none, [erl_syntax:atom(ok)]), erl_syntax:clause( [erl_syntax:underscore()], none, [InsertBranch])]). make_sql_upsert_update(Table, ParseRes) -> WPairs = lists:flatmap( fun({_Field, {_}, _ST}) -> []; ({Field, key, ST}) -> [ST#state{ 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' }] end, ParseRes), Where = join_states(WPairs, " AND "), SPairs = lists:flatmap( fun({_Field, key, _ST}) -> []; ({_Field, {false}, _ST}) -> []; ({Field, {true}, ST}) -> [ST#state{ 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' }] end, ParseRes), Set = join_states(SPairs, ", "), State = concat_states( [#state{'query' = [{str, "UPDATE "}, {str, Table}, {str, " SET "}]}, Set, #state{'query' = [{str, " WHERE "}]}, Where ]), State. make_sql_upsert_insert(Table, ParseRes) -> make_sql_upsert_insert_replace(Table, ParseRes, "INSERT"). make_sql_upsert_insert_replace(Table, ParseRes, Keyword) -> Vals = lists:map( fun({_Field, _, ST}) -> ST end, ParseRes), Fields = lists:map( fun({Field, _, _ST}) -> #state{'query' = [{str, Field}]} end, ParseRes), State = concat_states( [#state{'query' = [{str, Keyword ++" INTO "}, {str, Table}, {str, "("}]}, join_states(Fields, ", "), #state{'query' = [{str, ") VALUES ("}]}, join_states(Vals, ", "), #state{'query' = [{str, ");"}]} ]), State. make_sql_upsert_replace(Table, ParseRes) -> make_sql_upsert_insert_replace(Table, ParseRes, "REPLACE"). make_sql_upsert_mysql(Table, ParseRes) -> Replace = make_sql_query(make_sql_upsert_replace(Table, ParseRes)), erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), [Replace]). make_sql_upsert_pgsql901(Table, ParseRes0) -> ParseRes = lists:map( fun({"family", A2, A3}) -> {"\"family\"", A2, A3}; (Other) -> Other end, ParseRes0), Update = make_sql_upsert_update(Table, ParseRes), Vals = lists:map( fun({_Field, _, ST}) -> ST end, ParseRes), Fields = lists:map( fun({Field, _, _ST}) -> #state{'query' = [{str, Field}]} end, ParseRes), Insert = concat_states( [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, join_states(Fields, ", "), #state{'query' = [{str, ") SELECT "}]}, join_states(Vals, ", "), #state{'query' = [{str, " WHERE NOT EXISTS (SELECT * FROM upsert)"}]} ]), State = concat_states( [#state{'query' = [{str, "WITH upsert AS ("}]}, Update, #state{'query' = [{str, " RETURNING *) "}]}, Insert ]), Upsert = make_sql_query(State), erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), [Upsert]). make_sql_upsert_pgsql905(Table, ParseRes0) -> ParseRes = lists:map( fun({"family", A2, A3}) -> {"\"family\"", A2, A3}; (Other) -> Other end, ParseRes0), Vals = lists:map( fun({_Field, _, ST}) -> ST end, ParseRes), Fields = lists:map( fun({Field, _, _ST}) -> #state{'query' = [{str, Field}]} end, ParseRes), SPairs = lists:flatmap( fun({_Field, key, _ST}) -> []; ({_Field, {false}, _ST}) -> []; ({Field, {true}, ST}) -> [ST#state{ 'query' = [{str, Field}, {str, "="}] ++ ST#state.'query' }] end, ParseRes), Set = join_states(SPairs, ", "), KeyFields = lists:flatmap( fun({Field, key, _ST}) -> [#state{'query' = [{str, Field}]}]; ({_Field, _, _ST}) -> [] end, ParseRes), State = concat_states( [#state{'query' = [{str, "INSERT INTO "}, {str, Table}, {str, "("}]}, join_states(Fields, ", "), #state{'query' = [{str, ") VALUES ("}]}, join_states(Vals, ", "), #state{'query' = [{str, ") ON CONFLICT ("}]}, join_states(KeyFields, ", "), #state{'query' = [{str, ") DO UPDATE SET "}]}, Set ]), Upsert = make_sql_query(State), erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(sql_query_t), [Upsert]). check_upsert(ParseRes, Pos) -> Set = lists:filter( fun({_Field, Match, _ST}) -> Match /= key end, ParseRes), case Set of [] -> throw({error, Pos, "No ?SQL_UPSERT fields to set, use INSERT instead"}); _ -> ok end, ok. parse_insert(Fields) -> {Fs, _} = lists:foldr( fun(F, {Acc, Param}) -> case erl_syntax:type(F) of string -> V = erl_syntax:string_value(F), {_, _, State} = Res = parse_insert_field( V, Param, erl_syntax:get_pos(F)), {[Res | Acc], State#state.param_pos}; _ -> throw({error, erl_syntax:get_pos(F), "?SQL_INSERT field must be " "a constant string"}) end end, {[], 0}, Fields), Fs. parse_insert_field([$! | _S], _ParamPos, Loc) -> throw({error, Loc, "?SQL_INSERT fields must not start with \"!\""}); parse_insert_field([$- | _S], _ParamPos, Loc) -> throw({error, Loc, "?SQL_INSERT fields must not start with \"-\""}); parse_insert_field(S, ParamPos, Loc) -> {Name, ParseState} = parse_insert_field1(S, [], ParamPos, Loc), {Name, {true}, ParseState}. parse_insert_field1([], _Acc, _ParamPos, Loc) -> throw({error, Loc, "?SQL_INSERT fields must have the " "following form: \"name=value\""}); parse_insert_field1([$= | S], Acc, ParamPos, Loc) -> {lists:reverse(Acc), parse(S, ParamPos, Loc, true)}; parse_insert_field1([C | S], Acc, ParamPos, Loc) -> parse_insert_field1(S, [C | Acc], ParamPos, Loc). make_sql_insert(Table, ParseRes) -> make_sql_query(make_sql_upsert_insert(Table, ParseRes)). make_schema_check(Tree, Tree) -> Tree; make_schema_check(New, Old) -> erl_syntax:case_expr( erl_syntax:application( erl_syntax:atom(ejabberd_sql), erl_syntax:atom(use_new_schema), []), [erl_syntax:clause( [erl_syntax:abstract(true)], none, [New]), erl_syntax:clause( [erl_syntax:abstract(false)], none, [Old])]). concat_states(States) -> lists:foldr( fun(ST11, ST2) -> ST1 = resolve_vars(ST11, ST2), ST1#state{ 'query' = ST1#state.'query' ++ ST2#state.'query', params = ST1#state.params ++ ST2#state.params, args = ST1#state.args ++ ST2#state.args, res = ST1#state.res ++ ST2#state.res, res_vars = ST1#state.res_vars ++ ST2#state.res_vars, loc = case ST1#state.loc of undefined -> ST2#state.loc; _ -> ST1#state.loc end } end, #state{}, States). resolve_vars(ST1, ST2) -> Max = lists:max([0 | ST1#state.params ++ ST2#state.params]), {Map, _} = lists:foldl( fun(Var, {Acc, New}) -> case lists:member(Var, ST2#state.params) of true -> {dict:store(Var, New, Acc), New + 1}; false -> {Acc, New} end end, {dict:new(), Max + 1}, ST1#state.params), NewParams = lists:map( fun(Var) -> case dict:find(Var, Map) of {ok, New} -> New; error -> Var end end, ST1#state.params), NewQuery = lists:map( fun({var, Var}) -> case dict:find(Var, Map) of {ok, New} -> {var, New}; error -> {var, Var} end; (S) -> S end, ST1#state.'query'), ST1#state{params = NewParams, 'query' = NewQuery}. join_states([], _Sep) -> #state{}; join_states([H | T], Sep) -> J = [[H] | [[#state{'query' = [{str, Sep}]}, X] || X <- T]], concat_states(lists:append(J)). set_pos(Tree, Pos) -> erl_syntax_lib:map( fun(Node) -> case erl_syntax:get_pos(Node) of 0 -> erl_syntax:set_pos(Node, Pos); _ -> Node end end, Tree). filter_upsert_sh(Table, ParseRes) -> lists:filter( fun({Field, _Match, _ST}) -> Field /= "server_host" orelse Table == "route" end, ParseRes). -ifdef(ENABLE_PT_WARNINGS). add_warning(Pos, Warning) -> Marker = erl_syntax:revert( erl_syntax:warning_marker({Pos, ?MODULE, Warning})), put(warnings, [Marker | get(warnings)]), ok. -else. add_warning(_Pos, _Warning) -> ok. -endif. ejabberd-21.12/src/ejd2sql.erl0000644000232200023220000003067214154362354016501 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejd2sql.erl %%% Author : Alexey Shchepin %%% Purpose : Export some mnesia tables to SQL DB %%% Created : 22 Aug 2005 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(ejd2sql). -author('alexey@process-one.net'). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). -export([export/2, export/3, import/3, import/4, delete/1, import_info/1]). -define(MAX_RECORDS_PER_TRANSACTION, 100). -record(sql_dump, {fd, type}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%% How to use: %%% A table can be converted from Mnesia to an ODBC database by calling %%% one of the API function with the following parameters: %%% - Server is the server domain you want to convert %%% - Output can be either sql to export to the configured relational %%% database or "Filename" to export to text file. modules() -> [ejabberd_auth, mod_announce, mod_caps, mod_last, mod_mam, mod_muc, mod_offline, mod_privacy, mod_private, mod_pubsub, mod_push, mod_roster, mod_shared_roster, mod_vcard]. export(Server, Output) -> LServer = jid:nameprep(iolist_to_binary(Server)), Modules = modules(), IO = prepare_output(Output), lists:foreach( fun(Module) -> export(LServer, IO, Module) end, Modules), close_output(Output, IO). export(Server, Output, mod_mam = M1) -> MucServices = gen_mod:get_module_opt_hosts(Server, mod_muc), [export2(MucService, Output, M1, M1) || MucService <- MucServices], export2(Server, Output, M1, M1); export(Server, Output, mod_pubsub = M1) -> export2(Server, Output, M1, pubsub_db); export(Server, Output, M1) -> export2(Server, Output, M1, M1). export2(Server, Output, Module1, Module) -> SQLMod = gen_mod:db_mod(sql, Module), LServer = jid:nameprep(iolist_to_binary(Server)), IO = prepare_output(Output), lists:foreach( fun({Table, ConvertFun}) -> case export(LServer, Table, IO, ConvertFun) of {atomic, ok} -> ok; {aborted, {no_exists, _}} -> ?WARNING_MSG("Ignoring export for module ~ts: " "Mnesia table ~ts doesn't exist (most likely " "because the module is unused)", [Module1, Table]); {aborted, Reason} -> ?ERROR_MSG("Failed export for module ~p and table ~p: ~p", [Module, Table, Reason]) end end, SQLMod:export(Server)), close_output(Output, IO). delete(Server) -> Modules = modules(), lists:foreach( fun(Module) -> delete(Server, Module) end, Modules). delete(Server, Module1) -> LServer = jid:nameprep(iolist_to_binary(Server)), Module = case Module1 of mod_pubsub -> pubsub_db; _ -> Module1 end, SQLMod = gen_mod:db_mod(sql, Module), lists:foreach( fun({Table, ConvertFun}) -> delete(LServer, Table, ConvertFun) end, SQLMod:export(Server)). import(Server, Dir, ToType) -> lists:foreach( fun(Mod) -> ?INFO_MSG("Importing ~p...", [Mod]), import(Mod, Server, Dir, ToType) end, modules()). import(Mod, Server, Dir, ToType) -> LServer = jid:nameprep(iolist_to_binary(Server)), try Mod:import_start(LServer, ToType) catch error:undef -> ok end, lists:foreach( fun({File, Tab, _Mod, FieldsNumber}) -> FileName = filename:join([Dir, File]), case open_sql_dump(FileName) of {ok, #sql_dump{type = FromType} = Dump} -> import_rows(LServer, {sql, FromType}, ToType, Tab, Mod, Dump, FieldsNumber), close_sql_dump(Dump); {error, enoent} -> ok; eof -> ?INFO_MSG("It seems like SQL dump ~ts is empty", [FileName]); Err -> ?ERROR_MSG("Failed to open SQL dump ~ts: ~ts", [FileName, format_error(Err)]) end end, import_info(Mod)), try Mod:import_stop(LServer, ToType) catch error:undef -> ok end. import_info(Mod) -> Info = Mod:import_info(), lists:map( fun({Tab, FieldsNum}) -> FileName = <>, {FileName, Tab, Mod, FieldsNum} end, Info). %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- export(LServer, Table, IO, ConvertFun) -> DbType = ejabberd_option:sql_type(LServer), F = fun () -> mnesia:read_lock_table(Table), {_N, SQLs} = mnesia:foldl( fun(R, {N, SQLs} = Acc) -> case ConvertFun(LServer, R) of [] -> Acc; SQL1 -> SQL = format_queries(DbType, SQL1), if N < (?MAX_RECORDS_PER_TRANSACTION) - 1 -> {N + 1, [SQL | SQLs]}; true -> output(LServer, Table, IO, flatten([SQL | SQLs])), {0, []} end end end, {0, []}, Table), output(LServer, Table, IO, flatten(SQLs)) end, mnesia:transaction(F). output(_LServer, _Table, _IO, []) -> ok; output(LServer, _Table, sql, SQLs) -> {atomic, ok} = ejabberd_sql:sql_transaction(LServer, SQLs), ok; output(_LServer, Table, Fd, SQLs) -> file:write(Fd, ["-- \n-- Mnesia table: ", atom_to_list(Table), "\n--\n", SQLs]). delete(LServer, Table, ConvertFun) -> F = fun () -> mnesia:write_lock_table(Table), {_N, _SQLs} = mnesia:foldl( fun(R, Acc) -> case ConvertFun(LServer, R) of [] -> Acc; _SQL -> mnesia:delete_object(R), Acc end end, {0, []}, Table) end, mnesia:transaction(F). prepare_output(FileName) -> prepare_output(FileName, normal). prepare_output(FileName, Type) when is_binary(FileName) -> prepare_output(binary_to_list(FileName), Type); prepare_output(FileName, normal) when is_list(FileName) -> case file:open(FileName, [write, raw]) of {ok, Fd} -> Fd; {error, eacces} -> exit({"Not enough permission to the file or path", FileName}); {error, enoent} -> exit({"Path does not exist", FileName}); Err -> exit(Err) end; prepare_output(Output, _Type) -> Output. close_output(FileName, Fd) when FileName /= Fd -> file:close(Fd), ok; close_output(_, _) -> ok. flatten(SQLs) -> flatten(SQLs, []). flatten([L|Ls], Acc) -> flatten(Ls, flatten1(lists:reverse(L), Acc)); flatten([], Acc) -> Acc. flatten1([H|T], Acc) -> flatten1(T, [[H, $\n]|Acc]); flatten1([], Acc) -> Acc. import_rows(LServer, FromType, ToType, Tab, Mod, Dump, FieldsNumber) -> case read_row_from_sql_dump(Dump, FieldsNumber) of {ok, Fields} -> case catch Mod:import(LServer, FromType, ToType, Tab, Fields) of ok -> ok; Err -> ?ERROR_MSG("Failed to import fields ~p for tab ~p: ~p", [Fields, Tab, Err]) end, import_rows(LServer, FromType, ToType, Tab, Mod, Dump, FieldsNumber); eof -> ok; Err -> ?ERROR_MSG("Failed to read row from SQL dump: ~ts", [format_error(Err)]) end. open_sql_dump(FileName) -> case file:open(FileName, [raw, read, binary, read_ahead]) of {ok, Fd} -> case file:read(Fd, 11) of {ok, <<"PGCOPY\n", 16#ff, "\r\n", 0>>} -> case skip_pgcopy_header(Fd) of ok -> {ok, #sql_dump{fd = Fd, type = pgsql}}; Err -> Err end; {ok, _} -> file:position(Fd, 0), {ok, #sql_dump{fd = Fd, type = mysql}}; Err -> Err end; Err -> Err end. close_sql_dump(#sql_dump{fd = Fd}) -> file:close(Fd). read_row_from_sql_dump(#sql_dump{fd = Fd, type = pgsql}, _) -> case file:read(Fd, 2) of {ok, <<(-1):16/signed>>} -> eof; {ok, <>} -> read_fields(Fd, FieldsNum, []); {ok, _} -> {error, eof}; eof -> {error, eof}; {error, _} = Err -> Err end; read_row_from_sql_dump(#sql_dump{fd = Fd, type = mysql}, FieldsNum) -> read_lines(Fd, FieldsNum, <<"">>, []). skip_pgcopy_header(Fd) -> try {ok, <<_:4/binary, ExtSize:32>>} = file:read(Fd, 8), {ok, <<_:ExtSize/binary>>} = file:read(Fd, ExtSize), ok catch error:{badmatch, {error, _} = Err} -> Err; error:{badmatch, _} -> {error, eof} end. read_fields(_Fd, 0, Acc) -> {ok, lists:reverse(Acc)}; read_fields(Fd, N, Acc) -> case file:read(Fd, 4) of {ok, <<(-1):32/signed>>} -> read_fields(Fd, N-1, [null|Acc]); {ok, <>} -> case file:read(Fd, ValSize) of {ok, <>} -> read_fields(Fd, N-1, [Val|Acc]); {ok, _} -> {error, eof}; Err -> Err end; {ok, _} -> {error, eof}; eof -> {error, eof}; {error, _} = Err -> Err end. read_lines(_Fd, 0, <<"">>, Acc) -> {ok, lists:reverse(Acc)}; read_lines(Fd, N, Buf, Acc) -> case file:read_line(Fd) of {ok, Data} when size(Data) >= 2 -> Size = size(Data) - 2, case Data of <> -> NewBuf = <>, read_lines(Fd, N-1, <<"">>, [NewBuf|Acc]); _ -> NewBuf = <>, read_lines(Fd, N, NewBuf, Acc) end; {ok, Data} -> NewBuf = <>, read_lines(Fd, N, NewBuf, Acc); eof when Buf == <<"">>, Acc == [] -> eof; eof -> {error, eof}; {error, _} = Err -> Err end. format_error({error, eof}) -> "unexpected end of file"; format_error({error, Posix}) -> file:format_error(Posix). format_queries(DbType, SQLs) -> lists:map( fun(#sql_query{} = SQL) -> ejabberd_sql:sql_query_to_iolist(DbType, SQL); (SQL) -> SQL end, SQLs). ejabberd-21.12/src/mod_proxy65_opt.erl0000644000232200023220000000712114154362354020203 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_proxy65_opt). -export([access/1]). -export([auth_type/1]). -export([host/1]). -export([hostname/1]). -export([hosts/1]). -export([ip/1]). -export([max_connections/1]). -export([name/1]). -export([port/1]). -export([ram_db_type/1]). -export([recbuf/1]). -export([server_host/1]). -export([shaper/1]). -export([sndbuf/1]). -export([vcard/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, access). -spec auth_type(gen_mod:opts() | global | binary()) -> 'anonymous' | 'plain'. auth_type(Opts) when is_map(Opts) -> gen_mod:get_opt(auth_type, Opts); auth_type(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, auth_type). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, host). -spec hostname(gen_mod:opts() | global | binary()) -> 'undefined' | binary(). hostname(Opts) when is_map(Opts) -> gen_mod:get_opt(hostname, Opts); hostname(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, hostname). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, hosts). -spec ip(gen_mod:opts() | global | binary()) -> 'undefined' | inet:ip_address(). ip(Opts) when is_map(Opts) -> gen_mod:get_opt(ip, Opts); ip(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, ip). -spec max_connections(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_connections(Opts) when is_map(Opts) -> gen_mod:get_opt(max_connections, Opts); max_connections(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, max_connections). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, name). -spec port(gen_mod:opts() | global | binary()) -> 1..1114111. port(Opts) when is_map(Opts) -> gen_mod:get_opt(port, Opts); port(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, port). -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, ram_db_type). -spec recbuf(gen_mod:opts() | global | binary()) -> pos_integer(). recbuf(Opts) when is_map(Opts) -> gen_mod:get_opt(recbuf, Opts); recbuf(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, recbuf). -spec server_host(gen_mod:opts() | global | binary()) -> binary(). server_host(Opts) when is_map(Opts) -> gen_mod:get_opt(server_host, Opts); server_host(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, server_host). -spec shaper(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(shaper, Opts); shaper(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, shaper). -spec sndbuf(gen_mod:opts() | global | binary()) -> pos_integer(). sndbuf(Opts) when is_map(Opts) -> gen_mod:get_opt(sndbuf, Opts); sndbuf(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, sndbuf). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_proxy65, vcard). ejabberd-21.12/src/mod_roster.erl0000644000232200023220000013605314154362354017312 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_roster.erl %%% Author : Alexey Shchepin %%% Purpose : Roster management %%% Created : 11 Dec 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% @doc Roster management (Mnesia storage). %%% %%% Includes support for XEP-0237: Roster Versioning. %%% The roster versioning follows an all-or-nothing strategy: %%% - If the version supplied by the client is the latest, return an empty response. %%% - If not, return the entire new roster (with updated version string). %%% Roster version is a hash digest of the entire roster. %%% No additional data is stored in DB. -module(mod_roster). -protocol({xep, 237, '1.3'}). -author('alexey@process-one.net'). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_iq/1, export/1, import_info/0, process_local_iq/1, get_user_roster/2, import/5, get_roster/2, push_item/3, import_start/2, import_stop/2, is_subscribed/2, c2s_self_presence/1, in_subscription/2, out_subscription/1, set_items/3, remove_user/2, get_jid_info/4, encode_item/1, webadmin_page/3, webadmin_user/4, get_versioning_feature/2, roster_version/2, mod_doc/0, mod_opt_type/1, mod_options/1, set_roster/1, del_roster/3, process_rosteritems/5, depends/2, set_item_and_notify_clients/3]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_roster.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). -define(ROSTER_CACHE, roster_cache). -define(ROSTER_ITEM_CACHE, roster_item_cache). -define(ROSTER_VERSION_CACHE, roster_version_cache). -type c2s_state() :: ejabberd_c2s:state(). -export_type([subscription/0]). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), binary(), #roster{} | [binary()]) -> ok. -callback read_roster_version(binary(), binary()) -> {ok, binary()} | error. -callback write_roster_version(binary(), binary(), boolean(), binary()) -> any(). -callback get_roster(binary(), binary()) -> {ok, [#roster{}]} | error. -callback get_roster_item(binary(), binary(), ljid()) -> {ok, #roster{}} | error. -callback read_subscription_and_groups(binary(), binary(), ljid()) -> {ok, {subscription(), ask(), [binary()]}} | error. -callback roster_subscribe(binary(), binary(), ljid(), #roster{}) -> any(). -callback transaction(binary(), fun(() -> T)) -> {atomic, T} | {aborted, any()}. -callback remove_user(binary(), binary()) -> any(). -callback update_roster(binary(), binary(), ljid(), #roster{}) -> any(). -callback del_roster(binary(), binary(), ljid()) -> any(). -callback use_cache(binary(), roster | roster_version) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/2, cache_nodes/1]). start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), ejabberd_hooks:add(roster_get, Host, ?MODULE, get_user_roster, 50), ejabberd_hooks:add(roster_in_subscription, Host, ?MODULE, in_subscription, 50), ejabberd_hooks:add(roster_out_subscription, Host, ?MODULE, out_subscription, 50), ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, get_jid_info, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:add(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, get_versioning_feature, 50), ejabberd_hooks:add(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:add(webadmin_user, Host, ?MODULE, webadmin_user, 50), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_ROSTER, ?MODULE, process_iq). stop(Host) -> ejabberd_hooks:delete(roster_get, Host, ?MODULE, get_user_roster, 50), ejabberd_hooks:delete(roster_in_subscription, Host, ?MODULE, in_subscription, 50), ejabberd_hooks:delete(roster_out_subscription, Host, ?MODULE, out_subscription, 50), ejabberd_hooks:delete(roster_get_jid_info, Host, ?MODULE, get_jid_info, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:delete(c2s_self_presence, Host, ?MODULE, c2s_self_presence, 50), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, get_versioning_feature, 50), ejabberd_hooks:delete(webadmin_page_host, Host, ?MODULE, webadmin_page, 50), ejabberd_hooks:delete(webadmin_user, Host, ?MODULE, webadmin_user, 50), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_ROSTER). reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). depends(_Host, _Opts) -> []. -spec process_iq(iq()) -> iq(). process_iq(#iq{from = #jid{luser = U, lserver = S}, to = #jid{luser = U, lserver = S}} = IQ) -> process_local_iq(IQ); process_iq(#iq{lang = Lang, to = To} = IQ) -> case ejabberd_hooks:run_fold(roster_remote_access, To#jid.lserver, false, [IQ]) of false -> Txt = ?T("Query to another users is forbidden"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)); true -> process_local_iq(IQ) end. -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set,lang = Lang, sub_els = [#roster_query{ items = [#roster_item{ask = Ask}]}]} = IQ) when Ask /= undefined -> Txt = ?T("Possessing 'ask' attribute is not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); process_local_iq(#iq{type = set, from = From, lang = Lang, sub_els = [#roster_query{ items = [#roster_item{} = Item]}]} = IQ) -> case has_duplicated_groups(Item#roster_item.groups) of true -> Txt = ?T("Duplicated groups are not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); false -> #jid{lserver = LServer} = From, Access = mod_roster_opt:access(LServer), case acl:match_rule(LServer, Access, From) of deny -> Txt = ?T("Access denied by service policy"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); allow -> process_iq_set(IQ) end end; process_local_iq(#iq{type = set, lang = Lang, sub_els = [#roster_query{items = [_|_]}]} = IQ) -> Txt = ?T("Multiple elements are not allowed by RFC6121"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)); process_local_iq(#iq{type = get, lang = Lang, sub_els = [#roster_query{items = Items}]} = IQ) -> case Items of [] -> process_iq_get(IQ); [_|_] -> Txt = ?T("The query must not contain elements"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) end; process_local_iq(#iq{lang = Lang} = IQ) -> Txt = ?T("No module is handling this query"), xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)). -spec roster_hash([#roster{}]) -> binary(). roster_hash(Items) -> str:sha(term_to_binary(lists:sort([R#roster{groups = lists:sort(Grs)} || R = #roster{groups = Grs} <- Items]))). %% Returns a list that may contain an xmlelement with the XEP-237 feature if it's enabled. -spec get_versioning_feature([xmpp_element()], binary()) -> [xmpp_element()]. get_versioning_feature(Acc, Host) -> case gen_mod:is_loaded(Host, ?MODULE) of true -> case mod_roster_opt:versioning(Host) of true -> [#rosterver_feature{}|Acc]; false -> Acc end; false -> Acc end. -spec roster_version(binary(), binary()) -> undefined | binary(). roster_version(LServer, LUser) -> US = {LUser, LServer}, case mod_roster_opt:store_current_id(LServer) of true -> case read_roster_version(LUser, LServer) of error -> undefined; {ok, V} -> V end; false -> roster_hash(ejabberd_hooks:run_fold(roster_get, LServer, [], [US])) end. -spec read_roster_version(binary(), binary()) -> {ok, binary()} | error. read_roster_version(LUser, LServer) -> ets_cache:lookup( ?ROSTER_VERSION_CACHE, {LUser, LServer}, fun() -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:read_roster_version(LUser, LServer) end). -spec write_roster_version(binary(), binary()) -> binary(). write_roster_version(LUser, LServer) -> write_roster_version(LUser, LServer, false). -spec write_roster_version_t(binary(), binary()) -> binary(). write_roster_version_t(LUser, LServer) -> write_roster_version(LUser, LServer, true). -spec write_roster_version(binary(), binary(), boolean()) -> binary(). write_roster_version(LUser, LServer, InTransaction) -> Ver = str:sha(term_to_binary(erlang:unique_integer())), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:write_roster_version(LUser, LServer, InTransaction, Ver), if InTransaction -> ok; true -> ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)) end, Ver. %% Load roster from DB only if necessary. %% It is necessary if %% - roster versioning is disabled in server OR %% - roster versioning is not used by the client OR %% - roster versioning is used by server and client, BUT the server isn't storing versions on db OR %% - the roster version from client don't match current version. -spec process_iq_get(iq()) -> iq(). process_iq_get(#iq{to = To, sub_els = [#roster_query{ver = RequestedVersion}]} = IQ) -> LUser = To#jid.luser, LServer = To#jid.lserver, US = {LUser, LServer}, {ItemsToSend, VersionToSend} = case {mod_roster_opt:versioning(LServer), mod_roster_opt:store_current_id(LServer)} of {true, true} when RequestedVersion /= undefined -> case read_roster_version(LUser, LServer) of error -> RosterVersion = write_roster_version(LUser, LServer), {lists:map(fun encode_item/1, ejabberd_hooks:run_fold( roster_get, To#jid.lserver, [], [US])), RosterVersion}; {ok, RequestedVersion} -> {false, false}; {ok, NewVersion} -> {lists:map(fun encode_item/1, ejabberd_hooks:run_fold( roster_get, To#jid.lserver, [], [US])), NewVersion} end; {true, false} when RequestedVersion /= undefined -> RosterItems = ejabberd_hooks:run_fold( roster_get, To#jid.lserver, [], [US]), case roster_hash(RosterItems) of RequestedVersion -> {false, false}; New -> {lists:map(fun encode_item/1, RosterItems), New} end; _ -> {lists:map(fun encode_item/1, ejabberd_hooks:run_fold( roster_get, To#jid.lserver, [], [US])), false} end, xmpp:make_iq_result( IQ, case {ItemsToSend, VersionToSend} of {false, false} -> undefined; {Items, false} -> #roster_query{items = Items}; {Items, Version} -> #roster_query{items = Items, ver = Version} end). -spec get_user_roster([#roster{}], {binary(), binary()}) -> [#roster{}]. get_user_roster(Acc, {LUser, LServer}) -> Items = get_roster(LUser, LServer), lists:filter(fun (#roster{subscription = none, ask = in}) -> false; (_) -> true end, Items) ++ Acc. -spec get_roster(binary(), binary()) -> [#roster{}]. get_roster(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), R = case use_cache(Mod, LServer, roster) of true -> ets_cache:lookup( ?ROSTER_CACHE, {LUser, LServer}, fun() -> Mod:get_roster(LUser, LServer) end); false -> Mod:get_roster(LUser, LServer) end, case R of {ok, Items} -> Items; error -> [] end. -spec get_roster_item(binary(), binary(), ljid()) -> #roster{}. get_roster_item(LUser, LServer, LJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:get_roster_item(LUser, LServer, LJID) of {ok, Item} -> Item; error -> LBJID = jid:remove_resource(LJID), #roster{usj = {LUser, LServer, LBJID}, us = {LUser, LServer}, jid = LBJID} end. -spec get_subscription_and_groups(binary(), binary(), ljid()) -> {subscription(), ask(), [binary()]}. get_subscription_and_groups(LUser, LServer, LJID) -> LBJID = jid:remove_resource(LJID), Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer, roster) of true -> ets_cache:lookup( ?ROSTER_ITEM_CACHE, {LUser, LServer, LBJID}, fun() -> Items = get_roster(LUser, LServer), case lists:keyfind(LBJID, #roster.jid, Items) of #roster{subscription = Sub, ask = Ask, groups = Groups} -> {ok, {Sub, Ask, Groups}}; false -> error end end); false -> case Mod:read_subscription_and_groups(LUser, LServer, LBJID) of {ok, {Sub, Groups}} -> %% Backward compatibility for third-party backends {ok, {Sub, none, Groups}}; Other -> Other end end, case Res of {ok, SubAndGroups} -> SubAndGroups; error -> {none, none, []} end. -spec set_roster(#roster{}) -> {atomic | aborted, any()}. set_roster(#roster{us = {LUser, LServer}, jid = LJID} = Item) -> transaction( LUser, LServer, [LJID], fun() -> update_roster_t(LUser, LServer, LJID, Item) end). -spec del_roster(binary(), binary(), ljid()) -> {atomic | aborted, any()}. del_roster(LUser, LServer, LJID) -> transaction( LUser, LServer, [LJID], fun() -> del_roster_t(LUser, LServer, LJID) end). -spec encode_item(#roster{}) -> roster_item(). encode_item(Item) -> #roster_item{jid = jid:make(Item#roster.jid), name = Item#roster.name, subscription = Item#roster.subscription, ask = case ask_to_pending(Item#roster.ask) of out -> subscribe; both -> subscribe; _ -> undefined end, groups = Item#roster.groups}. -spec decode_item(roster_item(), #roster{}, boolean()) -> #roster{}. decode_item(#roster_item{subscription = remove} = Item, R, _) -> R#roster{jid = jid:tolower(Item#roster_item.jid), name = <<"">>, subscription = remove, ask = none, groups = [], askmessage = <<"">>, xs = []}; decode_item(Item, R, Managed) -> R#roster{jid = jid:tolower(Item#roster_item.jid), name = Item#roster_item.name, subscription = case Item#roster_item.subscription of Sub when Managed -> Sub; _ -> R#roster.subscription end, groups = Item#roster_item.groups}. -spec process_iq_set(iq()) -> iq(). process_iq_set(#iq{from = _From, to = To, lang = Lang, sub_els = [#roster_query{items = [QueryItem]}]} = IQ) -> case set_item_and_notify_clients(To, QueryItem, false) of ok -> xmpp:make_iq_result(IQ); {error, _} -> Txt = ?T("Database failure"), Err = xmpp:err_internal_server_error(Txt, Lang), xmpp:make_error(IQ, Err) end. -spec set_item_and_notify_clients(jid(), #roster_item{}, boolean()) -> ok | {error, any()}. set_item_and_notify_clients(To, #roster_item{jid = PeerJID} = RosterItem, OverrideSubscription) -> #jid{luser = LUser, lserver = LServer} = To, PeerLJID = jid:tolower(PeerJID), F = fun () -> Item1 = get_roster_item(LUser, LServer, PeerLJID), Item2 = decode_item(RosterItem, Item1, OverrideSubscription), Item3 = ejabberd_hooks:run_fold(roster_process_item, LServer, Item2, [LServer]), case Item3#roster.subscription of remove -> del_roster_t(LUser, LServer, PeerLJID); _ -> update_roster_t(LUser, LServer, PeerLJID, Item3) end, case mod_roster_opt:store_current_id(LServer) of true -> write_roster_version_t(LUser, LServer); false -> ok end, {Item1, Item3} end, case transaction(LUser, LServer, [PeerLJID], F) of {atomic, {OldItem, NewItem}} -> push_item(To, OldItem, NewItem), case NewItem#roster.subscription of remove -> send_unsubscribing_presence(To, OldItem); _ -> ok end; {aborted, Reason} -> {error, Reason} end. -spec push_item(jid(), #roster{}, #roster{}) -> ok. push_item(To, OldItem, NewItem) -> #jid{luser = LUser, lserver = LServer} = To, Ver = case mod_roster_opt:versioning(LServer) of true -> roster_version(LServer, LUser); false -> undefined end, lists:foreach( fun(Resource) -> To1 = jid:replace_resource(To, Resource), push_item(To1, OldItem, NewItem, Ver) end, ejabberd_sm:get_user_resources(LUser, LServer)). -spec push_item(jid(), #roster{}, #roster{}, undefined | binary()) -> ok. push_item(To, OldItem, NewItem, Ver) -> route_presence_change(To, OldItem, NewItem), IQ = #iq{type = set, to = To, from = jid:remove_resource(To), id = <<"push", (p1_rand:get_string())/binary>>, sub_els = [#roster_query{ver = Ver, items = [encode_item(NewItem)]}]}, ejabberd_router:route(IQ). -spec route_presence_change(jid(), #roster{}, #roster{}) -> ok. route_presence_change(From, OldItem, NewItem) -> OldSub = OldItem#roster.subscription, NewSub = NewItem#roster.subscription, To = jid:make(NewItem#roster.jid), NewIsFrom = NewSub == both orelse NewSub == from, OldIsFrom = OldSub == both orelse OldSub == from, if NewIsFrom andalso not OldIsFrom -> case ejabberd_sm:get_session_pid( From#jid.luser, From#jid.lserver, From#jid.lresource) of none -> ok; Pid -> ejabberd_c2s:resend_presence(Pid, To) end; OldIsFrom andalso not NewIsFrom -> PU = #presence{from = From, to = To, type = unavailable}, case ejabberd_hooks:run_fold( privacy_check_packet, allow, [From, PU, out]) of deny -> ok; allow -> ejabberd_router:route(PU) end; true -> ok end. -spec ask_to_pending(ask()) -> none | in | out | both. ask_to_pending(subscribe) -> out; ask_to_pending(unsubscribe) -> none; ask_to_pending(Ask) -> Ask. -spec roster_subscribe_t(binary(), binary(), ljid(), #roster{}) -> any(). roster_subscribe_t(LUser, LServer, LJID, Item) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:roster_subscribe(LUser, LServer, LJID, Item). -spec transaction(binary(), binary(), [ljid()], fun(() -> T)) -> {atomic, T} | {aborted, any()}. transaction(LUser, LServer, LJIDs, F) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:transaction(LServer, F) of {atomic, _} = Result -> delete_cache(Mod, LUser, LServer, LJIDs), Result; Err -> Err end. -spec in_subscription(boolean(), presence()) -> boolean(). in_subscription(_, #presence{from = JID, to = To, type = Type, status = Status}) -> #jid{user = User, server = Server} = To, Reason = if Type == subscribe -> xmpp:get_text(Status); true -> <<"">> end, process_subscription(in, User, Server, JID, Type, Reason). -spec out_subscription(presence()) -> boolean(). out_subscription(#presence{from = From, to = JID, type = Type}) -> #jid{user = User, server = Server} = From, process_subscription(out, User, Server, JID, Type, <<"">>). -spec process_subscription(in | out, binary(), binary(), jid(), subscribe | subscribed | unsubscribe | unsubscribed, binary()) -> boolean(). process_subscription(Direction, User, Server, JID1, Type, Reason) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJID = jid:tolower(jid:remove_resource(JID1)), F = fun () -> Item = get_roster_item(LUser, LServer, LJID), NewState = case Direction of out -> out_state_change(Item#roster.subscription, Item#roster.ask, Type); in -> in_state_change(Item#roster.subscription, Item#roster.ask, Type) end, AutoReply = case Direction of out -> none; in -> in_auto_reply(Item#roster.subscription, Item#roster.ask, Type) end, AskMessage = case NewState of {_, both} -> Reason; {_, in} -> Reason; _ -> <<"">> end, case NewState of none -> {none, AutoReply}; {none, none} when Item#roster.subscription == none, Item#roster.ask == in -> del_roster_t(LUser, LServer, LJID), {none, AutoReply}; {Subscription, Pending} -> NewItem = Item#roster{subscription = Subscription, ask = Pending, askmessage = AskMessage}, roster_subscribe_t(LUser, LServer, LJID, NewItem), case mod_roster_opt:store_current_id(LServer) of true -> write_roster_version_t(LUser, LServer); false -> ok end, {{push, Item, NewItem}, AutoReply} end end, case transaction(LUser, LServer, [LJID], F) of {atomic, {Push, AutoReply}} -> case AutoReply of none -> ok; _ -> ejabberd_router:route( #presence{type = AutoReply, from = jid:make(User, Server), to = JID1}) end, case Push of {push, OldItem, NewItem} -> if NewItem#roster.subscription == none, NewItem#roster.ask == in -> ok; true -> push_item(jid:make(User, Server), OldItem, NewItem) end, true; none -> false end; _ -> false end. %% in_state_change(Subscription, Pending, Type) -> NewState %% NewState = none | {NewSubscription, NewPending} -ifdef(ROSTER_GATEWAY_WORKAROUND). -define(NNSD, {to, none}). -define(NISD, {to, in}). -else. -define(NNSD, none). -define(NISD, none). -endif. in_state_change(none, none, subscribe) -> {none, in}; in_state_change(none, none, subscribed) -> ?NNSD; in_state_change(none, none, unsubscribe) -> none; in_state_change(none, none, unsubscribed) -> none; in_state_change(none, out, subscribe) -> {none, both}; in_state_change(none, out, subscribed) -> {to, none}; in_state_change(none, out, unsubscribe) -> none; in_state_change(none, out, unsubscribed) -> {none, none}; in_state_change(none, in, subscribe) -> none; in_state_change(none, in, subscribed) -> ?NISD; in_state_change(none, in, unsubscribe) -> {none, none}; in_state_change(none, in, unsubscribed) -> none; in_state_change(none, both, subscribe) -> none; in_state_change(none, both, subscribed) -> {to, in}; in_state_change(none, both, unsubscribe) -> {none, out}; in_state_change(none, both, unsubscribed) -> {none, in}; in_state_change(to, none, subscribe) -> {to, in}; in_state_change(to, none, subscribed) -> none; in_state_change(to, none, unsubscribe) -> none; in_state_change(to, none, unsubscribed) -> {none, none}; in_state_change(to, in, subscribe) -> none; in_state_change(to, in, subscribed) -> none; in_state_change(to, in, unsubscribe) -> {to, none}; in_state_change(to, in, unsubscribed) -> {none, in}; in_state_change(from, none, subscribe) -> none; in_state_change(from, none, subscribed) -> {both, none}; in_state_change(from, none, unsubscribe) -> {none, none}; in_state_change(from, none, unsubscribed) -> none; in_state_change(from, out, subscribe) -> none; in_state_change(from, out, subscribed) -> {both, none}; in_state_change(from, out, unsubscribe) -> {none, out}; in_state_change(from, out, unsubscribed) -> {from, none}; in_state_change(both, none, subscribe) -> none; in_state_change(both, none, subscribed) -> none; in_state_change(both, none, unsubscribe) -> {to, none}; in_state_change(both, none, unsubscribed) -> {from, none}; % Invalid states that can occurs from roster modification from API in_state_change(to, out, subscribe) -> {to, in}; in_state_change(to, out, subscribed) -> none; in_state_change(to, out, unsubscribe) -> none; in_state_change(to, out, unsubscribed) -> {none, none}; in_state_change(to, both, subscribe) -> none; in_state_change(to, both, subscribed) -> none; in_state_change(to, both, unsubscribe) -> {to, none}; in_state_change(to, both, unsubscribed) -> {none, in}; in_state_change(from, in, subscribe) -> none; in_state_change(from, in, subscribed) -> {both, none}; in_state_change(from, in, unsubscribe) -> {none, none}; in_state_change(from, in, unsubscribed) -> none; in_state_change(from, both, subscribe) -> none; in_state_change(from, both, subscribed) -> {both, none}; in_state_change(from, both, unsubscribe) -> {none, out}; in_state_change(from, both, unsubscribed) -> {from, none}; in_state_change(both, _, subscribe) -> none; in_state_change(both, _, subscribed) -> none; in_state_change(both, _, unsubscribe) -> {to, none}; in_state_change(both, _, unsubscribed) -> {from, none}. out_state_change(none, none, subscribe) -> {none, out}; out_state_change(none, none, subscribed) -> none; out_state_change(none, none, unsubscribe) -> none; out_state_change(none, none, unsubscribed) -> none; out_state_change(none, out, subscribe) -> {none, out}; %% We need to resend query (RFC3921, section 9.2) out_state_change(none, out, subscribed) -> none; out_state_change(none, out, unsubscribe) -> {none, none}; out_state_change(none, out, unsubscribed) -> none; out_state_change(none, in, subscribe) -> {none, both}; out_state_change(none, in, subscribed) -> {from, none}; out_state_change(none, in, unsubscribe) -> none; out_state_change(none, in, unsubscribed) -> {none, none}; out_state_change(none, both, subscribe) -> none; out_state_change(none, both, subscribed) -> {from, out}; out_state_change(none, both, unsubscribe) -> {none, in}; out_state_change(none, both, unsubscribed) -> {none, out}; out_state_change(to, none, subscribe) -> none; out_state_change(to, none, subscribed) -> {both, none}; out_state_change(to, none, unsubscribe) -> {none, none}; out_state_change(to, none, unsubscribed) -> none; out_state_change(to, in, subscribe) -> none; out_state_change(to, in, subscribed) -> {both, none}; out_state_change(to, in, unsubscribe) -> {none, in}; out_state_change(to, in, unsubscribed) -> {to, none}; out_state_change(from, none, subscribe) -> {from, out}; out_state_change(from, none, subscribed) -> none; out_state_change(from, none, unsubscribe) -> none; out_state_change(from, none, unsubscribed) -> {none, none}; out_state_change(from, out, subscribe) -> none; out_state_change(from, out, subscribed) -> none; out_state_change(from, out, unsubscribe) -> {from, none}; out_state_change(from, out, unsubscribed) -> {none, out}; out_state_change(both, none, subscribe) -> none; out_state_change(both, none, subscribed) -> none; out_state_change(both, none, unsubscribe) -> {from, none}; out_state_change(both, none, unsubscribed) -> {to, none}; % Invalid states that can occurs from roster modification from API out_state_change(to, out, subscribe) -> none; out_state_change(to, out, subscribed) -> {both, none}; out_state_change(to, out, unsubscribe) -> {none, none}; out_state_change(to, out, unsubscribed) -> none; out_state_change(to, both, subscribe) -> none; out_state_change(to, both, subscribed) -> {both, none}; out_state_change(to, both, unsubscribe) -> {none, in}; out_state_change(to, both, unsubscribed) -> {to, none}; out_state_change(from, in, subscribe) -> {from, out}; out_state_change(from, in, subscribed) -> none; out_state_change(from, in, unsubscribe) -> none; out_state_change(from, in, unsubscribed) -> {none, none}; out_state_change(from, both, subscribe) -> none; out_state_change(from, both, subscribed) -> none; out_state_change(from, both, unsubscribe) -> {from, none}; out_state_change(from, both, unsubscribed) -> {none, out}; out_state_change(both, _, subscribe) -> none; out_state_change(both, _, subscribed) -> none; out_state_change(both, _, unsubscribe) -> {from, none}; out_state_change(both, _, unsubscribed) -> {to, none}. in_auto_reply(from, none, subscribe) -> subscribed; in_auto_reply(from, out, subscribe) -> subscribed; in_auto_reply(both, none, subscribe) -> subscribed; in_auto_reply(none, in, unsubscribe) -> unsubscribed; in_auto_reply(none, both, unsubscribe) -> unsubscribed; in_auto_reply(to, in, unsubscribe) -> unsubscribed; in_auto_reply(from, none, unsubscribe) -> unsubscribed; in_auto_reply(from, out, unsubscribe) -> unsubscribed; in_auto_reply(both, none, unsubscribe) -> unsubscribed; in_auto_reply(_, _, _) -> none. -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Items = get_user_roster([], {LUser, LServer}), send_unsubscription_to_rosteritems(LUser, LServer, Items), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), delete_cache(Mod, LUser, LServer, [Item#roster.jid || Item <- Items]). %% For each contact with Subscription: %% Both or From, send a "unsubscribed" presence stanza; %% Both or To, send a "unsubscribe" presence stanza. -spec send_unsubscription_to_rosteritems(binary(), binary(), [#roster{}]) -> ok. send_unsubscription_to_rosteritems(LUser, LServer, RosterItems) -> From = jid:make({LUser, LServer, <<"">>}), lists:foreach(fun (RosterItem) -> send_unsubscribing_presence(From, RosterItem) end, RosterItems). -spec send_unsubscribing_presence(jid(), #roster{}) -> ok. send_unsubscribing_presence(From, Item) -> IsTo = case Item#roster.subscription of both -> true; to -> true; _ -> false end, IsFrom = case Item#roster.subscription of both -> true; from -> true; _ -> false end, if IsTo -> ejabberd_router:route( #presence{type = unsubscribe, from = jid:remove_resource(From), to = jid:make(Item#roster.jid)}); true -> ok end, if IsFrom -> ejabberd_router:route( #presence{type = unsubscribed, from = jid:remove_resource(From), to = jid:make(Item#roster.jid)}); true -> ok end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec set_items(binary(), binary(), roster_query()) -> {atomic, ok} | {aborted, any()}. set_items(User, Server, #roster_query{items = Items}) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJIDs = [jid:tolower(Item#roster_item.jid) || Item <- Items], F = fun () -> lists:foreach( fun(Item) -> process_item_set_t(LUser, LServer, Item) end, Items) end, transaction(LUser, LServer, LJIDs, F). -spec update_roster_t(binary(), binary(), ljid(), #roster{}) -> any(). update_roster_t(LUser, LServer, LJID, Item) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:update_roster(LUser, LServer, LJID, Item). -spec del_roster_t(binary(), binary(), ljid()) -> any(). del_roster_t(LUser, LServer, LJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:del_roster(LUser, LServer, LJID). -spec process_item_set_t(binary(), binary(), roster_item()) -> any(). process_item_set_t(LUser, LServer, #roster_item{jid = JID1} = QueryItem) -> JID = {JID1#jid.user, JID1#jid.server, <<>>}, LJID = {JID1#jid.luser, JID1#jid.lserver, <<>>}, Item = #roster{usj = {LUser, LServer, LJID}, us = {LUser, LServer}, jid = JID}, Item2 = decode_item(QueryItem, Item, _Managed = true), case Item2#roster.subscription of remove -> del_roster_t(LUser, LServer, LJID); _ -> update_roster_t(LUser, LServer, LJID, Item2) end; process_item_set_t(_LUser, _LServer, _) -> ok. -spec c2s_self_presence({presence(), c2s_state()}) -> {presence(), c2s_state()}. c2s_self_presence({_, #{pres_last := _}} = Acc) -> Acc; c2s_self_presence({#presence{type = available} = Pkt, State}) -> Prio = get_priority_from_presence(Pkt), if Prio >= 0 -> State1 = resend_pending_subscriptions(State), {Pkt, State1}; true -> {Pkt, State} end; c2s_self_presence(Acc) -> Acc. -spec resend_pending_subscriptions(c2s_state()) -> c2s_state(). resend_pending_subscriptions(#{jid := JID} = State) -> BareJID = jid:remove_resource(JID), Result = get_roster(JID#jid.luser, JID#jid.lserver), lists:foldl( fun(#roster{ask = Ask} = R, AccState) when Ask == in; Ask == both -> Message = R#roster.askmessage, Status = if is_binary(Message) -> (Message); true -> <<"">> end, Sub = #presence{from = jid:make(R#roster.jid), to = BareJID, type = subscribe, status = xmpp:mk_text(Status)}, ejabberd_c2s:send(AccState, Sub); (_, AccState) -> AccState end, State, Result). -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of undefined -> 0; _ -> Prio end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec get_jid_info({subscription(), ask(), [binary()]}, binary(), binary(), jid()) -> {subscription(), ask(), [binary()]}. get_jid_info(_, User, Server, JID) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), LJID = jid:tolower(JID), get_subscription_and_groups(LUser, LServer, LJID). %% Check if `From` is subscriberd to `To`s presence %% note 1: partial subscriptions are also considered, i.e. %% `To` has already sent a subscription request to `From` %% note 2: it's assumed a user is subscribed to self %% note 3: `To` MUST be a local user, `From` can be any user -spec is_subscribed(jid(), jid()) -> boolean(). is_subscribed(#jid{luser = LUser, lserver = LServer}, #jid{luser = LUser, lserver = LServer}) -> true; is_subscribed(From, #jid{luser = LUser, lserver = LServer}) -> {Sub, Ask, _} = ejabberd_hooks:run_fold( roster_get_jid_info, LServer, {none, none, []}, [LUser, LServer, From]), (Sub /= none) orelse (Ask == subscribe) orelse (Ask == out) orelse (Ask == both). process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> LServer = ejabberd_config:get_myname(), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% webadmin_page(_, Host, #request{us = _US, path = [<<"user">>, U, <<"roster">>], q = Query, lang = Lang} = _Request) -> Res = user_roster(U, Host, Query, Lang), {stop, Res}; webadmin_page(Acc, _, _) -> Acc. user_roster(User, Server, Query, Lang) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), US = {LUser, LServer}, Items1 = get_roster(LUser, LServer), Res = user_roster_parse_query(User, Server, Items1, Query), Items = get_roster(LUser, LServer), SItems = lists:sort(Items), FItems = case SItems of [] -> [?CT(?T("None"))]; _ -> [?XE(<<"table">>, [?XE(<<"thead">>, [?XE(<<"tr">>, [?XCT(<<"td">>, ?T("Jabber ID")), ?XCT(<<"td">>, ?T("Nickname")), ?XCT(<<"td">>, ?T("Subscription")), ?XCT(<<"td">>, ?T("Pending")), ?XCT(<<"td">>, ?T("Groups"))])]), ?XE(<<"tbody">>, (lists:map(fun (R) -> Groups = lists:flatmap(fun (Group) -> [?C(Group), ?BR] end, R#roster.groups), Pending = ask_to_pending(R#roster.ask), TDJID = build_contact_jid_td(R#roster.jid), ?XE(<<"tr">>, [TDJID, ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], (R#roster.name)), ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], (iolist_to_binary(atom_to_list(R#roster.subscription)))), ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], (iolist_to_binary(atom_to_list(Pending)))), ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], Groups), if Pending == in -> ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], [?INPUTT(<<"submit">>, <<"validate", (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>, ?T("Validate"))]); true -> ?X(<<"td">>) end, ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], [?INPUTTD(<<"submit">>, <<"remove", (ejabberd_web_admin:term_to_id(R#roster.jid))/binary>>, ?T("Remove"))])]) end, SItems)))])] end, PageTitle = str:translate_and_format(Lang, ?T("Roster of ~ts"), [us_to_list(US)]), (?H1GL(PageTitle, <<"modules/#mod-roster">>, <<"mod_roster">>)) ++ case Res of ok -> [?XREST(?T("Submitted"))]; error -> [?XREST(?T("Bad format"))]; nothing -> [] end ++ [?XAE(<<"form">>, [{<<"action">>, <<"">>}, {<<"method">>, <<"post">>}], (FItems ++ [?P, ?INPUT(<<"text">>, <<"newjid">>, <<"">>), ?C(<<" ">>), ?INPUTT(<<"submit">>, <<"addjid">>, ?T("Add Jabber ID"))]))]. build_contact_jid_td(RosterJID) -> ContactJID = jid:make(RosterJID), JIDURI = case {ContactJID#jid.luser, ContactJID#jid.lserver} of {<<"">>, _} -> <<"">>; {CUser, CServer} -> case lists:member(CServer, ejabberd_option:hosts()) of false -> <<"">>; true -> <<"../../../../../server/", CServer/binary, "/user/", CUser/binary, "/">> end end, case JIDURI of <<>> -> ?XAC(<<"td">>, [{<<"class">>, <<"valign">>}], (jid:encode(RosterJID))); URI when is_binary(URI) -> ?XAE(<<"td">>, [{<<"class">>, <<"valign">>}], [?AC(JIDURI, (jid:encode(RosterJID)))]) end. user_roster_parse_query(User, Server, Items, Query) -> case lists:keysearch(<<"addjid">>, 1, Query) of {value, _} -> case lists:keysearch(<<"newjid">>, 1, Query) of {value, {_, SJID}} -> try jid:decode(SJID) of JID -> user_roster_subscribe_jid(User, Server, JID), ok catch _:{bad_jid, _} -> error end; false -> error end; false -> case catch user_roster_item_parse_query(User, Server, Items, Query) of submitted -> ok; {'EXIT', _Reason} -> error; _ -> nothing end end. user_roster_subscribe_jid(User, Server, JID) -> UJID = jid:make(User, Server), Presence = #presence{from = UJID, to = JID, type = subscribe}, out_subscription(Presence), ejabberd_router:route(Presence). user_roster_item_parse_query(User, Server, Items, Query) -> lists:foreach(fun (R) -> JID = R#roster.jid, case lists:keysearch(<<"validate", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of {value, _} -> JID1 = jid:make(JID), UJID = jid:make(User, Server), Pres = #presence{from = UJID, to = JID1, type = subscribed}, out_subscription(Pres), ejabberd_router:route(Pres), throw(submitted); false -> case lists:keysearch(<<"remove", (ejabberd_web_admin:term_to_id(JID))/binary>>, 1, Query) of {value, _} -> UJID = jid:make(User, Server), RosterItem = #roster_item{ jid = jid:make(JID), subscription = remove}, process_iq_set( #iq{type = set, from = UJID, to = UJID, id = p1_rand:get_string(), sub_els = [#roster_query{ items = [RosterItem]}]}), throw(submitted); false -> ok end end end, Items), nothing. us_to_list({User, Server}) -> jid:encode({User, Server, <<"">>}). webadmin_user(Acc, User, Server, Lang) -> QueueLen = length(get_roster(jid:nodeprep(User), jid:nameprep(Server))), FQueueLen = ?C(integer_to_binary(QueueLen)), FQueueView = ?AC(<<"roster/">>, ?T("View Roster")), Acc ++ [?XCT(<<"h3">>, ?T("Roster:")), FQueueLen, ?C(<<" | ">>), FQueueView]. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec has_duplicated_groups([binary()]) -> boolean(). has_duplicated_groups(Groups) -> GroupsPrep = lists:usort([jid:resourceprep(G) || G <- Groups]), not (length(GroupsPrep) == length(Groups)). -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> CacheOpts = cache_opts(Opts), case use_cache(Mod, Host, roster_version) of true -> ets_cache:new(?ROSTER_VERSION_CACHE, CacheOpts); false -> ets_cache:delete(?ROSTER_VERSION_CACHE) end, case use_cache(Mod, Host, roster) of true -> ets_cache:new(?ROSTER_CACHE, CacheOpts), ets_cache:new(?ROSTER_ITEM_CACHE, CacheOpts); false -> ets_cache:delete(?ROSTER_CACHE), ets_cache:delete(?ROSTER_ITEM_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_roster_opt:cache_size(Opts), CacheMissed = mod_roster_opt:cache_missed(Opts), LifeTime = mod_roster_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary(), roster | roster_version) -> boolean(). use_cache(Mod, Host, Table) -> case erlang:function_exported(Mod, use_cache, 2) of true -> Mod:use_cache(Host, Table); false -> mod_roster_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. -spec delete_cache(module(), binary(), binary(), [ljid()]) -> ok. delete_cache(Mod, LUser, LServer, LJIDs) -> case use_cache(Mod, LServer, roster_version) of true -> ets_cache:delete(?ROSTER_VERSION_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end, case use_cache(Mod, LServer, roster) of true -> Nodes = cache_nodes(Mod, LServer), ets_cache:delete(?ROSTER_CACHE, {LUser, LServer}, Nodes), lists:foreach( fun(LJID) -> ets_cache:delete( ?ROSTER_ITEM_CACHE, {LUser, LServer, jid:remove_resource(LJID)}, Nodes) end, LJIDs); false -> ok end. export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"roster_version">>, 2}, {<<"rostergroups">>, 3}, {<<"rosterusers">>, 10}]. import_start(LServer, DBType) -> Mod = gen_mod:db_mod(DBType, ?MODULE), ets:new(rostergroups_tmp, [private, named_table, bag]), Mod:init(LServer, []), ok. import_stop(_LServer, _DBType) -> ets:delete(rostergroups_tmp), ok. row_length() -> case ejabberd_sql:use_new_schema() of true -> 10; false -> 9 end. import(LServer, {sql, _}, _DBType, <<"rostergroups">>, [LUser, SJID, Group]) -> LJID = jid:tolower(jid:decode(SJID)), ets:insert(rostergroups_tmp, {{LUser, LServer, LJID}, Group}), ok; import(LServer, {sql, _}, DBType, <<"rosterusers">>, Row) -> I = mod_roster_sql:raw_to_record(LServer, lists:sublist(Row, row_length())), Groups = [G || {_, G} <- ets:lookup(rostergroups_tmp, I#roster.usj)], RosterItem = I#roster{groups = Groups}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, <<"rosterusers">>, RosterItem); import(LServer, {sql, _}, DBType, <<"roster_version">>, [LUser, Ver]) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, <<"roster_version">>, [LUser, Ver]). mod_opt_type(access) -> econf:acl(); mod_opt_type(store_current_id) -> econf:bool(); mod_opt_type(versioning) -> econf:bool(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{access, all}, {store_current_id, false}, {versioning, false}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module implements roster management as " "defined in https://tools.ietf.org/html/rfc6121#section-2" "[RFC6121 Section 2]. The module also adds support for " "https://xmpp.org/extensions/xep-0237.html" "[XEP-0237: Roster Versioning]."), opts => [{access, #{value => ?T("AccessName"), desc => ?T("This option can be configured to specify " "rules to restrict roster management. " "If the rule returns 'deny' on the requested " "user name, that user cannot modify their personal " "roster, i.e. they cannot add/remove/modify contacts " "or send presence subscriptions. " "The default value is 'all', i.e. no restrictions.")}}, {versioning, #{value => "true | false", desc => ?T("Enables/disables Roster Versioning. " "The default value is 'false'.")}}, {store_current_id, #{value => "true | false", desc => ?T("If this option is set to 'true', the current " "roster version number is stored on the database. " "If set to 'false', the roster version number is " "calculated on the fly each time. Enabling this " "option reduces the load for both ejabberd and the database. " "This option does not affect the client in any way. " "This option is only useful if option 'versioning' is " "set to 'true'. The default value is 'false'. " "IMPORTANT: if you use _`mod_shared_roster`_ or " " _`mod_shared_roster_ldap`_, you must set the value " "of the option to 'false'.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}], example => ["modules:", " ...", " mod_roster:", " versioning: true", " store_current_id: false", " ..."]}. ejabberd-21.12/src/ejabberd_update.erl0000644000232200023220000002050114154362354020223 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_update.erl %%% Author : Alexey Shchepin %%% Purpose : ejabberd code updater %%% Created : 27 Jan 2006 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_update). -author('alexey@process-one.net'). %% API -export([update/0, update/1, update_info/0]). -include("logger.hrl"). %%==================================================================== %% API %%==================================================================== %% Update all the modified modules update() -> case update_info() of {ok, Dir, _UpdatedBeams, _Script, LowLevelScript, _Check} -> Eval = eval_script( LowLevelScript, [], [{ejabberd, "", filename:join(Dir, "..")}]), ?DEBUG("Eval: ~p~n", [Eval]), Eval; {error, Reason} -> {error, Reason} end. %% Update only the specified modules update(ModulesToUpdate) -> case update_info() of {ok, Dir, UpdatedBeamsAll, _Script, _LowLevelScript, _Check} -> UpdatedBeamsNow = [A || A <- UpdatedBeamsAll, B <- ModulesToUpdate, A == B], {_, LowLevelScript, _} = build_script(Dir, UpdatedBeamsNow), Eval = eval_script( LowLevelScript, [], [{ejabberd, "", filename:join(Dir, "..")}]), ?DEBUG("Eval: ~p~n", [Eval]), Eval; {error, Reason} -> {error, Reason} end. eval_script(Script, Apps, LibDirs) -> release_handler_1:eval_script(Script, Apps, LibDirs, [], []). %% Get information about the modified modules update_info() -> Dir = filename:dirname(code:which(ejabberd)), case file:list_dir(Dir) of {ok, Files} -> update_info(Dir, Files); {error, Reason} -> {error, Reason} end. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- update_info(Dir, Files) -> Beams = lists:sort(get_beams(Files)), UpdatedBeams = get_updated_beams(Beams), ?DEBUG("BEAM files: ~p~n", [UpdatedBeams]), {Script, LowLevelScript, Check} = build_script(Dir, UpdatedBeams), {ok, Dir, UpdatedBeams, Script, LowLevelScript, Check}. get_beams(Files) -> [list_to_atom(filename:rootname(FN)) || FN <- Files, lists:suffix(".beam", FN)]. %% Return only the beams that have different version get_updated_beams(Beams) -> lists:filter( fun(Module) -> NewVsn = get_new_version(Module), case code:is_loaded(Module) of {file, _} -> CurVsn = get_current_version(Module), (NewVsn /= CurVsn andalso NewVsn /= unknown_version); false -> false end end, Beams). get_new_version(Module) -> Path = code:which(Module), VersionRes = beam_lib:version(Path), case VersionRes of {ok, {Module, NewVsn}} -> NewVsn; %% If a m1.erl has -module("m2"): _ -> unknown_version end. get_current_version(Module) -> Attrs = Module:module_info(attributes), case lists:keysearch(vsn, 1, Attrs) of {value, {vsn, CurVsn}} -> CurVsn; _ -> unknown_version end. %% @spec(Dir::string(), UpdatedBeams::[atom()]) -> {Script,LowLevelScript,Check} build_script(Dir, UpdatedBeams) -> Script = make_script(UpdatedBeams), LowLevelScript = make_low_level_script(UpdatedBeams, Script), Check = release_handler_1:check_script( LowLevelScript, [{ejabberd, "", filename:join(Dir, "..")}]), Check1 = case Check of {ok, []} -> ?DEBUG("Script: ~p~n", [Script]), ?DEBUG("Low level script: ~p~n", [LowLevelScript]), ?DEBUG("Check: ~p~n", [Check]), ok; _ -> ?ERROR_MSG("Script: ~p~n", [Script]), ?ERROR_MSG("Low level script: ~p~n", [LowLevelScript]), ?ERROR_MSG("Check: ~p~n", [Check]), error end, {Script, LowLevelScript, Check1}. %% Copied from Erlang/OTP file: lib/sasl/src/systools.hrl -ifdef(SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL). -record(application, {name, %% Name of the application, atom(). type = permanent, %% Application start type, atom(). vsn = "", %% Version of the application, string(). id = "", %% Id of the application, string(). description = "", %% Description of application, string(). modules = [], %% [Module | {Module,Vsn}] of modules %% incorporated in the application, %% Module = atom(), Vsn = string(). uses = [], %% [Application] list of applications required %% by the application, Application = atom(). includes = [], %% [Application] list of applications included %% by the application, Application = atom(). regs = [], %% [RegNames] a list of registered process %% names used by the application, RegNames = %% atom(). env = [], %% [{Key,Value}] environment variable of %% application, Key = Value = term(). maxT = infinity, %% Max time an application may exist, %% integer() | infinity. maxP = infinity, %% Max number of processes in an application, %% integer() | infinity. mod = [], %% [] | {Mod, StartArgs}, Mod= atom(), %% StartArgs = list(). start_phases, %% [{Phase, PhaseArgs}] | undefined, %% Phase = atom(), %% PhaseArgs = list(). dir = "" %% The directory where the .app file was %% found (internal use). }). -else. -record(application, {name, %% Name of the application, atom(). type = permanent, %% Application start type, atom(). vsn = "", %% Version of the application, string(). id = "", %% Id of the application, string(). description = "", %% Description of application, string(). modules = [], %% [Module | {Module,Vsn}] of modules %% incorporated in the application, %% Module = atom(), Vsn = string(). uses = [], %% [Application] list of applications required %% by the application, Application = atom(). optional = [], %% [Application] list of applications in uses %% that are optional, Application = atom(). includes = [], %% [Application] list of applications included %% by the application, Application = atom(). regs = [], %% [RegNames] a list of registered process %% names used by the application, RegNames = %% atom(). env = [], %% [{Key,Value}] environment variable of %% application, Key = Value = term(). maxT = infinity, %% Max time an application may exist, %% integer() | infinity. maxP = infinity, %% Max number of processes in an application, %% integer() | infinity. mod = [], %% [] | {Mod, StartArgs}, Mod= atom(), %% StartArgs = list(). start_phases, %% [{Phase, PhaseArgs}] | undefined, %% Phase = atom(), %% PhaseArgs = list(). dir = "" %% The directory where the .app file was %% found (internal use). }). -endif. make_script(UpdatedBeams) -> lists:map( fun(Module) -> {ok, {Module, [{attributes, NewAttrs}]}} = beam_lib:chunks(code:which(Module), [attributes]), CurAttrs = Module:module_info(attributes), case lists:keysearch(update_info, 1, NewAttrs) of {value, {_, [{update, _}]}} -> case lists:keysearch(update_info, 1, CurAttrs) of {value, {_, [{update, Extra}]}} -> {update, Module, {advanced, Extra}}; false -> {update, Module, {advanced, 0}} end; false -> {load_module, Module} end end, UpdatedBeams). make_low_level_script(UpdatedBeams, Script) -> EJDApp = #application{name = ejabberd, modules = UpdatedBeams}, {ok, LowLevelScript} = systools_rc:translate_scripts([Script], [EJDApp], [EJDApp]), LowLevelScript. ejabberd-21.12/src/mod_muc_opt.erl0000644000232200023220000002177114154362354017442 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_muc_opt). -export([access/1]). -export([access_admin/1]). -export([access_create/1]). -export([access_mam/1]). -export([access_persistent/1]). -export([access_register/1]). -export([db_type/1]). -export([default_room_options/1]). -export([hibernation_timeout/1]). -export([history_size/1]). -export([host/1]). -export([hosts/1]). -export([max_captcha_whitelist/1]). -export([max_password/1]). -export([max_room_desc/1]). -export([max_room_id/1]). -export([max_room_name/1]). -export([max_rooms_discoitems/1]). -export([max_user_conferences/1]). -export([max_users/1]). -export([max_users_admin_threshold/1]). -export([max_users_presence/1]). -export([min_message_interval/1]). -export([min_presence_interval/1]). -export([name/1]). -export([preload_rooms/1]). -export([queue_type/1]). -export([ram_db_type/1]). -export([regexp_room_id/1]). -export([room_shaper/1]). -export([user_message_shaper/1]). -export([user_presence_shaper/1]). -export([vcard/1]). -spec access(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access(Opts) when is_map(Opts) -> gen_mod:get_opt(access, Opts); access(Host) -> gen_mod:get_module_opt(Host, mod_muc, access). -spec access_admin(gen_mod:opts() | global | binary()) -> 'none' | acl:acl(). access_admin(Opts) when is_map(Opts) -> gen_mod:get_opt(access_admin, Opts); access_admin(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_admin). -spec access_create(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_create(Opts) when is_map(Opts) -> gen_mod:get_opt(access_create, Opts); access_create(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_create). -spec access_mam(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_mam(Opts) when is_map(Opts) -> gen_mod:get_opt(access_mam, Opts); access_mam(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_mam). -spec access_persistent(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_persistent(Opts) when is_map(Opts) -> gen_mod:get_opt(access_persistent, Opts); access_persistent(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_persistent). -spec access_register(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_register(Opts) when is_map(Opts) -> gen_mod:get_opt(access_register, Opts); access_register(Host) -> gen_mod:get_module_opt(Host, mod_muc, access_register). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, db_type). -spec default_room_options(gen_mod:opts() | global | binary()) -> [{atom(),'anyone' | 'false' | 'moderators' | 'nobody' | 'true' | binary() | ['moderator' | 'participant' | 'visitor'] | pos_integer()}]. default_room_options(Opts) when is_map(Opts) -> gen_mod:get_opt(default_room_options, Opts); default_room_options(Host) -> gen_mod:get_module_opt(Host, mod_muc, default_room_options). -spec hibernation_timeout(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). hibernation_timeout(Opts) when is_map(Opts) -> gen_mod:get_opt(hibernation_timeout, Opts); hibernation_timeout(Host) -> gen_mod:get_module_opt(Host, mod_muc, hibernation_timeout). -spec history_size(gen_mod:opts() | global | binary()) -> non_neg_integer(). history_size(Opts) when is_map(Opts) -> gen_mod:get_opt(history_size, Opts); history_size(Host) -> gen_mod:get_module_opt(Host, mod_muc, history_size). -spec host(gen_mod:opts() | global | binary()) -> binary(). host(Opts) when is_map(Opts) -> gen_mod:get_opt(host, Opts); host(Host) -> gen_mod:get_module_opt(Host, mod_muc, host). -spec hosts(gen_mod:opts() | global | binary()) -> [binary()]. hosts(Opts) when is_map(Opts) -> gen_mod:get_opt(hosts, Opts); hosts(Host) -> gen_mod:get_module_opt(Host, mod_muc, hosts). -spec max_captcha_whitelist(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_captcha_whitelist(Opts) when is_map(Opts) -> gen_mod:get_opt(max_captcha_whitelist, Opts); max_captcha_whitelist(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_captcha_whitelist). -spec max_password(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_password(Opts) when is_map(Opts) -> gen_mod:get_opt(max_password, Opts); max_password(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_password). -spec max_room_desc(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_desc(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_desc, Opts); max_room_desc(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_desc). -spec max_room_id(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_id(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_id, Opts); max_room_id(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_id). -spec max_room_name(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). max_room_name(Opts) when is_map(Opts) -> gen_mod:get_opt(max_room_name, Opts); max_room_name(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_room_name). -spec max_rooms_discoitems(gen_mod:opts() | global | binary()) -> non_neg_integer(). max_rooms_discoitems(Opts) when is_map(Opts) -> gen_mod:get_opt(max_rooms_discoitems, Opts); max_rooms_discoitems(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_rooms_discoitems). -spec max_user_conferences(gen_mod:opts() | global | binary()) -> pos_integer(). max_user_conferences(Opts) when is_map(Opts) -> gen_mod:get_opt(max_user_conferences, Opts); max_user_conferences(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_user_conferences). -spec max_users(gen_mod:opts() | global | binary()) -> pos_integer(). max_users(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users, Opts); max_users(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users). -spec max_users_admin_threshold(gen_mod:opts() | global | binary()) -> pos_integer(). max_users_admin_threshold(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users_admin_threshold, Opts); max_users_admin_threshold(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users_admin_threshold). -spec max_users_presence(gen_mod:opts() | global | binary()) -> integer(). max_users_presence(Opts) when is_map(Opts) -> gen_mod:get_opt(max_users_presence, Opts); max_users_presence(Host) -> gen_mod:get_module_opt(Host, mod_muc, max_users_presence). -spec min_message_interval(gen_mod:opts() | global | binary()) -> number(). min_message_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(min_message_interval, Opts); min_message_interval(Host) -> gen_mod:get_module_opt(Host, mod_muc, min_message_interval). -spec min_presence_interval(gen_mod:opts() | global | binary()) -> number(). min_presence_interval(Opts) when is_map(Opts) -> gen_mod:get_opt(min_presence_interval, Opts); min_presence_interval(Host) -> gen_mod:get_module_opt(Host, mod_muc, min_presence_interval). -spec name(gen_mod:opts() | global | binary()) -> binary(). name(Opts) when is_map(Opts) -> gen_mod:get_opt(name, Opts); name(Host) -> gen_mod:get_module_opt(Host, mod_muc, name). -spec preload_rooms(gen_mod:opts() | global | binary()) -> boolean(). preload_rooms(Opts) when is_map(Opts) -> gen_mod:get_opt(preload_rooms, Opts); preload_rooms(Host) -> gen_mod:get_module_opt(Host, mod_muc, preload_rooms). -spec queue_type(gen_mod:opts() | global | binary()) -> 'file' | 'ram'. queue_type(Opts) when is_map(Opts) -> gen_mod:get_opt(queue_type, Opts); queue_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, queue_type). -spec ram_db_type(gen_mod:opts() | global | binary()) -> atom(). ram_db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(ram_db_type, Opts); ram_db_type(Host) -> gen_mod:get_module_opt(Host, mod_muc, ram_db_type). -spec regexp_room_id(gen_mod:opts() | global | binary()) -> <<>> | re:mp(). regexp_room_id(Opts) when is_map(Opts) -> gen_mod:get_opt(regexp_room_id, Opts); regexp_room_id(Host) -> gen_mod:get_module_opt(Host, mod_muc, regexp_room_id). -spec room_shaper(gen_mod:opts() | global | binary()) -> atom(). room_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(room_shaper, Opts); room_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, room_shaper). -spec user_message_shaper(gen_mod:opts() | global | binary()) -> atom(). user_message_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(user_message_shaper, Opts); user_message_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, user_message_shaper). -spec user_presence_shaper(gen_mod:opts() | global | binary()) -> atom(). user_presence_shaper(Opts) when is_map(Opts) -> gen_mod:get_opt(user_presence_shaper, Opts); user_presence_shaper(Host) -> gen_mod:get_module_opt(Host, mod_muc, user_presence_shaper). -spec vcard(gen_mod:opts() | global | binary()) -> 'undefined' | tuple(). vcard(Opts) when is_map(Opts) -> gen_mod:get_opt(vcard, Opts); vcard(Host) -> gen_mod:get_module_opt(Host, mod_muc, vcard). ejabberd-21.12/src/mod_client_state.erl0000644000232200023220000003704114154362354020447 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_client_state.erl %%% Author : Holger Weiss %%% Purpose : Filter stanzas sent to inactive clients (XEP-0352) %%% Created : 11 Sep 2014 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2014-2021 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(mod_client_state). -author('holger@zedat.fu-berlin.de'). -protocol({xep, 85, '2.1'}). -protocol({xep, 352, '0.1'}). -behaviour(gen_mod). %% gen_mod callbacks. -export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. -export([filter_presence/1, filter_chat_states/1, filter_pep/1, filter_other/1, c2s_stream_started/2, add_stream_feature/2, c2s_authenticated_packet/2, csi_activity/2, c2s_copy_session/2, c2s_session_resumed/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(CSI_QUEUE_MAX, 100). -type csi_type() :: presence | chatstate | {pep, binary()}. -type csi_queue() :: {non_neg_integer(), #{csi_key() => csi_element()}}. -type csi_timestamp() :: {non_neg_integer(), erlang:timestamp()}. -type csi_key() :: {ljid(), csi_type()}. -type csi_element() :: {csi_timestamp(), stanza()}. -type c2s_state() :: ejabberd_c2s:state(). -type filter_acc() :: {stanza() | drop, c2s_state()}. %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> ok. start(Host, Opts) -> QueuePresence = mod_client_state_opt:queue_presence(Opts), QueueChatStates = mod_client_state_opt:queue_chat_states(Opts), QueuePEP = mod_client_state_opt:queue_pep(Opts), if QueuePresence; QueueChatStates; QueuePEP -> register_hooks(Host), if QueuePresence -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_presence, 50); true -> ok end, if QueueChatStates -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_chat_states, 50); true -> ok end, if QueuePEP -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_pep, 50); true -> ok end; true -> ok end. -spec stop(binary()) -> ok. stop(Host) -> QueuePresence = mod_client_state_opt:queue_presence(Host), QueueChatStates = mod_client_state_opt:queue_chat_states(Host), QueuePEP = mod_client_state_opt:queue_pep(Host), if QueuePresence; QueueChatStates; QueuePEP -> unregister_hooks(Host), if QueuePresence -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_presence, 50); true -> ok end, if QueueChatStates -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_chat_states, 50); true -> ok end, if QueuePEP -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_pep, 50); true -> ok end; true -> ok end. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, _OldOpts) -> QueuePresence = mod_client_state_opt:queue_presence(NewOpts), QueueChatStates = mod_client_state_opt:queue_chat_states(NewOpts), QueuePEP = mod_client_state_opt:queue_pep(NewOpts), if QueuePresence; QueueChatStates; QueuePEP -> register_hooks(Host); true -> unregister_hooks(Host) end, if QueuePresence -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_presence, 50); true -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_presence, 50) end, if QueueChatStates -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_chat_states, 50); true -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_chat_states, 50) end, if QueuePEP -> ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_pep, 50); true -> ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_pep, 50) end. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(queue_presence) -> econf:bool(); mod_opt_type(queue_chat_states) -> econf:bool(); mod_opt_type(queue_pep) -> econf:bool(). mod_options(_) -> [{queue_presence, true}, {queue_chat_states, true}, {queue_pep, true}]. mod_doc() -> #{desc => [?T("This module allows for queueing certain types of stanzas " "when a client indicates that the user is not actively using " "the client right now (see https://xmpp.org/extensions/xep-0352.html" "[XEP-0352: Client State Indication]). This can save bandwidth and " "resources."), "", ?T("A stanza is dropped from the queue if it's effectively obsoleted " "by a new one (e.g., a new presence stanza would replace an old " "one from the same client). The queue is flushed if a stanza arrives " "that won't be queued, or if the queue size reaches a certain limit " "(currently 100 stanzas), or if the client becomes active again.")], opts => [{queue_presence, #{value => "true | false", desc => ?T("While a client is inactive, queue presence stanzas " "that indicate (un)availability. The default value is 'true'.")}}, {queue_chat_states, #{value => "true | false", desc => ?T("Queue \"standalone\" chat state notifications (as defined in " "https://xmpp.org/extensions/xep-0085.html" "[XEP-0085: Chat State Notifications]) while a client " "indicates inactivity. The default value is 'true'.")}}, {queue_pep, #{value => "true | false", desc => ?T("Queue PEP notifications while a client is inactive. " "When the queue is flushed, only the most recent notification " "of a given PEP node is delivered. The default value is 'true'.")}}]}. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. -spec register_hooks(binary()) -> ok. register_hooks(Host) -> ejabberd_hooks:add(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 50), ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, add_stream_feature, 50), ejabberd_hooks:add(c2s_authenticated_packet, Host, ?MODULE, c2s_authenticated_packet, 50), ejabberd_hooks:add(csi_activity, Host, ?MODULE, csi_activity, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:add(c2s_filter_send, Host, ?MODULE, filter_other, 75). -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> ejabberd_hooks:delete(c2s_stream_started, Host, ?MODULE, c2s_stream_started, 50), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, add_stream_feature, 50), ejabberd_hooks:delete(c2s_authenticated_packet, Host, ?MODULE, c2s_authenticated_packet, 50), ejabberd_hooks:delete(csi_activity, Host, ?MODULE, csi_activity, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(c2s_session_resumed, Host, ?MODULE, c2s_session_resumed, 50), ejabberd_hooks:delete(c2s_filter_send, Host, ?MODULE, filter_other, 75). %%-------------------------------------------------------------------- %% ejabberd_hooks callbacks. %%-------------------------------------------------------------------- -spec c2s_stream_started(c2s_state(), stream_start()) -> c2s_state(). c2s_stream_started(State, _) -> init_csi_state(State). -spec c2s_authenticated_packet(c2s_state(), xmpp_element()) -> c2s_state(). c2s_authenticated_packet(#{lserver := LServer} = C2SState, #csi{type = active}) -> ejabberd_hooks:run_fold(csi_activity, LServer, C2SState, [active]); c2s_authenticated_packet(#{lserver := LServer} = C2SState, #csi{type = inactive}) -> ejabberd_hooks:run_fold(csi_activity, LServer, C2SState, [inactive]); c2s_authenticated_packet(C2SState, _) -> C2SState. -spec csi_activity(c2s_state(), active | inactive) -> c2s_state(). csi_activity(C2SState, active) -> C2SState1 = C2SState#{csi_state => active}, flush_queue(C2SState1); csi_activity(C2SState, inactive) -> C2SState#{csi_state => inactive}. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(C2SState, #{csi_queue := Q}) -> C2SState#{csi_queue => Q}; c2s_copy_session(C2SState, _) -> C2SState. -spec c2s_session_resumed(c2s_state()) -> c2s_state(). c2s_session_resumed(C2SState) -> flush_queue(C2SState). -spec filter_presence(filter_acc()) -> filter_acc(). filter_presence({#presence{meta = #{csi_resend := true}}, _} = Acc) -> Acc; filter_presence({#presence{to = To, type = Type} = Pres, #{csi_state := inactive} = C2SState}) when Type == available; Type == unavailable -> ?DEBUG("Got availability presence stanza for ~ts", [jid:encode(To)]), enqueue_stanza(presence, Pres, C2SState); filter_presence(Acc) -> Acc. -spec filter_chat_states(filter_acc()) -> filter_acc(). filter_chat_states({#message{meta = #{csi_resend := true}}, _} = Acc) -> Acc; filter_chat_states({#message{from = From, to = To} = Msg, #{csi_state := inactive} = C2SState} = Acc) -> case misc:is_standalone_chat_state(Msg) of true -> case {From, To} of {#jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}} -> %% Don't queue (carbon copies of) chat states from other %% resources, as they might be used to sync the state of %% conversations across clients. Acc; _ -> ?DEBUG("Got standalone chat state notification for ~ts", [jid:encode(To)]), enqueue_stanza(chatstate, Msg, C2SState) end; false -> Acc end; filter_chat_states(Acc) -> Acc. -spec filter_pep(filter_acc()) -> filter_acc(). filter_pep({#message{meta = #{csi_resend := true}}, _} = Acc) -> Acc; filter_pep({#message{to = To} = Msg, #{csi_state := inactive} = C2SState} = Acc) -> case get_pep_node(Msg) of undefined -> Acc; Node -> ?DEBUG("Got PEP notification for ~ts", [jid:encode(To)]), enqueue_stanza({pep, Node}, Msg, C2SState) end; filter_pep(Acc) -> Acc. -spec filter_other(filter_acc()) -> filter_acc(). filter_other({Stanza, #{jid := JID} = C2SState} = Acc) when ?is_stanza(Stanza) -> case xmpp:get_meta(Stanza) of #{csi_resend := true} -> Acc; _ -> ?DEBUG("Won't add stanza for ~ts to CSI queue", [jid:encode(JID)]), From = case xmpp:get_from(Stanza) of undefined -> JID; F -> F end, C2SState1 = dequeue_sender(From, C2SState), {Stanza, C2SState1} end; filter_other(Acc) -> Acc. -spec add_stream_feature([xmpp_element()], binary()) -> [xmpp_element()]. add_stream_feature(Features, Host) -> case gen_mod:is_loaded(Host, ?MODULE) of true -> [#feature_csi{} | Features]; false -> Features end. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec init_csi_state(c2s_state()) -> c2s_state(). init_csi_state(C2SState) -> C2SState#{csi_state => active, csi_queue => queue_new()}. -spec enqueue_stanza(csi_type(), stanza(), c2s_state()) -> filter_acc(). enqueue_stanza(Type, Stanza, #{csi_state := inactive, csi_queue := Q} = C2SState) -> case queue_len(Q) >= ?CSI_QUEUE_MAX of true -> ?DEBUG("CSI queue too large, going to flush it", []), C2SState1 = flush_queue(C2SState), enqueue_stanza(Type, Stanza, C2SState1); false -> From = jid:tolower(xmpp:get_from(Stanza)), Q1 = queue_in({From, Type}, Stanza, Q), {stop, {drop, C2SState#{csi_queue => Q1}}} end; enqueue_stanza(_Type, Stanza, State) -> {Stanza, State}. -spec dequeue_sender(jid(), c2s_state()) -> c2s_state(). dequeue_sender(#jid{luser = U, lserver = S} = Sender, #{jid := JID} = C2SState) -> case maps:get(csi_queue, C2SState, undefined) of undefined -> %% This may happen when the module is (re)loaded in runtime init_csi_state(C2SState); Q -> ?DEBUG("Flushing packets of ~ts@~ts from CSI queue of ~ts", [U, S, jid:encode(JID)]), {Elems, Q1} = queue_take(Sender, Q), C2SState1 = flush_stanzas(C2SState, Elems), C2SState1#{csi_queue => Q1} end. -spec flush_queue(c2s_state()) -> c2s_state(). flush_queue(#{csi_queue := Q, jid := JID} = C2SState) -> ?DEBUG("Flushing CSI queue of ~ts", [jid:encode(JID)]), C2SState1 = flush_stanzas(C2SState, queue_to_list(Q)), C2SState1#{csi_queue => queue_new()}. -spec flush_stanzas(c2s_state(), [{csi_type(), csi_timestamp(), stanza()}]) -> c2s_state(). flush_stanzas(#{lserver := LServer} = C2SState, Elems) -> lists:foldl( fun({Time, Stanza}, AccState) -> Stanza1 = add_delay_info(Stanza, LServer, Time), ejabberd_c2s:send(AccState, Stanza1) end, C2SState, Elems). -spec add_delay_info(stanza(), binary(), csi_timestamp()) -> stanza(). add_delay_info(Stanza, LServer, {_Seq, TimeStamp}) -> Stanza1 = misc:add_delay_info( Stanza, jid:make(LServer), TimeStamp, <<"Client Inactive">>), xmpp:put_meta(Stanza1, csi_resend, true). -spec get_pep_node(message()) -> binary() | undefined. get_pep_node(#message{from = #jid{luser = <<>>}}) -> %% It's not PEP. undefined; get_pep_node(#message{} = Msg) -> case xmpp:get_subtag(Msg, #ps_event{}) of #ps_event{items = #ps_items{node = Node}} -> Node; _ -> undefined end. %%-------------------------------------------------------------------- %% Queue interface %%-------------------------------------------------------------------- -spec queue_new() -> csi_queue(). queue_new() -> {0, #{}}. -spec queue_in(csi_key(), stanza(), csi_queue()) -> csi_queue(). queue_in(Key, Stanza, {Seq, Q}) -> Seq1 = Seq + 1, Time = {Seq1, erlang:timestamp()}, Q1 = maps:put(Key, {Time, Stanza}, Q), {Seq1, Q1}. -spec queue_take(jid(), csi_queue()) -> {[csi_element()], csi_queue()}. queue_take(#jid{luser = LUser, lserver = LServer}, {Seq, Q}) -> {Vals, Q1} = maps:fold(fun({{U, S, _}, _} = Key, Val, {AccVals, AccQ}) when U == LUser, S == LServer -> {[Val | AccVals], maps:remove(Key, AccQ)}; (_, _, Acc) -> Acc end, {[], Q}, Q), {lists:keysort(1, Vals), {Seq, Q1}}. -spec queue_len(csi_queue()) -> non_neg_integer(). queue_len({_, Q}) -> maps:size(Q). -spec queue_to_list(csi_queue()) -> [csi_element()]. queue_to_list({_, Q}) -> lists:keysort(1, maps:values(Q)). ejabberd-21.12/src/pubsub_subscription.erl0000644000232200023220000003054314154362354021236 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : pubsub_subscription.erl %%% Author : Brian Cully %%% Purpose : Handle pubsub subscriptions options %%% Created : 29 May 2009 by Brian Cully %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(pubsub_subscription). -author("bjc@kublai.com"). %% API -export([init/3, subscribe_node/3, unsubscribe_node/3, get_subscription/3, set_subscription/4, make_subid/0, get_options_xform/2, parse_options_xform/1]). % Internal function also exported for use in transactional bloc from pubsub plugins -export([add_subscription/3, delete_subscription/3, read_subscription/3, write_subscription/4]). -include("pubsub.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(PUBSUB_DELIVER, <<"pubsub#deliver">>). -define(PUBSUB_DIGEST, <<"pubsub#digest">>). -define(PUBSUB_DIGEST_FREQUENCY, <<"pubsub#digest_frequency">>). -define(PUBSUB_EXPIRE, <<"pubsub#expire">>). -define(PUBSUB_INCLUDE_BODY, <<"pubsub#include_body">>). -define(PUBSUB_SHOW_VALUES, <<"pubsub#show-values">>). -define(PUBSUB_SUBSCRIPTION_TYPE, <<"pubsub#subscription_type">>). -define(PUBSUB_SUBSCRIPTION_DEPTH, <<"pubsub#subscription_depth">>). -define(DELIVER_LABEL, <<"Whether an entity wants to receive or disable notifications">>). -define(DIGEST_LABEL, <<"Whether an entity wants to receive digests " "(aggregations) of notifications or all notifications individually">>). -define(DIGEST_FREQUENCY_LABEL, <<"The minimum number of milliseconds between " "sending any two notification digests">>). -define(EXPIRE_LABEL, <<"The DateTime at which a leased subscription will end or has ended">>). -define(INCLUDE_BODY_LABEL, <<"Whether an entity wants to receive an " "XMPP message body in addition to the payload format">>). -define(SHOW_VALUES_LABEL, <<"The presence states for which an entity wants to receive notifications">>). -define(SUBSCRIPTION_TYPE_LABEL, <<"Type of notification to receive">>). -define(SUBSCRIPTION_DEPTH_LABEL, <<"Depth from subscription for which to receive notifications">>). -define(SHOW_VALUE_AWAY_LABEL, <<"XMPP Show Value of Away">>). -define(SHOW_VALUE_CHAT_LABEL, <<"XMPP Show Value of Chat">>). -define(SHOW_VALUE_DND_LABEL, <<"XMPP Show Value of DND (Do Not Disturb)">>). -define(SHOW_VALUE_ONLINE_LABEL, <<"Mere Availability in XMPP (No Show Value)">>). -define(SHOW_VALUE_XA_LABEL, <<"XMPP Show Value of XA (Extended Away)">>). -define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, <<"Receive notification of new items only">>). -define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, <<"Receive notification of new nodes only">>). -define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, <<"Receive notification from direct child nodes only">>). -define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, <<"Receive notification from all descendent nodes">>). %%==================================================================== %% API %%==================================================================== init(_Host, _ServerHost, _Opts) -> ok = create_table(). subscribe_node(JID, NodeId, Options) -> case catch mnesia:sync_dirty(fun add_subscription/3, [JID, NodeId, Options]) of {'EXIT', {aborted, Error}} -> Error; {error, Error} -> {error, Error}; Result -> {result, Result} end. unsubscribe_node(JID, NodeId, SubID) -> case catch mnesia:sync_dirty(fun delete_subscription/3, [JID, NodeId, SubID]) of {'EXIT', {aborted, Error}} -> Error; {error, Error} -> {error, Error}; Result -> {result, Result} end. get_subscription(JID, NodeId, SubID) -> case catch mnesia:sync_dirty(fun read_subscription/3, [JID, NodeId, SubID]) of {'EXIT', {aborted, Error}} -> Error; {error, Error} -> {error, Error}; Result -> {result, Result} end. set_subscription(JID, NodeId, SubID, Options) -> case catch mnesia:sync_dirty(fun write_subscription/4, [JID, NodeId, SubID, Options]) of {'EXIT', {aborted, Error}} -> Error; {error, Error} -> {error, Error}; Result -> {result, Result} end. get_options_xform(Lang, Options) -> Keys = [deliver, show_values, subscription_type, subscription_depth], XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], {result, #xdata{type = form, fields = [#xdata_field{type = hidden, var = <<"FORM_TYPE">>, values = [?NS_PUBSUB_SUB_OPTIONS]}| XFields]}}. parse_options_xform(XFields) -> Opts = set_xoption(XFields, []), {result, Opts}. %%==================================================================== %% Internal functions %%==================================================================== create_table() -> case ejabberd_mnesia:create(?MODULE, pubsub_subscription, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_subscription)}, {type, set}]) of {atomic, ok} -> ok; {aborted, {already_exists, _}} -> ok; Other -> Other end. -spec add_subscription(_JID :: ljid(), _NodeId :: mod_pubsub:nodeIdx(), Options :: [] | mod_pubsub:subOptions()) -> SubId :: mod_pubsub:subId(). add_subscription(_JID, _NodeId, []) -> make_subid(); add_subscription(_JID, _NodeId, Options) -> SubID = make_subid(), mnesia:write(#pubsub_subscription{subid = SubID, options = Options}), SubID. -spec delete_subscription(_JID :: _, _NodeId :: _, SubId :: mod_pubsub:subId()) -> ok. delete_subscription(_JID, _NodeId, SubID) -> mnesia:delete({pubsub_subscription, SubID}). -spec read_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId()) -> mod_pubsub:pubsubSubscription() | {error, notfound}. read_subscription(_JID, _NodeId, SubID) -> case mnesia:read({pubsub_subscription, SubID}) of [Sub] -> Sub; _ -> {error, notfound} end. -spec write_subscription(_JID :: ljid(), _NodeId :: _, SubID :: mod_pubsub:subId(), Options :: mod_pubsub:subOptions()) -> ok. write_subscription(_JID, _NodeId, SubID, Options) -> mnesia:write(#pubsub_subscription{subid = SubID, options = Options}). -spec make_subid() -> SubId::mod_pubsub:subId(). make_subid() -> {T1, T2, T3} = erlang:timestamp(), (str:format("~.16B~.16B~.16B", [T1, T2, T3])). %% %% Subscription XForm processing. %% %% Return processed options, with types converted and so forth, using %% Opts as defaults. set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of {error, _} -> Opts; Key -> Val = val_xfield(Key, Value), lists:keystore(Key, 1, Opts, {Key, Val}) end, set_xoption(T, NewOpts). %% Return the options list's key for an XForm var. %% Convert Values for option list's Key. var_xfield(?PUBSUB_DELIVER) -> deliver; var_xfield(?PUBSUB_DIGEST) -> digest; var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; var_xfield(?PUBSUB_EXPIRE) -> expire; var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; var_xfield(_) -> {error, badarg}. val_xfield(deliver = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(digest_frequency = Opt, [Val]) -> case catch binary_to_integer(Val) of N when is_integer(N) -> N; _ -> Txt = {?T("Value of '~s' should be integer"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(expire = Opt, [Val]) -> try xmpp_util:decode_timestamp(Val) catch _:{bad_timestamp, _} -> Txt = {?T("Value of '~s' should be datetime string"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end; val_xfield(include_body = Opt, [Val]) -> xopt_to_bool(Opt, Val); val_xfield(show_values, Vals) -> Vals; val_xfield(subscription_type, [<<"items">>]) -> items; val_xfield(subscription_type, [<<"nodes">>]) -> nodes; val_xfield(subscription_depth, [<<"all">>]) -> all; val_xfield(subscription_depth = Opt, [Depth]) -> case catch binary_to_integer(Depth) of N when is_integer(N) -> N; _ -> Txt = {?T("Value of '~s' should be integer"), [Opt]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())} end. %% Convert XForm booleans to Erlang booleans. xopt_to_bool(_, <<"0">>) -> false; xopt_to_bool(_, <<"1">>) -> true; xopt_to_bool(_, <<"false">>) -> false; xopt_to_bool(_, <<"true">>) -> true; xopt_to_bool(Option, _) -> Txt = {?T("Value of '~s' should be boolean"), [Option]}, {error, xmpp:err_not_acceptable(Txt, ejabberd_option:language())}. %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. get_option_xfield(Lang, Key, Options) -> Var = xfield_var(Key), Label = xfield_label(Key), {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of {value, {_, Val}} -> [xfield_val(Key, Val)]; false -> [] end, #xdata_field{type = Type, var = Var, label = translate:translate(Lang, Label), values = Vals, options = OptEls}. type_and_options({Type, Options}, Lang) -> {Type, [tr_xfield_options(O, Lang) || O <- Options]}; type_and_options(Type, _Lang) -> {Type, []}. tr_xfield_options({Value, Label}, Lang) -> #xdata_option{label = translate:translate(Lang, Label), value = Value}. xfield_var(deliver) -> ?PUBSUB_DELIVER; %xfield_var(digest) -> ?PUBSUB_DIGEST; %xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; %xfield_var(expire) -> ?PUBSUB_EXPIRE; %xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. xfield_type(deliver) -> boolean; %xfield_type(digest) -> boolean; %xfield_type(digest_frequency) -> 'text-single'; %xfield_type(expire) -> 'text-single'; %xfield_type(include_body) -> boolean; xfield_type(show_values) -> {'list-multi', [{<<"away">>, ?SHOW_VALUE_AWAY_LABEL}, {<<"chat">>, ?SHOW_VALUE_CHAT_LABEL}, {<<"dnd">>, ?SHOW_VALUE_DND_LABEL}, {<<"online">>, ?SHOW_VALUE_ONLINE_LABEL}, {<<"xa">>, ?SHOW_VALUE_XA_LABEL}]}; xfield_type(subscription_type) -> {'list-single', [{<<"items">>, ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, {<<"nodes">>, ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; xfield_type(subscription_depth) -> {'list-single', [{<<"1">>, ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, {<<"all">>, ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. %% Return the XForm variable label for a subscription option key. xfield_label(deliver) -> ?DELIVER_LABEL; %xfield_label(digest) -> ?DIGEST_LABEL; %xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; %xfield_label(expire) -> ?EXPIRE_LABEL; %xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; xfield_label(show_values) -> ?SHOW_VALUES_LABEL; %% Return the XForm value for a subscription option key. %% Convert erlang booleans to XForms. xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest, Val) -> [bool_to_xopt(Val)]; %xfield_val(digest_frequency, Val) -> % [integer_to_binary(Val))]; %xfield_val(expire, Val) -> % [jlib:now_to_utc_string(Val)]; %xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; xfield_val(show_values, Val) -> Val; xfield_val(subscription_type, items) -> [<<"items">>]; xfield_val(subscription_type, nodes) -> [<<"nodes">>]; xfield_val(subscription_depth, all) -> [<<"all">>]; xfield_val(subscription_depth, N) -> [integer_to_binary(N)]. bool_to_xopt(true) -> <<"true">>; bool_to_xopt(false) -> <<"false">>. ejabberd-21.12/src/mod_mam.erl0000644000232200023220000014572614154362354016555 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_mam.erl %%% Author : Evgeniy Khramtsov %%% Purpose : Message Archive Management (XEP-0313) %%% Created : 4 Jul 2013 by Evgeniy Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2013-2021 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(mod_mam). -protocol({xep, 313, '0.6.1'}). -protocol({xep, 334, '0.2'}). -protocol({xep, 359, '0.5.0'}). -protocol({xep, 441, '0.2.0'}). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, depends/2, mod_doc/0]). -export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1, user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1, disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1, muc_process_iq/2, muc_filter_message/3, message_is_archived/3, delete_old_messages/2, get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/3, offline_message/1, export/1, mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2, is_empty_for_user/2, is_empty_for_room/3, check_create_room/4, process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("mod_muc_room.hrl"). -include("ejabberd_commands.hrl"). -include("mod_mam.hrl"). -include("translate.hrl"). -define(DEF_PAGE_SIZE, 50). -define(MAX_PAGE_SIZE, 250). -type c2s_state() :: ejabberd_c2s:state(). -type count() :: non_neg_integer() | undefined. -callback init(binary(), gen_mod:opts()) -> any(). -callback remove_user(binary(), binary()) -> any(). -callback remove_room(binary(), binary(), binary()) -> any(). -callback delete_old_messages(binary() | global, erlang:timestamp(), all | chat | groupchat) -> any(). -callback extended_fields() -> [mam_query:property() | #xdata_field{}]. -callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat, jid(), binary(), recv | send, integer()) -> ok | any(). -callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any(). -callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error | {error, db_failure}. -callback select(binary(), jid(), jid(), mam_query:result(), #rsm_set{} | undefined, chat | groupchat) -> {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | {error, db_failure}. -callback select(binary(), jid(), jid(), mam_query:result(), #rsm_set{} | undefined, chat | groupchat, all | only_count | only_messages) -> {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | {error, db_failure}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}. -callback is_empty_for_user(binary(), binary()) -> boolean(). -callback is_empty_for_room(binary(), binary(), binary()) -> boolean(). -callback select_with_mucsub(binary(), jid(), jid(), mam_query:result(), #rsm_set{} | undefined, all | only_count | only_messages) -> {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} | {error, db_failure}. -optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7]). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> case mod_mam_opt:db_type(Opts) of mnesia -> ?WARNING_MSG("Mnesia backend for ~ts is not recommended: " "it's limited to 2GB and often gets corrupted " "when reaching this limit. SQL backend is " "recommended. Namely, for small servers SQLite " "is a preferred choice because it's very easy " "to configure.", [?MODULE]); _ -> ok end, Mod = gen_mod:db_mod(Opts, ?MODULE), case Mod:init(Host, Opts) of ok -> init_cache(Mod, Host, Opts), register_iq_handlers(Host), ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 88), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 88), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet_strip_tag, 500), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, offline_message, 49), ejabberd_hooks:add(muc_filter_message, Host, ?MODULE, muc_filter_message, 50), ejabberd_hooks:add(muc_process_iq, Host, ?MODULE, muc_process_iq, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:add(get_room_config, Host, ?MODULE, get_room_config, 50), ejabberd_hooks:add(set_room_option, Host, ?MODULE, set_room_option, 50), ejabberd_hooks:add(store_mam_message, Host, ?MODULE, store_mam_message, 100), case mod_mam_opt:assume_mam_usage(Opts) of true -> ejabberd_hooks:add(message_is_archived, Host, ?MODULE, message_is_archived, 50); false -> ok end, case mod_mam_opt:clear_archive_on_room_destroy(Opts) of true -> ejabberd_hooks:add(remove_room, Host, ?MODULE, remove_room, 50); false -> ejabberd_hooks:add(check_create_room, Host, ?MODULE, check_create_room, 50) end, ejabberd_commands:register_commands(?MODULE, get_commands_spec()), ok; Err -> Err end. use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 2) of true -> Mod:use_cache(Host); false -> mod_mam_opt:use_cache(Host) end. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> ets_cache:new(archive_prefs_cache, cache_opts(Opts)); false -> ets_cache:delete(archive_prefs_cache) end. cache_opts(Opts) -> MaxSize = mod_mam_opt:cache_size(Opts), CacheMissed = mod_mam_opt:cache_missed(Opts), LifeTime = mod_mam_opt:cache_life_time(Opts), [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}]. stop(Host) -> unregister_iq_handlers(Host), ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE, sm_receive_packet, 50), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 88), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 88), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet_strip_tag, 500), ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, offline_message, 49), ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE, muc_filter_message, 50), ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE, muc_process_iq, 50), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50), ejabberd_hooks:delete(get_room_config, Host, ?MODULE, get_room_config, 50), ejabberd_hooks:delete(set_room_option, Host, ?MODULE, set_room_option, 50), ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, store_mam_message, 100), case mod_mam_opt:assume_mam_usage(Host) of true -> ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, message_is_archived, 50); false -> ok end, case mod_mam_opt:clear_archive_on_room_destroy(Host) of true -> ejabberd_hooks:delete(remove_room, Host, ?MODULE, remove_room, 50); false -> ejabberd_hooks:delete(check_create_room, Host, ?MODULE, check_create_room, 50) end, case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts), case {mod_mam_opt:assume_mam_usage(NewOpts), mod_mam_opt:assume_mam_usage(OldOpts)} of {true, false} -> ejabberd_hooks:add(message_is_archived, Host, ?MODULE, message_is_archived, 50); {false, true} -> ejabberd_hooks:delete(message_is_archived, Host, ?MODULE, message_is_archived, 50); _ -> ok end. depends(_Host, _Opts) -> []. -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP, ?MODULE, process_iq_v0_2), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP, ?MODULE, process_iq_v0_2), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_0, ?MODULE, process_iq_v0_3), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_0, ?MODULE, process_iq_v0_3), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1, ?MODULE, process_iq_v0_3), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1, ?MODULE, process_iq_v0_3), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2, ?MODULE, process_iq_v0_3), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, ?MODULE, process_iq_v0_3). -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). -spec remove_user(binary(), binary()) -> ok. remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_user(LUser, LServer), case use_cache(Mod, LServer) of true -> ets_cache:delete(archive_prefs_cache, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end. -spec remove_room(binary(), binary(), binary()) -> ok. remove_room(LServer, Name, Host) -> LName = jid:nodeprep(Name), LHost = jid:nameprep(Host), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:remove_room(LServer, LName, LHost), ok. -spec remove_mam_for_user(binary(), binary()) -> {ok, binary()} | {error, binary()}. remove_mam_for_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:remove_from_archive(LUser, LServer, none) of ok -> {ok, <<"MAM archive removed">>}; {error, Bin} when is_binary(Bin) -> {error, Bin}; {error, _} -> {error, <<"Db returned error">>} end. -spec remove_mam_for_user_with_peer(binary(), binary(), binary()) -> {ok, binary()} | {error, binary()}. remove_mam_for_user_with_peer(User, Server, Peer) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), try jid:decode(Peer) of Jid -> Mod = get_module_host(LServer), case Mod:remove_from_archive(LUser, LServer, Jid) of ok -> {ok, <<"MAM archive removed">>}; {error, Bin} when is_binary(Bin) -> {error, Bin}; {error, _} -> {error, <<"Db returned error">>} end catch _:_ -> {error, <<"Invalid peer JID">>} end. get_module_host(LServer) -> try gen_mod:db_mod(LServer, ?MODULE) catch error:{module_not_loaded, ?MODULE, LServer} -> gen_mod:db_mod(ejabberd_router:host_of_route(LServer), ?MODULE) end. -spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(), jid(), binary()) -> [muc_roomconfig:property()]. get_room_config(Fields, RoomState, _From, _Lang) -> Config = RoomState#state.config, Fields ++ [{mam, Config#config.mam}]. -spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary()) -> {pos_integer(), _}. set_room_option(_Acc, {mam, Val}, _Lang) -> {#config.mam, Val}; set_room_option(Acc, _Property, _Lang) -> Acc. -spec sm_receive_packet(stanza()) -> stanza(). sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) -> init_stanza_id(Pkt, LServer); sm_receive_packet(Acc) -> Acc. -spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, Pkt1 = case should_archive(Pkt, LServer) of true -> case store_msg(Pkt, LUser, LServer, Peer, recv) of ok -> mark_stored_msg(Pkt, JID); _ -> Pkt end; _ -> Pkt end, {Pkt1, C2SState}; user_receive_packet(Acc) -> Acc. -spec user_send_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) -> LUser = JID#jid.luser, LServer = JID#jid.lserver, Pkt1 = init_stanza_id(Pkt, LServer), Pkt2 = case should_archive(Pkt1, LServer) of true -> case store_msg(xmpp:set_from_to(Pkt1, JID, Peer), LUser, LServer, Peer, send) of ok -> mark_stored_msg(Pkt1, JID); _ -> Pkt1 end; false -> Pkt1 end, {Pkt2, C2SState}; user_send_packet(Acc) -> Acc. -spec user_send_packet_strip_tag({stanza(), c2s_state()}) -> {stanza(), c2s_state()}. user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) -> LServer = JID#jid.lserver, Pkt1 = xmpp:del_meta(Pkt, stanza_id), Pkt2 = strip_my_stanza_id(Pkt1, LServer), {Pkt2, C2SState}; user_send_packet_strip_tag(Acc) -> Acc. -spec offline_message({any(), message()}) -> {any(), message()}. offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) -> LUser = To#jid.luser, LServer = To#jid.lserver, case should_archive(Pkt, LServer) of true -> case store_msg(Pkt, LUser, LServer, Peer, recv) of ok -> {archived, mark_stored_msg(Pkt, To)}; _ -> Acc end; false -> Acc end. -spec muc_filter_message(message(), mod_muc_room:state(), binary()) -> message(). muc_filter_message(#message{from = From} = Pkt, #state{config = Config, jid = RoomJID} = MUCState, FromNick) -> LServer = RoomJID#jid.lserver, Pkt1 = init_stanza_id(Pkt, LServer), if Config#config.mam -> StorePkt = strip_x_jid_tags(Pkt1), case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of ok -> mark_stored_msg(Pkt1, RoomJID); _ -> Pkt1 end; true -> Pkt1 end; muc_filter_message(Acc, _MUCState, _FromNick) -> Acc. -spec make_id() -> integer(). make_id() -> erlang:system_time(microsecond). -spec get_stanza_id(stanza()) -> integer(). get_stanza_id(#message{meta = #{stanza_id := ID}}) -> ID. -spec init_stanza_id(stanza(), binary()) -> stanza(). init_stanza_id(#message{meta = #{stanza_id := _ID}} = Pkt, _LServer) -> Pkt; init_stanza_id(#message{meta = #{from_offline := true}} = Pkt, _LServer) -> Pkt; init_stanza_id(Pkt, LServer) -> ID = make_id(), Pkt1 = strip_my_stanza_id(Pkt, LServer), xmpp:put_meta(Pkt1, stanza_id, ID). -spec set_stanza_id(stanza(), jid(), binary()) -> stanza(). set_stanza_id(Pkt, JID, ID) -> BareJID = jid:remove_resource(JID), Archived = #mam_archived{by = BareJID, id = ID}, StanzaID = #stanza_id{by = BareJID, id = ID}, NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)], xmpp:set_els(Pkt, NewEls). -spec mark_stored_msg(message(), jid()) -> message(). mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) -> Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)), xmpp:put_meta(Pkt1, mam_archived, true). % Query archive v0.2 process_iq_v0_2(#iq{from = #jid{lserver = LServer}, to = #jid{lserver = LServer}, type = get, sub_els = [#mam_query{}]} = IQ) -> process_iq(LServer, IQ, chat); process_iq_v0_2(IQ) -> process_iq(IQ). % Query archive v0.3 process_iq_v0_3(#iq{from = #jid{lserver = LServer}, to = #jid{lserver = LServer}, type = set, sub_els = [#mam_query{}]} = IQ) -> process_iq(LServer, IQ, chat); process_iq_v0_3(#iq{from = #jid{lserver = LServer}, to = #jid{lserver = LServer}, type = get, sub_els = [#mam_query{}]} = IQ) -> process_iq(LServer, IQ); process_iq_v0_3(IQ) -> process_iq(IQ). -spec muc_process_iq(ignore | iq(), mod_muc_room:state()) -> ignore | iq(). muc_process_iq(#iq{type = T, lang = Lang, from = From, sub_els = [#mam_query{xmlns = NS}]} = IQ, MUCState) when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse (T == get andalso NS == ?NS_MAM_TMP) -> case may_enter_room(From, MUCState) of true -> LServer = MUCState#state.server_host, Role = mod_muc_room:get_role(From, MUCState), process_iq(LServer, IQ, {groupchat, Role, MUCState}); false -> Text = ?T("Only members may query archives of this room"), xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang)) end; muc_process_iq(#iq{type = get, sub_els = [#mam_query{xmlns = NS}]} = IQ, MUCState) when NS /= ?NS_MAM_TMP -> LServer = MUCState#state.server_host, process_iq(LServer, IQ); muc_process_iq(IQ, _MUCState) -> IQ. parse_query(#mam_query{xmlns = ?NS_MAM_TMP, start = Start, 'end' = End, with = With, withtext = Text}, _Lang) -> {ok, [{start, Start}, {'end', End}, {with, With}, {withtext, Text}]}; parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) -> X = xmpp_util:set_xdata_field( #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [?NS_MAM_1]}, Query#mam_query.xdata), try mam_query:decode(X#xdata.fields) of Form -> {ok, Form} catch _:{mam_query, Why} -> Txt = mam_query:format_error(Why), {error, xmpp:err_bad_request(Txt, Lang)} end; parse_query(#mam_query{}, _Lang) -> {ok, []}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures}, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, <<"">>, _Lang) -> {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0 | OtherFeatures]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. -spec message_is_archived(boolean(), c2s_state(), message()) -> boolean(). message_is_archived(true, _C2SState, _Pkt) -> true; message_is_archived(false, #{lserver := LServer}, Pkt) -> case mod_mam_opt:assume_mam_usage(LServer) of true -> is_archived(Pkt, LServer); false -> false end. delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>; TypeBin == <<"groupchat">>; TypeBin == <<"all">> -> CurrentTime = make_id(), Diff = Days * 24 * 60 * 60 * 1000000, TimeStamp = misc:usec_to_now(CurrentTime - Diff), Type = misc:binary_to_atom(TypeBin), DBTypes = lists:usort( lists:map( fun(Host) -> case mod_mam_opt:db_type(Host) of sql -> {sql, Host}; Other -> {Other, global} end end, ejabberd_option:hosts())), Results = lists:map( fun({DBType, ServerHost}) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:delete_old_messages(ServerHost, TimeStamp, Type) end, DBTypes), case lists:filter(fun(Res) -> Res /= ok end, Results) of [] -> ok; [NotOk|_] -> NotOk end; delete_old_messages(_TypeBin, _Days) -> unsupported_type. export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). -spec is_empty_for_user(binary(), binary()) -> boolean(). is_empty_for_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:is_empty_for_user(LUser, LServer). -spec is_empty_for_room(binary(), binary(), binary()) -> boolean(). is_empty_for_room(LServer, Name, Host) -> LName = jid:nodeprep(Name), LHost = jid:nameprep(Host), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:is_empty_for_room(LServer, LName, LHost). -spec check_create_room(boolean(), binary(), binary(), binary()) -> boolean(). check_create_room(Acc, ServerHost, RoomID, Host) -> Acc and is_empty_for_room(ServerHost, RoomID, Host). %%%=================================================================== %%% Internal functions %%%=================================================================== process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) -> Mod = gen_mod:db_mod(LServer, ?MODULE), CommonFields = [{with, undefined}, {start, undefined}, {'end', undefined}], ExtendedFields = Mod:extended_fields(), Fields = mam_query:encode(CommonFields ++ ExtendedFields), X = xmpp_util:set_xdata_field( #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]}, #xdata{type = form, fields = Fields}), xmpp:make_iq_result(IQ, #mam_query{xmlns = NS, xdata = X}). % Preference setting (both v0.2 & v0.3) process_iq(#iq{type = set, lang = Lang, sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) -> Why = {missing_attr, <<"default">>, <<"prefs">>, NS}, ErrTxt = xmpp:io_format_error(Why), xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang)); process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, to = #jid{lserver = LServer}, type = set, lang = Lang, sub_els = [#mam_prefs{xmlns = NS, default = Default, always = Always0, never = Never0}]} = IQ) -> Access = mod_mam_opt:access_preferences(LServer), case acl:match_rule(LServer, Access, jid:make(LUser, LServer)) of allow -> Always = lists:usort(get_jids(Always0)), Never = lists:usort(get_jids(Never0)), case write_prefs(LUser, LServer, LServer, Default, Always, Never) of ok -> NewPrefs = prefs_el(Default, Always, Never, NS), xmpp:make_iq_result(IQ, NewPrefs); _Err -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; deny -> Txt = ?T("MAM preference modification denied by service policy"), xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang)) end; process_iq(#iq{from = #jid{luser = LUser, lserver = LServer}, to = #jid{lserver = LServer}, lang = Lang, type = get, sub_els = [#mam_prefs{xmlns = NS}]} = IQ) -> case get_prefs(LUser, LServer) of {ok, Prefs} -> PrefsEl = prefs_el(Prefs#archive_prefs.default, Prefs#archive_prefs.always, Prefs#archive_prefs.never, NS), xmpp:make_iq_result(IQ, PrefsEl); {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end; process_iq(IQ) -> xmpp:make_error(IQ, xmpp:err_not_allowed()). process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang, sub_els = [SubEl]} = IQ, MsgType) -> Ret = case MsgType of chat -> maybe_activate_mam(LUser, LServer); _ -> ok end, case Ret of ok -> case SubEl of #mam_query{rsm = #rsm_set{index = I}} when is_integer(I) -> Txt = ?T("Unsupported element"), xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); #mam_query{rsm = RSM, flippage = FlipPage, xmlns = NS} -> case parse_query(SubEl, Lang) of {ok, Query} -> NewRSM = limit_max(RSM, NS), select_and_send(LServer, Query, NewRSM, FlipPage, IQ, MsgType); {error, Err} -> xmpp:make_error(IQ, Err) end end; {error, _} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec should_archive(message(), binary()) -> boolean(). should_archive(#message{type = error}, _LServer) -> false; should_archive(#message{type = groupchat}, _LServer) -> false; should_archive(#message{meta = #{from_offline := true}}, _LServer) -> false; should_archive(#message{body = Body, subject = Subject, type = Type} = Pkt, LServer) -> case is_archived(Pkt, LServer) of true -> false; false -> case check_store_hint(Pkt) of store -> true; no_store -> false; none when Type == headline -> false; none -> case xmpp:get_text(Body) /= <<>> orelse xmpp:get_text(Subject) /= <<>> of true -> true; _ -> case misc:unwrap_mucsub_message(Pkt) of #message{type = groupchat} = Msg -> should_archive(Msg#message{type = chat}, LServer); #message{} = Msg -> should_archive(Msg, LServer); _ -> false end end end end; should_archive(_, _LServer) -> false. -spec strip_my_stanza_id(stanza(), binary()) -> stanza(). strip_my_stanza_id(Pkt, LServer) -> Els = xmpp:get_els(Pkt), NewEls = lists:filter( fun(El) -> Name = xmpp:get_name(El), NS = xmpp:get_ns(El), if (Name == <<"archived">> andalso NS == ?NS_MAM_TMP); (Name == <<"stanza-id">> andalso NS == ?NS_SID_0) -> try xmpp:decode(El) of #mam_archived{by = By} -> By#jid.lserver /= LServer; #stanza_id{by = By} -> By#jid.lserver /= LServer catch _:{xmpp_codec, _} -> false end; true -> true end end, Els), xmpp:set_els(Pkt, NewEls). -spec strip_x_jid_tags(stanza()) -> stanza(). strip_x_jid_tags(Pkt) -> Els = xmpp:get_els(Pkt), NewEls = lists:filter( fun(El) -> case xmpp:get_name(El) of <<"x">> -> NS = xmpp:get_ns(El), Items = if NS == ?NS_MUC_USER; NS == ?NS_MUC_ADMIN; NS == ?NS_MUC_OWNER -> try xmpp:decode(El) of #muc_user{items = Is} -> Is; #muc_admin{items = Is} -> Is; #muc_owner{items = Is} -> Is catch _:{xmpp_codec, _} -> [] end; true -> [] end, not lists:any( fun(#muc_item{jid = JID}) -> JID /= undefined end, Items); _ -> true end end, Els), xmpp:set_els(Pkt, NewEls). -spec should_archive_peer(binary(), binary(), #archive_prefs{}, jid()) -> boolean(). should_archive_peer(LUser, LServer, #archive_prefs{default = Default, always = Always, never = Never}, Peer) -> LPeer = jid:remove_resource(jid:tolower(Peer)), case lists:member(LPeer, Always) of true -> true; false -> case lists:member(LPeer, Never) of true -> false; false -> case Default of always -> true; never -> false; roster -> {Sub, _, _} = ejabberd_hooks:run_fold( roster_get_jid_info, LServer, {none, none, []}, [LUser, LServer, Peer]), Sub == both orelse Sub == from orelse Sub == to end end end. -spec should_archive_muc(message()) -> boolean(). should_archive_muc(#message{type = groupchat, body = Body, subject = Subj} = Pkt) -> case check_store_hint(Pkt) of store -> true; no_store -> false; none -> case xmpp:get_text(Body) of <<"">> -> case xmpp:get_text(Subj) of <<"">> -> false; _ -> true end; _ -> true end end; should_archive_muc(_) -> false. -spec check_store_hint(message()) -> store | no_store | none. check_store_hint(Pkt) -> case has_store_hint(Pkt) of true -> store; false -> case has_no_store_hint(Pkt) of true -> no_store; false -> none end end. -spec has_store_hint(message()) -> boolean(). has_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'store'}). -spec has_no_store_hint(message()) -> boolean(). has_no_store_hint(Message) -> xmpp:has_subtag(Message, #hint{type = 'no-store'}) orelse xmpp:has_subtag(Message, #hint{type = 'no-storage'}) orelse xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}). -spec is_archived(message(), binary()) -> boolean(). is_archived(Pkt, LServer) -> case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of #stanza_id{by = #jid{lserver = LServer}} -> true; _ -> false end. -spec may_enter_room(jid(), mod_muc_room:state()) -> boolean(). may_enter_room(From, #state{config = #config{members_only = false}} = MUCState) -> mod_muc_room:get_affiliation(From, MUCState) /= outcast; may_enter_room(From, MUCState) -> mod_muc_room:is_occupant_or_admin(From, MUCState). -spec store_msg(message(), binary(), binary(), jid(), send | recv) -> ok | pass | any(). store_msg(Pkt, LUser, LServer, Peer, Dir) -> case get_prefs(LUser, LServer) of {ok, Prefs} -> UseMucArchive = mod_mam_opt:user_mucsub_from_muc_archive(LServer), StoredInMucMam = UseMucArchive andalso xmpp:get_meta(Pkt, in_muc_mam, false), case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt, StoredInMucMam} of {true, #message{meta = #{sm_copy := true}}, _} -> ok; % Already stored. {true, _, true} -> ok; % Stored in muc archive. {true, _, _} -> case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, [LUser, LServer, Peer, <<"">>, chat, Dir]) of #message{} -> ok; _ -> pass end; {false, _, _} -> pass end; {error, _} -> pass end. -spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary()) -> ok | pass | any(). store_muc(MUCState, Pkt, RoomJID, Peer, Nick) -> case should_archive_muc(Pkt) of true -> {U, S, _} = jid:tolower(RoomJID), LServer = MUCState#state.server_host, case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt, [U, S, Peer, Nick, groupchat, recv]) of #message{} -> ok; _ -> pass end; false -> pass end. store_mam_message(Pkt, U, S, Peer, Nick, Type, Dir) -> LServer = ejabberd_router:host_of_route(S), US = {U, S}, ID = get_stanza_id(Pkt), El = xmpp:encode(Pkt), Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:store(El, LServer, US, Type, Peer, Nick, Dir, ID), Pkt. write_prefs(LUser, LServer, Host, Default, Always, Never) -> Prefs = #archive_prefs{us = {LUser, LServer}, default = Default, always = Always, never = Never}, Mod = gen_mod:db_mod(Host, ?MODULE), case Mod:write_prefs(LUser, LServer, Prefs, Host) of ok -> case use_cache(Mod, LServer) of true -> ets_cache:delete(archive_prefs_cache, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end; _Err -> {error, db_failure} end. get_prefs(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup(archive_prefs_cache, {LUser, LServer}, fun() -> Mod:get_prefs(LUser, LServer) end); false -> Mod:get_prefs(LUser, LServer) end, case Res of {ok, Prefs} -> {ok, Prefs}; {error, _} -> {error, db_failure}; error -> ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), case ActivateOpt of true -> {ok, #archive_prefs{us = {LUser, LServer}, default = never}}; false -> Default = mod_mam_opt:default(LServer), {ok, #archive_prefs{us = {LUser, LServer}, default = Default}} end end. prefs_el(Default, Always, Never, NS) -> #mam_prefs{default = Default, always = [jid:make(LJ) || LJ <- Always], never = [jid:make(LJ) || LJ <- Never], xmlns = NS}. maybe_activate_mam(LUser, LServer) -> ActivateOpt = mod_mam_opt:request_activates_archiving(LServer), case ActivateOpt of true -> Mod = gen_mod:db_mod(LServer, ?MODULE), Res = case use_cache(Mod, LServer) of true -> ets_cache:lookup(archive_prefs_cache, {LUser, LServer}, fun() -> Mod:get_prefs(LUser, LServer) end); false -> Mod:get_prefs(LUser, LServer) end, case Res of {ok, _Prefs} -> ok; {error, _} -> {error, db_failure}; error -> Default = mod_mam_opt:default(LServer), write_prefs(LUser, LServer, LServer, Default, [], []) end; false -> ok end. select_and_send(LServer, Query, RSM, FlipPage, #iq{from = From, to = To} = IQ, MsgType) -> Ret = case MsgType of chat -> select(LServer, From, From, Query, RSM, MsgType); _ -> select(LServer, From, To, Query, RSM, MsgType) end, case Ret of {Msgs, IsComplete, Count} -> SortedMsgs = lists:keysort(2, Msgs), SortedMsgs2 = case FlipPage of true -> lists:reverse(SortedMsgs); false -> SortedMsgs end, send(SortedMsgs2, Count, IsComplete, IQ); {error, _} -> Txt = ?T("Database failure"), Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang), xmpp:make_error(IQ, Err) end. select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) -> select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, all). select(_LServer, JidRequestor, JidArchive, Query, RSM, {groupchat, _Role, #state{config = #config{mam = false}, history = History}} = MsgType, _Flags) -> Start = proplists:get_value(start, Query), End = proplists:get_value('end', Query), #lqueue{queue = Q} = History, L = p1_queue:len(Q), Msgs = lists:flatmap( fun({Nick, Pkt, _HaveSubject, Now, _Size}) -> TS = misc:now_to_usec(Now), case match_interval(Now, Start, End) and match_rsm(Now, RSM) of true -> case msg_to_el(#archive_msg{ id = integer_to_binary(TS), type = groupchat, timestamp = Now, peer = undefined, nick = Nick, packet = Pkt}, MsgType, JidRequestor, JidArchive) of {ok, Msg} -> [{integer_to_binary(TS), TS, Msg}]; {error, _} -> [] end; false -> [] end end, p1_queue:to_list(Q)), case RSM of #rsm_set{max = Max, before = Before} when is_binary(Before) -> {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max), {NewMsgs, IsComplete, L}; #rsm_set{max = Max} -> {NewMsgs, IsComplete} = filter_by_max(Msgs, Max), {NewMsgs, IsComplete, L}; _ -> {Msgs, true, L} end; select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> case might_expose_jid(Query, MsgType) of true -> {[], true, 0}; false -> case {MsgType, mod_mam_opt:user_mucsub_from_muc_archive(LServer)} of {chat, true} -> select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); _ -> db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) end end. select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> MucHosts = mod_muc_admin:find_hosts(LServer), Mod = gen_mod:db_mod(LServer, ?MODULE), case proplists:get_value(with, Query) of #jid{lserver = WithLServer} = MucJid -> case lists:member(WithLServer, MucHosts) of true -> select(LServer, JidRequestor, MucJid, Query, RSM, {groupchat, member, #state{config = #config{mam = true}}}); _ -> db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) end; _ -> case erlang:function_exported(Mod, select_with_mucsub, 6) of true -> Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags); false -> select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) end end. select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) -> case db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) of {error, _} = Err -> Err; {Entries, All, Count} -> {Dir, Max} = case RSM of #rsm_set{max = M, before = V} when is_binary(V) -> {desc, M}; #rsm_set{max = M} -> {asc, M}; _ -> {asc, undefined} end, SubRooms = case mod_muc_admin:find_hosts(LServer) of [First|_] -> case mod_muc:get_subscribed_rooms(First, JidRequestor) of {ok, L} -> L; {error, _} -> [] end; _ -> [] end, SubRoomJids = [Jid || {Jid, _, _} <- SubRooms], {E2, A2, C2} = lists:foldl( fun(MucJid, {E0, A0, C0}) -> case select(LServer, JidRequestor, MucJid, Query, RSM, {groupchat, member, #state{config = #config{mam = true}}}) of {error, _} -> {E0, A0, C0}; {E, A, C} -> {lists:keymerge(2, E0, wrap_as_mucsub(E, JidRequestor)), A0 andalso A, C0 + C} end end, {Entries, All, Count}, SubRoomJids), case {Dir, Max} of {_, undefined} -> {E2, A2, C2}; {desc, _} -> Start = case length(E2) of Len when Len < Max -> 1; Len -> Len - Max + 1 end, Sub = lists:sublist(E2, Start, Max), {Sub, if Sub == E2 -> A2; true -> false end, C2}; _ -> Sub = lists:sublist(E2, 1, Max), {Sub, if Sub == E2 -> A2; true -> false end, C2} end end. db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case erlang:function_exported(Mod, select, 7) of true -> Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags); _ -> Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) end. wrap_as_mucsub(Messages, #jid{lserver = LServer} = Requester) -> ReqBare = jid:remove_resource(Requester), ReqServer = jid:make(<<>>, LServer, <<>>), [{T1, T2, wrap_as_mucsub(M, ReqBare, ReqServer)} || {T1, T2, M} <- Messages]. wrap_as_mucsub(Message, Requester, ReqServer) -> case Message of #forwarded{delay = #delay{stamp = Stamp, desc = Desc}, sub_els = [#message{from = From, sub_els = SubEls, subject = Subject} = Msg]} -> {L1, SubEls2} = case lists:keytake(mam_archived, 1, SubEls) of {value, Arch, Rest} -> {[Arch#mam_archived{by = Requester}], Rest}; _ -> {[], SubEls} end, {Sid, L2, SubEls3} = case lists:keytake(stanza_id, 1, SubEls2) of {value, #stanza_id{id = Sid0} = SID, Rest2} -> {Sid0, [SID#stanza_id{by = Requester} | L1], Rest2}; _ -> {p1_rand:get_string(), L1, SubEls2} end, Msg2 = Msg#message{to = Requester, sub_els = SubEls3}, Node = case Subject of [] -> ?NS_MUCSUB_NODES_MESSAGES; _ -> ?NS_MUCSUB_NODES_SUBJECT end, #forwarded{delay = #delay{stamp = Stamp, desc = Desc, from = ReqServer}, sub_els = [ #message{from = jid:remove_resource(From), to = Requester, id = Sid, sub_els = [#ps_event{ items = #ps_items{ node = Node, items = [#ps_item{ id = Sid, sub_els = [Msg2] }]}} | L2]}]}; _ -> Message end. msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick, peer = Peer, id = ID}, MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of Pkt1 -> Pkt2 = case MsgType of chat -> set_stanza_id(Pkt1, JidArchive, ID); {groupchat, _, _} -> set_stanza_id(Pkt1, JidArchive, ID); _ -> Pkt1 end, Pkt3 = maybe_update_from_to( Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick), Delay = #delay{stamp = TS, from = jid:make(LServer)}, {ok, #forwarded{sub_els = [Pkt3], delay = Delay}} catch _:{xmpp_codec, Why} -> ?ERROR_MSG("Failed to decode raw element ~p from message " "archive of user ~ts: ~ts", [El, jid:encode(JidArchive), xmpp:format_error(Why)]), {error, invalid_xml} end. maybe_update_from_to(#message{sub_els = Els} = Pkt, JidRequestor, JidArchive, Peer, {groupchat, Role, #state{config = #config{anonymous = Anon}}}, Nick) -> ExposeJID = case {Peer, JidRequestor} of {undefined, _JidRequestor} -> false; {{U, S, _R}, #jid{luser = U, lserver = S}} -> true; {_Peer, _JidRequestor} when not Anon; Role == moderator -> true; {_Peer, _JidRequestor} -> false end, Items = case ExposeJID of true -> [#muc_user{items = [#muc_item{jid = Peer}]}]; false -> [] end, Pkt#message{from = jid:replace_resource(JidArchive, Nick), to = undefined, sub_els = Items ++ Els}; maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, _MsgType, _Nick) -> Pkt. -spec send([{binary(), integer(), xmlel()}], count(), boolean(), iq()) -> iq() | ignore. send(Msgs, Count, IsComplete, #iq{from = From, to = To, sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) -> Hint = #hint{type = 'no-store'}, Els = lists:map( fun({ID, _IDInt, El}) -> #message{from = To, to = From, sub_els = [#mam_result{xmlns = NS, id = ID, queryid = QID, sub_els = [El]}]} end, Msgs), RSMOut = make_rsm_out(Msgs, Count), Result = if NS == ?NS_MAM_TMP -> #mam_query{xmlns = NS, id = QID, rsm = RSMOut}; NS == ?NS_MAM_0 -> #mam_fin{xmlns = NS, id = QID, rsm = RSMOut, complete = IsComplete}; true -> #mam_fin{xmlns = NS, rsm = RSMOut, complete = IsComplete} end, if NS /= ?NS_MAM_0 -> lists:foreach( fun(El) -> ejabberd_router:route(El) end, Els), xmpp:make_iq_result(IQ, Result); true -> ejabberd_router:route(xmpp:make_iq_result(IQ)), lists:foreach( fun(El) -> ejabberd_router:route(El) end, Els), ejabberd_router:route( #message{from = To, to = From, sub_els = [Result, Hint]}), ignore end. -spec make_rsm_out([{binary(), integer(), xmlel()}], count()) -> rsm_set(). make_rsm_out([], Count) -> #rsm_set{count = Count}; make_rsm_out([{FirstID, _, _}|_] = Msgs, Count) -> {LastID, _, _} = lists:last(Msgs), #rsm_set{first = #rsm_first{data = FirstID}, last = LastID, count = Count}. filter_by_max(Msgs, undefined) -> {Msgs, true}; filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 -> {lists:sublist(Msgs, Len), length(Msgs) =< Len}; filter_by_max(_Msgs, _Junk) -> {[], true}. -spec limit_max(rsm_set(), binary()) -> rsm_set() | undefined. limit_max(RSM, ?NS_MAM_TMP) -> RSM; % XEP-0313 v0.2 doesn't require clients to support RSM. limit_max(undefined, _NS) -> #rsm_set{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) -> RSM#rsm_set{max = ?DEF_PAGE_SIZE}; limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE -> RSM#rsm_set{max = ?MAX_PAGE_SIZE}; limit_max(RSM, _NS) -> RSM. match_interval(Now, Start, undefined) -> Now >= Start; match_interval(Now, Start, End) -> (Now >= Start) and (Now =< End). match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> -> Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), Now > Now1; match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> -> Now1 = (catch misc:usec_to_now(binary_to_integer(ID))), Now < Now1; match_rsm(_Now, _) -> true. might_expose_jid(Query, {groupchat, Role, #state{config = #config{anonymous = true}}}) when Role /= moderator -> proplists:is_defined(with, Query); might_expose_jid(_Query, _MsgType) -> false. get_jids(undefined) -> []; get_jids(Js) -> [jid:tolower(jid:remove_resource(J)) || J <- Js]. get_commands_spec() -> [#ejabberd_commands{name = delete_old_mam_messages, tags = [purge], desc = "Delete MAM messages older than DAYS", longdesc = "Valid message TYPEs: " "\"chat\", \"groupchat\", \"all\".", module = ?MODULE, function = delete_old_messages, args_desc = ["Type of messages to delete (chat, groupchat, all)", "Days to keep messages"], args_example = [<<"all">>, 31], args = [{type, binary}, {days, integer}], result = {res, rescode}}, #ejabberd_commands{name = remove_mam_for_user, tags = [mam], desc = "Remove mam archive for user", module = ?MODULE, function = remove_mam_for_user, args = [{user, binary}, {host, binary}], args_rename = [{server, host}], args_desc = ["Username", "Server"], args_example = [<<"bob">>, <<"example.com">>], result = {res, restuple}, result_desc = "Result tuple", result_example = {ok, <<"MAM archive removed">>}}, #ejabberd_commands{name = remove_mam_for_user_with_peer, tags = [mam], desc = "Remove mam archive for user with peer", module = ?MODULE, function = remove_mam_for_user_with_peer, args = [{user, binary}, {host, binary}, {with, binary}], args_rename = [{server, host}], args_desc = ["Username", "Server", "Peer"], args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>], result = {res, restuple}, result_desc = "Result tuple", result_example = {ok, <<"MAM archive removed">>}} ]. mod_opt_type(compress_xml) -> econf:bool(); mod_opt_type(assume_mam_usage) -> econf:bool(); mod_opt_type(default) -> econf:enum([always, never, roster]); mod_opt_type(request_activates_archiving) -> econf:bool(); mod_opt_type(clear_archive_on_room_destroy) -> econf:bool(); mod_opt_type(user_mucsub_from_muc_archive) -> econf:bool(); mod_opt_type(access_preferences) -> econf:acl(); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{assume_mam_usage, false}, {default, never}, {request_activates_archiving, false}, {compress_xml, false}, {clear_archive_on_room_destroy, true}, {access_preferences, all}, {user_mucsub_from_muc_archive, false}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module implements " "https://xmpp.org/extensions/xep-0313.html" "[XEP-0313: Message Archive Management]. " "Compatible XMPP clients can use it to store their " "chat history on the server."), opts => [{access_preferences, #{value => ?T("AccessName"), desc => ?T("This access rule defines who is allowed to modify the " "MAM preferences. The default value is 'all'.")}}, {assume_mam_usage, #{value => "true | false", desc => ?T("This option determines how ejabberd's " "stream management code (see _`mod_stream_mgmt`_) " "handles unacknowledged messages when the " "connection is lost. Usually, such messages are " "either bounced or resent. However, neither is " "done for messages that were stored in the user's " "MAM archive if this option is set to 'true'. In " "this case, ejabberd assumes those messages will " "be retrieved from the archive. " "The default value is 'false'.")}}, {default, #{value => "always | never | roster", desc => ?T("The option defines default policy for chat history. " "When 'always' is set every chat message is stored. " "With 'roster' only chat history with contacts from " "user's roster is stored. And 'never' fully disables " "chat history. Note that a client can change its " "policy via protocol commands. " "The default value is 'never'.")}}, {request_activates_archiving, #{value => "true | false", desc => ?T("If the value is 'true', no messages are stored " "for a user until their client issue a MAM request, " "regardless of the value of the 'default' option. " "Once the server received a request, that user's " "messages are archived as usual. " "The default value is 'false'.")}}, {compress_xml, #{value => "true | false", desc => ?T("When enabled, new messages added to archives are " "compressed using a custom compression algorithm. " "This feature works only with SQL backends. " "The default value is 'false'.")}}, {clear_archive_on_room_destroy, #{value => "true | false", desc => ?T("Whether to destroy message archive of a room " "(see _`mod_muc`_) when it gets destroyed. " "The default value is 'true'.")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}, {user_mucsub_from_muc_archive, #{value => "true | false", desc => ?T("When this option is disabled, for each individual " "subscriber a separa mucsub message is stored. With this " "option enabled, when a user fetches archive virtual " "mucsub, messages are generated from muc archives. " "The default value is 'false'.")}}]}. ejabberd-21.12/src/mod_caps.erl0000644000232200023220000005135014154362354016716 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_caps.erl %%% Author : Magnus Henoch %%% Purpose : Request and cache Entity Capabilities (XEP-0115) %%% Created : 7 Oct 2006 by Magnus Henoch %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%% 2009, improvements from ProcessOne to support correct PEP handling %%% through s2s, use less memory, and speedup global caps handling %%%---------------------------------------------------------------------- -module(mod_caps). -author('henoch@dtek.chalmers.se'). -protocol({xep, 115, '1.5'}). -behaviour(gen_server). -behaviour(gen_mod). -export([read_caps/1, list_features/1, caps_stream_features/2, disco_features/5, disco_identity/5, disco_info/5, get_features/2, export/1, import_info/0, import/5, get_user_caps/2, import_start/2, import_stop/2, compute_disco_hash/2, is_valid_node/1]). %% gen_mod callbacks -export([start/2, stop/1, reload/3, depends/2]). %% gen_server callbacks -export([init/1, handle_info/2, handle_call/3, handle_cast/2, terminate/2, code_change/3]). -export([user_send_packet/1, user_receive_packet/1, c2s_presence_in/2, c2s_copy_session/2, mod_opt_type/1, mod_options/1, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("mod_caps.hrl"). -include("translate.hrl"). -define(BAD_HASH_LIFETIME, 600). -record(state, {host = <<"">> :: binary()}). -type digest_type() :: md5 | sha | sha224 | sha256 | sha384 | sha512. -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), {binary(), binary()}, [binary() | pos_integer()]) -> ok. -callback caps_read(binary(), {binary(), binary()}) -> {ok, non_neg_integer() | [binary()]} | error. -callback caps_write(binary(), {binary(), binary()}, non_neg_integer() | [binary()]) -> any(). -callback use_cache(binary()) -> boolean(). -optional_callbacks([use_cache/1]). start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). -spec get_features(binary(), nothing | caps()) -> [binary()]. get_features(_Host, nothing) -> []; get_features(Host, #caps{node = Node, version = Version, exts = Exts}) -> SubNodes = [Version | Exts], Mod = gen_mod:db_mod(Host, ?MODULE), lists:foldl( fun(SubNode, Acc) -> NodePair = {Node, SubNode}, Res = case use_cache(Mod, Host) of true -> ets_cache:lookup(caps_features_cache, NodePair, caps_read_fun(Host, NodePair)); false -> Mod:caps_read(Host, NodePair) end, case Res of {ok, Features} when is_list(Features) -> Features ++ Acc; _ -> Acc end end, [], SubNodes). -spec list_features(ejabberd_c2s:state()) -> [{ljid(), caps()}]. list_features(C2SState) -> Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), gb_trees:to_list(Rs). -spec get_user_caps(jid() | ljid(), ejabberd_c2s:state()) -> {ok, caps()} | error. get_user_caps(JID, C2SState) -> Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), LJID = jid:tolower(JID), case gb_trees:lookup(LJID, Rs) of {value, Caps} -> {ok, Caps}; none -> error end. -spec read_caps(#presence{}) -> nothing | caps(). read_caps(Presence) -> case xmpp:get_subtag(Presence, #caps{}) of false -> nothing; Caps -> Caps end. -spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_send_packet({#presence{type = available, from = #jid{luser = U, lserver = LServer} = From, to = #jid{luser = U, lserver = LServer, lresource = <<"">>}} = Pkt, #{jid := To} = State}) -> case read_caps(Pkt) of nothing -> ok; #caps{version = Version, exts = Exts} = Caps -> feature_request(LServer, From, To, Caps, [Version | Exts]) end, {Pkt, State}; user_send_packet(Acc) -> Acc. -spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_receive_packet({#presence{from = From, type = available} = Pkt, #{lserver := LServer, jid := To} = State}) -> IsRemote = case From#jid.lresource of % Don't store caps for presences sent by our muc rooms <<>> -> try ejabberd_router:host_of_route(From#jid.lserver) of MaybeMuc -> not lists:member(From#jid.lserver, gen_mod:get_module_opt_hosts(MaybeMuc, mod_muc)) catch error:{unregistered_route, _} -> true end; _ -> not ejabberd_router:is_my_host(From#jid.lserver) end, if IsRemote -> case read_caps(Pkt) of nothing -> ok; #caps{version = Version, exts = Exts} = Caps -> feature_request(LServer, To, From, Caps, [Version | Exts]) end; true -> ok end, {Pkt, State}; user_receive_packet(Acc) -> Acc. -spec caps_stream_features([xmpp_element()], binary()) -> [xmpp_element()]. caps_stream_features(Acc, MyHost) -> case gen_mod:is_loaded(MyHost, ?MODULE) of true -> case make_my_disco_hash(MyHost) of <<"">> -> Acc; Hash -> [#caps{hash = <<"sha-1">>, node = ejabberd_config:get_uri(), version = Hash} | Acc] end; false -> Acc end. -spec disco_features({error, stanza_error()} | {result, [binary()]} | empty, jid(), jid(), binary(), binary()) -> {error, stanza_error()} | {result, [binary()]} | empty. disco_features(Acc, From, To, Node, Lang) -> case is_valid_node(Node) of true -> ejabberd_hooks:run_fold(disco_local_features, To#jid.lserver, empty, [From, To, <<"">>, Lang]); false -> Acc end. -spec disco_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_identity(Acc, From, To, Node, Lang) -> case is_valid_node(Node) of true -> ejabberd_hooks:run_fold(disco_local_identity, To#jid.lserver, [], [From, To, <<"">>, Lang]); false -> Acc end. -spec disco_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()]; ([xdata()], jid(), jid(), binary(), binary()) -> [xdata()]. disco_info(Acc, Host, Module, Node, Lang) when is_atom(Module) -> case is_valid_node(Node) of true -> ejabberd_hooks:run_fold(disco_info, Host, [], [Host, Module, <<"">>, Lang]); false -> Acc end; disco_info(Acc, _, _, _Node, _Lang) -> Acc. -spec c2s_presence_in(ejabberd_c2s:state(), presence()) -> ejabberd_c2s:state(). c2s_presence_in(C2SState, #presence{from = From, to = To, type = Type} = Presence) -> ToSelf = (From#jid.luser == To#jid.luser) andalso (From#jid.lserver == To#jid.lserver), Caps = read_caps(Presence), Operation = case {Type, ToSelf, Caps} of {unavailable, _, _} -> delete; {error, _, _} -> delete; {available, _, nothing} -> skip; {available, true, _} -> insert; {available, _, _} -> {Subscription, _, _} = ejabberd_hooks:run_fold( roster_get_jid_info, To#jid.lserver, {none, none, []}, [To#jid.luser, To#jid.lserver, From]), case Subscription of from -> insert; both -> insert; _ -> skip end; _ -> skip end, case Operation of skip -> C2SState; delete -> LFrom = jid:tolower(From), Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), C2SState#{caps_resources => gb_trees:delete_any(LFrom, Rs)}; insert -> LFrom = jid:tolower(From), Rs = maps:get(caps_resources, C2SState, gb_trees:empty()), NewRs = case gb_trees:lookup(LFrom, Rs) of {value, Caps} -> Rs; none -> ejabberd_hooks:run(caps_add, To#jid.lserver, [From, To, get_features(To#jid.lserver, Caps)]), gb_trees:insert(LFrom, Caps, Rs); _ -> ejabberd_hooks:run(caps_update, To#jid.lserver, [From, To, get_features(To#jid.lserver, Caps)]), gb_trees:update(LFrom, Caps, Rs) end, C2SState#{caps_resources => NewRs} end. -spec c2s_copy_session(ejabberd_c2s:state(), ejabberd_c2s:state()) -> ejabberd_c2s:state(). c2s_copy_session(C2SState, #{caps_resources := Rs}) -> C2SState#{caps_resources => Rs}; c2s_copy_session(C2SState, _) -> C2SState. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if OldMod /= NewMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts). init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), Mod = gen_mod:db_mod(Opts, ?MODULE), init_cache(Mod, Host, Opts), Mod:init(Host, Opts), ejabberd_hooks:add(c2s_presence_in, Host, ?MODULE, c2s_presence_in, 75), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 75), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 75), ejabberd_hooks:add(c2s_post_auth_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:add(s2s_in_post_auth_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 75), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 75), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 75), ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 75), {ok, #state{host = Host}}. handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({iq_reply, IQReply, {Host, From, To, Caps, SubNodes}}, State) -> feature_response(IQReply, Host, From, To, Caps, SubNodes), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> Host = State#state.host, ejabberd_hooks:delete(c2s_presence_in, Host, ?MODULE, c2s_presence_in, 75), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 75), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 75), ejabberd_hooks:delete(c2s_post_auth_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:delete(s2s_in_post_auth_features, Host, ?MODULE, caps_stream_features, 75), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 75), ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 75), ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 75), ejabberd_hooks:delete(disco_info, Host, ?MODULE, disco_info, 75), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. -spec feature_request(binary(), jid(), jid(), caps(), [binary()]) -> any(). feature_request(Host, From, To, Caps, [SubNode | Tail] = SubNodes) -> Node = Caps#caps.node, NodePair = {Node, SubNode}, Mod = gen_mod:db_mod(Host, ?MODULE), Res = case use_cache(Mod, Host) of true -> ets_cache:lookup(caps_features_cache, NodePair, caps_read_fun(Host, NodePair)); false -> Mod:caps_read(Host, NodePair) end, case Res of {ok, Fs} when is_list(Fs) -> feature_request(Host, From, To, Caps, Tail); _ -> LTo = jid:tolower(To), case ets_cache:insert_new(caps_requests_cache, {LTo, NodePair}, ok) of true -> IQ = #iq{type = get, from = From, to = To, sub_els = [#disco_info{node = <>}]}, ejabberd_router:route_iq( IQ, {Host, From, To, Caps, SubNodes}, gen_mod:get_module_proc(Host, ?MODULE)); false -> ok end, feature_request(Host, From, To, Caps, Tail) end; feature_request(_Host, _From, _To, _Caps, []) -> ok. -spec feature_response(iq(), binary(), jid(), jid(), caps(), [binary()]) -> any(). feature_response(#iq{type = result, sub_els = [El]}, Host, From, To, Caps, [SubNode | SubNodes]) -> NodePair = {Caps#caps.node, SubNode}, try DiscoInfo = xmpp:decode(El), case check_hash(Caps, DiscoInfo) of true -> Features = DiscoInfo#disco_info.features, LServer = jid:nameprep(Host), Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:caps_write(LServer, NodePair, Features) of ok -> case use_cache(Mod, LServer) of true -> ets_cache:delete(caps_features_cache, NodePair); false -> ok end; {error, _} -> ok end; false -> ok end catch _:{xmpp_codec, _Why} -> ok end, feature_request(Host, From, To, Caps, SubNodes); feature_response(_IQResult, Host, From, To, Caps, [_SubNode | SubNodes]) -> feature_request(Host, From, To, Caps, SubNodes). -spec caps_read_fun(binary(), {binary(), binary()}) -> fun(() -> {ok, [binary()] | non_neg_integer()} | error). caps_read_fun(Host, Node) -> LServer = jid:nameprep(Host), Mod = gen_mod:db_mod(LServer, ?MODULE), fun() -> Mod:caps_read(LServer, Node) end. -spec make_my_disco_hash(binary()) -> binary(). make_my_disco_hash(Host) -> JID = jid:make(Host), case {ejabberd_hooks:run_fold(disco_local_features, Host, empty, [JID, JID, <<"">>, <<"">>]), ejabberd_hooks:run_fold(disco_local_identity, Host, [], [JID, JID, <<"">>, <<"">>]), ejabberd_hooks:run_fold(disco_info, Host, [], [Host, undefined, <<"">>, <<"">>])} of {{result, Features}, Identities, Info} -> Feats = lists:map(fun ({{Feat, _Host}}) -> Feat; (Feat) -> Feat end, Features), DiscoInfo = #disco_info{identities = Identities, features = Feats, xdata = Info}, compute_disco_hash(DiscoInfo, sha); _Err -> <<"">> end. -spec compute_disco_hash(disco_info(), digest_type()) -> binary(). compute_disco_hash(DiscoInfo, Algo) -> Concat = list_to_binary([concat_identities(DiscoInfo), concat_features(DiscoInfo), concat_info(DiscoInfo)]), base64:encode(case Algo of md5 -> erlang:md5(Concat); sha -> crypto:hash(sha, Concat); sha224 -> crypto:hash(sha224, Concat); sha256 -> crypto:hash(sha256, Concat); sha384 -> crypto:hash(sha384, Concat); sha512 -> crypto:hash(sha512, Concat) end). -spec check_hash(caps(), disco_info()) -> boolean(). check_hash(Caps, DiscoInfo) -> case Caps#caps.hash of <<"md5">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, md5); <<"sha-1">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, sha); <<"sha-224">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, sha224); <<"sha-256">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, sha256); <<"sha-384">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, sha384); <<"sha-512">> -> Caps#caps.version == compute_disco_hash(DiscoInfo, sha512); _ -> true end. -spec concat_features(disco_info()) -> iolist(). concat_features(#disco_info{features = Features}) -> lists:usort([[Feat, $<] || Feat <- Features]). -spec concat_identities(disco_info()) -> iolist(). concat_identities(#disco_info{identities = Identities}) -> lists:sort( [[Cat, $/, T, $/, Lang, $/, Name, $<] || #identity{category = Cat, type = T, lang = Lang, name = Name} <- Identities]). -spec concat_info(disco_info()) -> iolist(). concat_info(#disco_info{xdata = Xs}) -> lists:sort( [concat_xdata_fields(X) || #xdata{type = result} = X <- Xs]). -spec concat_xdata_fields(xdata()) -> iolist(). concat_xdata_fields(#xdata{fields = Fields} = X) -> Form = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), Res = [[Var, $<, lists:sort([[Val, $<] || Val <- Values])] || #xdata_field{var = Var, values = Values} <- Fields, is_binary(Var), Var /= <<"FORM_TYPE">>], [Form, $<, lists:sort(Res)]. -spec is_valid_node(binary()) -> boolean(). is_valid_node(Node) -> case str:tokens(Node, <<"#">>) of [H|_] -> H == ejabberd_config:get_uri(); [] -> false end. init_cache(Mod, Host, Opts) -> CacheOpts = cache_opts(Opts), case use_cache(Mod, Host) of true -> ets_cache:new(caps_features_cache, CacheOpts); false -> ets_cache:delete(caps_features_cache) end, CacheSize = proplists:get_value(max_size, CacheOpts), ets_cache:new(caps_requests_cache, [{max_size, CacheSize}, {life_time, timer:seconds(?BAD_HASH_LIFETIME)}]). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_caps_opt:use_cache(Host) end. cache_opts(Opts) -> MaxSize = mod_caps_opt:cache_size(Opts), CacheMissed = mod_caps_opt:cache_missed(Opts), LifeTime = mod_caps_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. export(LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), Mod:export(LServer). import_info() -> [{<<"caps_features">>, 4}]. import_start(LServer, DBType) -> ets:new(caps_features_tmp, [private, named_table, bag]), Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:init(LServer, []), ok. import(_LServer, {sql, _}, _DBType, <<"caps_features">>, [Node, SubNode, Feature, _TimeStamp]) -> Feature1 = case catch binary_to_integer(Feature) of I when is_integer(I), I>0 -> I; _ -> Feature end, ets:insert(caps_features_tmp, {{Node, SubNode}, Feature1}), ok. import_stop(LServer, DBType) -> import_next(LServer, DBType, ets:first(caps_features_tmp)), ets:delete(caps_features_tmp), ok. import_next(_LServer, _DBType, '$end_of_table') -> ok; import_next(LServer, DBType, NodePair) -> Features = [F || {_, F} <- ets:lookup(caps_features_tmp, NodePair)], Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, NodePair, Features), import_next(LServer, DBType, ets:next(caps_features_tmp, NodePair)). mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). mod_options(Host) -> [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => [?T("This module implements " "https://xmpp.org/extensions/xep-0115.html" "[XEP-0115: Entity Capabilities]."), ?T("The main purpose of the module is to provide " "PEP functionality (see _`mod_pubsub`_).")], opts => [{db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. ejabberd-21.12/src/eldap_utils.erl0000644000232200023220000002065414154362354017441 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : eldap_utils.erl %%% Author : Mickael Remond %%% Purpose : ejabberd LDAP helper functions %%% Created : 12 Oct 2006 by Mickael Remond %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(eldap_utils). -author('mremond@process-one.net'). -export([generate_subfilter/1, find_ldap_attrs/2, check_filter/1, get_ldap_attr/2, get_user_part/2, make_filter/2, get_state/2, case_insensitive_match/2, decode_octet_string/3, uids_domain_subst/2]). -include("logger.hrl"). -include("eldap.hrl"). %% Generate an 'or' LDAP query on one or several attributes %% If there is only one attribute generate_subfilter([UID]) -> subfilter(UID); %% If there is several attributes generate_subfilter(UIDs) -> iolist_to_binary(["(|", [subfilter(UID) || UID <- UIDs], ")"]). %% Subfilter for a single attribute subfilter({UIDAttr, UIDAttrFormat}) -> %% The default UiDAttrFormat is %u <<$(, UIDAttr/binary, $=, UIDAttrFormat/binary, $)>>; %% The default UiDAttrFormat is <<"%u">> subfilter({UIDAttr}) -> <<$(, UIDAttr/binary, $=, "%u)">>. %% Not tail-recursive, but it is not very terribly. %% It stops finding on the first not empty value. -spec find_ldap_attrs([{binary()} | {binary(), binary()}], [{binary(), [binary()]}]) -> <<>> | {binary(), binary()}. find_ldap_attrs([{Attr} | Rest], Attributes) -> find_ldap_attrs([{Attr, <<"%u">>} | Rest], Attributes); find_ldap_attrs([{Attr, Format} | Rest], Attributes) -> case get_ldap_attr(Attr, Attributes) of Value when is_binary(Value), Value /= <<>> -> {Value, Format}; _ -> find_ldap_attrs(Rest, Attributes) end; find_ldap_attrs([], _) -> <<>>. -spec get_ldap_attr(binary(), [{binary(), [binary()]}]) -> binary(). get_ldap_attr(LDAPAttr, Attributes) -> Res = lists:filter( fun({Name, _}) -> case_insensitive_match(Name, LDAPAttr) end, Attributes), case Res of [{_, [Value|_]}] -> Value; _ -> <<>> end. -spec get_user_part(binary(), binary()) -> {ok, binary()} | {error, badmatch}. get_user_part(String, Pattern) -> F = fun(S, P) -> First = str:str(P, <<"%u">>), TailLength = byte_size(P) - (First+1), str:sub_string(S, First, byte_size(S) - TailLength) end, case catch F(String, Pattern) of {'EXIT', _} -> {error, badmatch}; Result -> case catch ejabberd_regexp:replace(Pattern, <<"%u">>, Result) of {'EXIT', _} -> {error, badmatch}; StringRes -> case case_insensitive_match(StringRes, String) of true -> {ok, Result}; false -> {error, badmatch} end end end. -spec make_filter([{binary(), [binary()]}], [{binary(), binary()}]) -> any(). make_filter(Data, UIDs) -> NewUIDs = [{U, eldap_filter:do_sub( UF, [{<<"%u">>, <<"*%u*">>, 1}])} || {U, UF} <- UIDs], Filter = lists:flatmap( fun({Name, [Value | _]}) -> case Name of <<"%u">> when Value /= <<"">> -> case eldap_filter:parse( generate_subfilter(NewUIDs), [{<<"%u">>, Value}]) of {ok, F} -> [F]; _ -> [] end; _ when Value /= <<"">> -> [eldap:substrings( Name, [{any, Value}])]; _ -> [] end end, Data), case Filter of [F] -> F; _ -> eldap:'and'(Filter) end. check_filter(F) -> NewF = iolist_to_binary(F), {ok, _} = eldap_filter:parse(NewF), NewF. -spec case_insensitive_match(binary(), binary()) -> boolean(). case_insensitive_match(X, Y) -> X1 = str:to_lower(X), Y1 = str:to_lower(Y), if X1 == Y1 -> true; true -> false end. get_state(Server, Module) -> Proc = gen_mod:get_module_proc(Server, Module), gen_server:call(Proc, get_state). %% From the list of uids attribute: %% we look from alias domain (%d) and make the substitution %% with the actual host domain %% This help when you need to configure many virtual domains. -spec uids_domain_subst(binary(), [{binary(), binary()}]) -> [{binary(), binary()}]. uids_domain_subst(Host, UIDs) -> lists:map(fun({U,V}) -> {U, eldap_filter:do_sub(V,[{<<"%d">>, Host}])}; (A) -> A end, UIDs). %%---------------------------------------- %% Borrowed from asn1rt_ber_bin_v2.erl %%---------------------------------------- %%% The tag-number for universal types -define(N_BOOLEAN, 1). -define(N_INTEGER, 2). -define(N_BIT_STRING, 3). -define(N_OCTET_STRING, 4). -define(N_NULL, 5). -define(N_OBJECT_IDENTIFIER, 6). -define(N_OBJECT_DESCRIPTOR, 7). -define(N_EXTERNAL, 8). -define(N_REAL, 9). -define(N_ENUMERATED, 10). -define(N_EMBEDDED_PDV, 11). -define(N_SEQUENCE, 16). -define(N_SET, 17). -define(N_NumericString, 18). -define(N_PrintableString, 19). -define(N_TeletexString, 20). -define(N_VideotexString, 21). -define(N_IA5String, 22). -define(N_UTCTime, 23). -define(N_GeneralizedTime, 24). -define(N_GraphicString, 25). -define(N_VisibleString, 26). -define(N_GeneralString, 27). -define(N_UniversalString, 28). -define(N_BMPString, 30). decode_octet_string(Buffer, Range, Tags) -> % NewTags = new_tags(HasTag,#tag{class=?UNIVERSAL,number=?N_OCTET_STRING}), decode_restricted_string(Buffer, Range, Tags). decode_restricted_string(Tlv, Range, TagsIn) -> Val = match_tags(Tlv, TagsIn), Val2 = case Val of PartList = [_H|_T] -> % constructed val collect_parts(PartList); Bin -> Bin end, check_and_convert_restricted_string(Val2, Range). check_and_convert_restricted_string(Val, Range) -> {StrLen,NewVal} = if is_binary(Val) -> {size(Val), Val}; true -> {length(Val), list_to_binary(Val)} end, case Range of [] -> % No length constraint NewVal; {Lb,Ub} when StrLen >= Lb, Ub >= StrLen -> % variable length constraint NewVal; {{Lb,_Ub},[]} when StrLen >= Lb -> NewVal; {{Lb,_Ub},_Ext=[Min|_]} when StrLen >= Lb; StrLen >= Min -> NewVal; {{Lb1,Ub1},{Lb2,Ub2}} when StrLen >= Lb1, StrLen =< Ub1; StrLen =< Ub2, StrLen >= Lb2 -> NewVal; StrLen -> % fixed length constraint NewVal; {_,_} -> exit({error,{asn1,{length,Range,Val}}}); _Len when is_integer(_Len) -> exit({error,{asn1,{length,Range,Val}}}); _ -> % some strange constraint that we don't support yet NewVal end. %%---------------------------------------- %% Decode the in buffer to bits %%---------------------------------------- match_tags({T,V},[T]) -> V; match_tags({T,V}, [T|Tt]) -> match_tags(V,Tt); match_tags([{T,V}],[T|Tt]) -> match_tags(V, Tt); match_tags(Vlist = [{T,_V}|_], [T]) -> Vlist; match_tags(Tlv, []) -> Tlv; match_tags({Tag,_V},[T|_Tt]) -> {error,{asn1,{wrong_tag,{Tag,T}}}}. collect_parts(TlvList) -> collect_parts(TlvList,[]). collect_parts([{_,L}|Rest],Acc) when is_list(L) -> collect_parts(Rest,[collect_parts(L)|Acc]); collect_parts([{?N_BIT_STRING,<>}|Rest],_Acc) -> collect_parts_bit(Rest,[Bits],Unused); collect_parts([{_T,V}|Rest],Acc) -> collect_parts(Rest,[V|Acc]); collect_parts([],Acc) -> list_to_binary(lists:reverse(Acc)). collect_parts_bit([{?N_BIT_STRING,<>}|Rest],Acc,Uacc) -> collect_parts_bit(Rest,[Bits|Acc],Unused+Uacc); collect_parts_bit([],Acc,Uacc) -> list_to_binary([Uacc|lists:reverse(Acc)]). ejabberd-21.12/src/mod_pubsub_mnesia.erl0000644000232200023220000000255414154362354020626 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2002-2021 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(mod_pubsub_mnesia). %% API -export([init/3]). %%%=================================================================== %%% API %%%=================================================================== init(Host, ServerHost, Opts) -> pubsub_index:init(Host, ServerHost, Opts). %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/rest.erl0000644000232200023220000001672714154362354016117 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : rest.erl %%% Author : Christophe Romain %%% Purpose : Generic REST client %%% Created : 16 Oct 2014 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(rest). -export([start/1, stop/1, get/2, get/3, post/4, delete/2, put/4, patch/4, request/6, with_retry/4, encode_json/1]). -include("logger.hrl"). -define(HTTP_TIMEOUT, 10000). -define(CONNECT_TIMEOUT, 8000). -define(CONTENT_TYPE, "application/json"). start(Host) -> application:start(inets), Size = ejabberd_option:ext_api_http_pool_size(Host), httpc:set_options([{max_sessions, Size}]). stop(_Host) -> ok. with_retry(Method, Args, MaxRetries, Backoff) -> with_retry(Method, Args, 0, MaxRetries, Backoff). with_retry(Method, Args, Retries, MaxRetries, Backoff) -> case apply(?MODULE, Method, Args) of %% Only retry on timeout errors {error, {http_error,{error,Error}}} when Retries < MaxRetries andalso (Error == 'timeout' orelse Error == 'connect_timeout') -> timer:sleep(round(math:pow(2, Retries)) * Backoff), with_retry(Method, Args, Retries+1, MaxRetries, Backoff); Result -> Result end. get(Server, Path) -> request(Server, get, Path, [], ?CONTENT_TYPE, <<>>). get(Server, Path, Params) -> request(Server, get, Path, Params, ?CONTENT_TYPE, <<>>). delete(Server, Path) -> request(Server, delete, Path, [], ?CONTENT_TYPE, <<>>). post(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, post, Path, Params, ?CONTENT_TYPE, Data). put(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, put, Path, Params, ?CONTENT_TYPE, Data). patch(Server, Path, Params, Content) -> Data = encode_json(Content), request(Server, patch, Path, Params, ?CONTENT_TYPE, Data). request(Server, Method, Path, _Params, _Mime, {error, Error}) -> ejabberd_hooks:run(backend_api_error, Server, [Server, Method, Path, Error]), {error, Error}; request(Server, Method, Path, Params, Mime, Data) -> {Query, Opts} = case Params of {_, _} -> Params; _ -> {Params, []} end, URI = to_list(url(Server, Path, Query)), HttpOpts = [{connect_timeout, ?CONNECT_TIMEOUT}, {timeout, ?HTTP_TIMEOUT}], Hdrs = [{"connection", "keep-alive"}, {"Accept", "application/json"}, {"User-Agent", "ejabberd"}] ++ custom_headers(Server), Req = if (Method =:= post) orelse (Method =:= patch) orelse (Method =:= put) orelse (Method =:= delete) -> {URI, Hdrs, to_list(Mime), Data}; true -> {URI, Hdrs} end, Begin = os:timestamp(), ejabberd_hooks:run(backend_api_call, Server, [Server, Method, Path]), Result = try httpc:request(Method, Req, HttpOpts, [{body_format, binary}]) of {ok, {{_, Code, _}, RetHdrs, Body}} -> try decode_json(Body) of JSon -> case proplists:get_bool(return_headers, Opts) of true -> {ok, Code, RetHdrs, JSon}; false -> {ok, Code, JSon} end catch _:Reason -> {error, {invalid_json, Body, Reason}} end; {error, Reason} -> {error, {http_error, {error, Reason}}} catch exit:Reason -> {error, {http_error, {error, Reason}}} end, case Result of {error, {http_error, {error, timeout}}} -> ejabberd_hooks:run(backend_api_timeout, Server, [Server, Method, Path]); {error, {http_error, {error, connect_timeout}}} -> ejabberd_hooks:run(backend_api_timeout, Server, [Server, Method, Path]); {error, Error} -> ejabberd_hooks:run(backend_api_error, Server, [Server, Method, Path, Error]); _ -> End = os:timestamp(), Elapsed = timer:now_diff(End, Begin) div 1000, %% time in ms ejabberd_hooks:run(backend_api_response_time, Server, [Server, Method, Path, Elapsed]) end, Result. %%%---------------------------------------------------------------------- %%% HTTP helpers %%%---------------------------------------------------------------------- to_list(V) when is_binary(V) -> binary_to_list(V); to_list(V) when is_list(V) -> V. encode_json(Content) -> case catch jiffy:encode(Content) of {'EXIT', Reason} -> {error, {invalid_payload, Content, Reason}}; Encoded -> Encoded end. decode_json(<<>>) -> []; decode_json(<<" ">>) -> []; decode_json(<<"\r\n">>) -> []; decode_json(Data) -> jiffy:decode(Data). custom_headers(Server) -> case ejabberd_option:ext_api_headers(Server) of <<>> -> []; Hdrs -> lists:foldr(fun(Hdr, Acc) -> case binary:split(Hdr, <<":">>) of [K, V] -> [{binary_to_list(K), binary_to_list(V)}|Acc]; _ -> Acc end end, [], binary:split(Hdrs, <<",">>)) end. base_url(Server, Path) -> BPath = case iolist_to_binary(Path) of <<$/, Ok/binary>> -> Ok; Ok -> Ok end, Url = case BPath of <<"http", _/binary>> -> BPath; _ -> Base = ejabberd_option:ext_api_url(Server), case binary:last(Base) of $/ -> <>; _ -> <> end end, case binary:last(Url) of 47 -> binary_part(Url, 0, size(Url)-1); _ -> Url end. -ifdef(HAVE_URI_STRING). uri_hack(Str) -> case uri_string:normalize("%25") of "%" -> % This hack around bug in httpc >21 <23.2 binary:replace(Str, <<"%25">>, <<"%2525">>, [global]); _ -> Str end. -else. uri_hack(Str) -> Str. -endif. url(Url, []) -> Url; url(Url, Params) -> L = [<<"&", (iolist_to_binary(Key))/binary, "=", (misc:url_encode(Value))/binary>> || {Key, Value} <- Params], <<$&, Encoded0/binary>> = iolist_to_binary(L), Encoded = uri_hack(Encoded0), <>. url(Server, Path, Params) -> case binary:split(base_url(Server, Path), <<"?">>) of [Url] -> url(Url, Params); [Url, Extra] -> Custom = [list_to_tuple(binary:split(P, <<"=">>)) || P <- binary:split(Extra, <<"&">>, [global])], url(Url, Custom++Params) end. ejabberd-21.12/src/mod_time.erl0000644000232200023220000000514614154362354016730 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_time.erl %%% Author : Alexey Shchepin %%% Purpose : %%% Purpose : %%% Created : 18 Jan 2003 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_time). -author('alexey@process-one.net'). -protocol({xep, 202, '2.0'}). -behaviour(gen_mod). -export([start/2, stop/1, reload/3, process_local_iq/1, mod_options/1, depends/2, mod_doc/0]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). start(Host, _Opts) -> gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_TIME, ?MODULE, process_local_iq). stop(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_TIME). reload(_Host, _NewOpts, _OldOpts) -> ok. -spec process_local_iq(iq()) -> iq(). process_local_iq(#iq{type = set, lang = Lang} = IQ) -> Txt = ?T("Value 'set' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_local_iq(#iq{type = get} = IQ) -> Now = os:timestamp(), Now_universal = calendar:now_to_universal_time(Now), Now_local = calendar:universal_time_to_local_time(Now_universal), Seconds_diff = calendar:datetime_to_gregorian_seconds(Now_local) - calendar:datetime_to_gregorian_seconds(Now_universal), {Hd, Md, _} = calendar:seconds_to_time(abs(Seconds_diff)), xmpp:make_iq_result(IQ, #time{tzo = {Hd, Md}, utc = Now}). depends(_Host, _Opts) -> []. mod_options(_Host) -> []. mod_doc() -> #{desc => ?T("This module adds support for " "https://xmpp.org/extensions/xep-0202.html" "[XEP-0202: Entity Time]. In other words, " "the module reports server's system time.")}. ejabberd-21.12/src/ejabberd_c2s_config.erl0000644000232200023220000000353414154362354020764 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_c2s_config.erl %%% Author : Mickael Remond %%% Purpose : Functions for c2s interactions from other client %%% connector modules %%% Created : 2 Nov 2007 by Mickael Remond %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_c2s_config). -author('mremond@process-one.net'). -export([get_c2s_limits/0]). %% Get first c2s configuration limitations to apply it to other c2s %% connectors. get_c2s_limits() -> C2SFirstListen = ejabberd_option:listen(), case lists:keysearch(ejabberd_c2s, 2, C2SFirstListen) of false -> []; {value, {_Port, ejabberd_c2s, Opts}} -> select_opts_values(Opts) end. %% Only get access, shaper and max_stanza_size values select_opts_values(Opts) -> maps:fold( fun(Opt, Val, Acc) when Opt == access; Opt == shaper; Opt == max_stanza_size -> [{Opt, Val}|Acc]; (_, _, Acc) -> Acc end, [], Opts). ejabberd-21.12/src/mod_metrics.erl0000644000232200023220000002151114154362354017432 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_metrics.erl %%% Author : Christophe Romain %%% Purpose : Simple metrics handler for runtime statistics %%% Created : 22 Oct 2015 by Christophe Romain %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_metrics). -author('christophe.romain@process-one.net'). -behaviour(gen_mod). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -export([start/2, stop/1, mod_opt_type/1, mod_options/1, depends/2, reload/3]). -export([push/2, mod_doc/0]). -export([offline_message_hook/1, sm_register_connection_hook/3, sm_remove_connection_hook/3, user_send_packet/1, user_receive_packet/1, s2s_send_packet/1, s2s_receive_packet/1, remove_user/2, register_user/2]). -define(SOCKET_NAME, mod_metrics_udp_socket). -define(SOCKET_REGISTER_RETRIES, 10). -type probe() :: atom() | {atom(), integer()}. %%==================================================================== %% API %%==================================================================== start(Host, _Opts) -> ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, offline_message_hook, 20), ejabberd_hooks:add(sm_register_connection_hook, Host, ?MODULE, sm_register_connection_hook, 20), ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, sm_remove_connection_hook, 20), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send_packet, 20), ejabberd_hooks:add(user_receive_packet, Host, ?MODULE, user_receive_packet, 20), ejabberd_hooks:add(s2s_send_packet, Host, ?MODULE, s2s_send_packet, 20), ejabberd_hooks:add(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 20), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 20), ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 20). stop(Host) -> ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, offline_message_hook, 20), ejabberd_hooks:delete(sm_register_connection_hook, Host, ?MODULE, sm_register_connection_hook, 20), ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, sm_remove_connection_hook, 20), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send_packet, 20), ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE, user_receive_packet, 20), ejabberd_hooks:delete(s2s_send_packet, Host, ?MODULE, s2s_send_packet, 20), ejabberd_hooks:delete(s2s_receive_packet, Host, ?MODULE, s2s_receive_packet, 20), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 20), ejabberd_hooks:delete(register_user, Host, ?MODULE, register_user, 20). reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. %%==================================================================== %% Hooks handlers %%==================================================================== -spec offline_message_hook({any(), message()}) -> {any(), message()}. offline_message_hook({_Action, #message{to = #jid{lserver = LServer}}} = Acc) -> push(LServer, offline_message), Acc. -spec sm_register_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). sm_register_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> push(LServer, sm_register_connection). -spec sm_remove_connection_hook(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). sm_remove_connection_hook(_SID, #jid{lserver=LServer}, _Info) -> push(LServer, sm_remove_connection). -spec user_send_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_send_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> push(LServer, user_send_packet), {Packet, C2SState}. -spec user_receive_packet({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_receive_packet({Packet, #{jid := #jid{lserver = LServer}} = C2SState}) -> push(LServer, user_receive_packet), {Packet, C2SState}. -spec s2s_send_packet(stanza()) -> stanza(). s2s_send_packet(Packet) -> #jid{lserver = LServer} = xmpp:get_from(Packet), push(LServer, s2s_send_packet), Packet. -spec s2s_receive_packet({stanza(), ejabberd_s2s_in:state()}) -> {stanza(), ejabberd_s2s_in:state()}. s2s_receive_packet({Packet, S2SState}) -> To = xmpp:get_to(Packet), LServer = ejabberd_router:host_of_route(To#jid.lserver), push(LServer, s2s_receive_packet), {Packet, S2SState}. -spec remove_user(binary(), binary()) -> any(). remove_user(_User, Server) -> push(jid:nameprep(Server), remove_user). -spec register_user(binary(), binary()) -> any(). register_user(_User, Server) -> push(jid:nameprep(Server), register_user). %%==================================================================== %% metrics push handler %%==================================================================== -spec push(binary(), probe()) -> ok | {error, not_owner | inet:posix()}. push(Host, Probe) -> IP = mod_metrics_opt:ip(Host), Port = mod_metrics_opt:port(Host), send_metrics(Host, Probe, IP, Port). -spec send_metrics(binary(), probe(), inet:ip4_address(), inet:port_number()) -> ok | {error, not_owner | inet:posix()}. send_metrics(Host, Probe, Peer, Port) -> % our default metrics handler is https://github.com/processone/grapherl % grapherl metrics are named first with service domain, then nodename % and name of the data itself, followed by type timestamp and value % example => process-one.net/xmpp-1.user_receive_packet:c/1441784958:1 [_, FQDN] = binary:split(misc:atom_to_binary(node()), <<"@">>), [Node|_] = binary:split(FQDN, <<".">>), BaseId = <>, TS = integer_to_binary(erlang:system_time(second)), case get_socket(?SOCKET_REGISTER_RETRIES) of {ok, Socket} -> case Probe of {Key, Val} -> BVal = integer_to_binary(Val), Data = <>, gen_udp:send(Socket, Peer, Port, Data); Key -> Data = <>, gen_udp:send(Socket, Peer, Port, Data) end; Err -> Err end. -spec get_socket(integer()) -> {ok, gen_udp:socket()} | {error, inet:posix()}. get_socket(N) -> case whereis(?SOCKET_NAME) of undefined -> case gen_udp:open(0) of {ok, Socket} -> try register(?SOCKET_NAME, Socket) of true -> {ok, Socket} catch _:badarg when N > 1 -> gen_udp:close(Socket), get_socket(N-1) end; {error, Reason} = Err -> ?ERROR_MSG("Can not open udp socket to grapherl: ~ts", [inet:format_error(Reason)]), Err end; Socket -> {ok, Socket} end. mod_opt_type(ip) -> econf:ipv4(); mod_opt_type(port) -> econf:port(). mod_options(_) -> [{ip, {127,0,0,1}}, {port, 11111}]. mod_doc() -> #{desc => [?T("This module sends events to external backend " "(by now only https://github.com/processone/grapherl" "[grapherl] is supported). Supported events are:"), "", "- sm_register_connection", "", "- sm_remove_connection", "", "- user_send_packet", "", "- user_receive_packet", "", "- s2s_send_packet", "", "- s2s_receive_packet", "", "- register_user", "", "- remove_user", "", "- offline_message", "", ?T("When enabled, every call to these hooks triggers " "a counter event to be sent to the external backend.")], opts => [{ip, #{value => ?T("IPv4Address"), desc => ?T("IPv4 address where the backend is located. " "The default value is '127.0.0.1'.")}}, {port, #{value => ?T("Port"), desc => ?T("An internet port number at which the backend " "is listening for incoming connections/packets. " "The default value is '11111'.")}}]}. ejabberd-21.12/src/pubsub_db_sql.erl0000644000232200023220000001675514154362354017767 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : pubsub_db_sql.erl %%% Author : Pablo Polvorin %%% Purpose : Provide helpers for PubSub ODBC backend %%% Created : 7 Aug 2009 by Pablo Polvorin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(pubsub_db_sql). -author("pablo.polvorin@process-one.net"). -include("pubsub.hrl"). -include("ejabberd_sql_pt.hrl"). -export([add_subscription/1, read_subscription/1, delete_subscription/1, update_subscription/1]). -export([export/1]). -spec read_subscription(SubID :: mod_pubsub:subId()) -> {ok, #pubsub_subscription{}} | notfound. read_subscription(SubID) -> case ejabberd_sql:sql_query_t( ?SQL("select @(opt_name)s, @(opt_value)s from pubsub_subscription_opt where subid = %(SubID)s")) of {selected, []} -> notfound; {selected, Options} -> {ok, #pubsub_subscription{subid = SubID, options = lists:map(fun subscription_opt_from_sql/1, Options)}} end. -spec delete_subscription(SubID :: mod_pubsub:subId()) -> ok. delete_subscription(SubID) -> ejabberd_sql:sql_query_t( ?SQL("delete from pubsub_subscription_opt " "where subid = %(SubID)s")), ok. -spec update_subscription(#pubsub_subscription{}) -> ok . update_subscription(#pubsub_subscription{subid = SubId} = Sub) -> delete_subscription(SubId), add_subscription(Sub). -spec add_subscription(#pubsub_subscription{}) -> ok. add_subscription(#pubsub_subscription{subid = SubId, options = Opts}) -> lists:foreach( fun(Opt) -> {OdbcOptName, OdbcOptValue} = subscription_opt_to_sql(Opt), ejabberd_sql:sql_query_t( ?SQL("insert into pubsub_subscription_opt(subid, " "opt_name, opt_value) values " "(%(SubId)s, %(OdbcOptName)s, %(OdbcOptValue)s)")) end, Opts), ok. subscription_opt_from_sql({<<"DELIVER">>, Value}) -> {deliver, sql_to_boolean(Value)}; subscription_opt_from_sql({<<"DIGEST">>, Value}) -> {digest, sql_to_boolean(Value)}; subscription_opt_from_sql({<<"DIGEST_FREQUENCY">>, Value}) -> {digest_frequency, sql_to_integer(Value)}; subscription_opt_from_sql({<<"EXPIRE">>, Value}) -> {expire, sql_to_timestamp(Value)}; subscription_opt_from_sql({<<"INCLUDE_BODY">>, Value}) -> {include_body, sql_to_boolean(Value)}; %%TODO: might be > than 1 show_values value??. %% need to use compact all in only 1 opt. subscription_opt_from_sql({<<"SHOW_VALUES">>, Value}) -> {show_values, Value}; subscription_opt_from_sql({<<"SUBSCRIPTION_TYPE">>, Value}) -> {subscription_type, case Value of <<"items">> -> items; <<"nodes">> -> nodes end}; subscription_opt_from_sql({<<"SUBSCRIPTION_DEPTH">>, Value}) -> {subscription_depth, case Value of <<"all">> -> all; N -> sql_to_integer(N) end}. subscription_opt_to_sql({deliver, Bool}) -> {<<"DELIVER">>, boolean_to_sql(Bool)}; subscription_opt_to_sql({digest, Bool}) -> {<<"DIGEST">>, boolean_to_sql(Bool)}; subscription_opt_to_sql({digest_frequency, Int}) -> {<<"DIGEST_FREQUENCY">>, integer_to_sql(Int)}; subscription_opt_to_sql({expire, Timestamp}) -> {<<"EXPIRE">>, timestamp_to_sql(Timestamp)}; subscription_opt_to_sql({include_body, Bool}) -> {<<"INCLUDE_BODY">>, boolean_to_sql(Bool)}; subscription_opt_to_sql({show_values, Values}) -> {<<"SHOW_VALUES">>, Values}; subscription_opt_to_sql({subscription_type, Type}) -> {<<"SUBSCRIPTION_TYPE">>, case Type of items -> <<"items">>; nodes -> <<"nodes">> end}; subscription_opt_to_sql({subscription_depth, Depth}) -> {<<"SUBSCRIPTION_DEPTH">>, case Depth of all -> <<"all">>; N -> integer_to_sql(N) end}. integer_to_sql(N) -> integer_to_binary(N). boolean_to_sql(true) -> <<"1">>; boolean_to_sql(false) -> <<"0">>. timestamp_to_sql(T) -> xmpp_util:encode_timestamp(T). sql_to_integer(N) -> binary_to_integer(N). sql_to_boolean(B) -> B == <<"1">>. sql_to_timestamp(T) -> xmpp_util:decode_timestamp(T). export(_Server) -> [{pubsub_node, fun(_Host, #pubsub_node{nodeid = {Host, Node}, id = Nidx, parents = Parents, type = Type, options = Options}) -> H = node_flat_sql:encode_host(Host), Parent = case Parents of [] -> <<>>; [First | _] -> First end, [?SQL("delete from pubsub_node where nodeid=%(Nidx)d;"), ?SQL("delete from pubsub_node_option where nodeid=%(Nidx)d;"), ?SQL("delete from pubsub_node_owner where nodeid=%(Nidx)d;"), ?SQL("delete from pubsub_state where nodeid=%(Nidx)d;"), ?SQL("delete from pubsub_item where nodeid=%(Nidx)d;"), ?SQL("insert into pubsub_node(host,node,nodeid,parent,plugin)" " values (%(H)s, %(Node)s, %(Nidx)d, %(Parent)s, %(Type)s);")] ++ lists:map( fun ({Key, Value}) -> SKey = iolist_to_binary(atom_to_list(Key)), SValue = misc:term_to_expr(Value), ?SQL("insert into pubsub_node_option(nodeid,name,val)" " values (%(Nidx)d, %(SKey)s, %(SValue)s);") end, Options); (_Host, _R) -> [] end}, {pubsub_state, fun(_Host, #pubsub_state{stateid = {JID, Nidx}, affiliation = Affiliation, subscriptions = Subscriptions}) -> J = jid:encode(JID), S = node_flat_sql:encode_subscriptions(Subscriptions), A = node_flat_sql:encode_affiliation(Affiliation), [?SQL("insert into pubsub_state(nodeid,jid,affiliation,subscriptions)" " values (%(Nidx)d, %(J)s, %(A)s, %(S)s);")]; (_Host, _R) -> [] end}, {pubsub_item, fun(_Host, #pubsub_item{itemid = {ItemId, Nidx}, creation = {C, _}, modification = {M, JID}, payload = Payload}) -> P = jid:encode(JID), XML = str:join([fxml:element_to_binary(X) || X<-Payload], <<>>), SM = encode_now(M), SC = encode_now(C), [?SQL("insert into pubsub_item(itemid,nodeid,creation,modification,publisher,payload)" " values (%(ItemId)s, %(Nidx)d, %(SC)s, %(SM)s, %(P)s, %(XML)s);")]; (_Host, _R) -> [] end}]. encode_now({T1, T2, T3}) -> <<(misc:i2l(T1, 6))/binary, ":", (misc:i2l(T2, 6))/binary, ":", (misc:i2l(T3, 6))/binary>>. ejabberd-21.12/src/ejabberd_s2s.erl0000644000232200023220000005105514154362354017460 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_s2s.erl %%% Author : Alexey Shchepin %%% Purpose : S2S connections manager %%% Created : 7 Dec 2002 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_s2s). -protocol({xep, 220, '1.1'}). -author('alexey@process-one.net'). -behaviour(gen_server). %% API -export([start_link/0, stop/0, route/1, have_connection/1, get_connections_pids/1, start_connection/2, start_connection/3, dirty_get_connections/0, allow_host/2, incoming_s2s_number/0, outgoing_s2s_number/0, stop_s2s_connections/0, clean_temporarily_blocked_table/0, list_temporarily_blocked_hosts/0, external_host_overloaded/1, is_temporarly_blocked/1, get_commands_spec/0, zlib_enabled/1, get_idle_timeout/1, tls_required/1, tls_enabled/1, tls_options/3, host_up/1, host_down/1, queue_type/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([get_info_s2s_connections/1]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("ejabberd_commands.hrl"). -include_lib("stdlib/include/ms_transform.hrl"). -include("ejabberd_stacktrace.hrl"). -include("translate.hrl"). -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER, 1). -define(DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE, 1). -define(S2S_OVERLOAD_BLOCK_PERIOD, 60). %% once a server is temporary blocked, it stay blocked for 60 seconds -record(s2s, {fromto :: {binary(), binary()} | '_', pid :: pid()}). -record(state, {}). -record(temporarily_blocked, {host :: binary(), timestamp :: integer()}). -type temporarily_blocked() :: #temporarily_blocked{}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec stop() -> ok. stop() -> _ = supervisor:terminate_child(ejabberd_sup, ?MODULE), _ = supervisor:delete_child(ejabberd_sup, ?MODULE), ok. clean_temporarily_blocked_table() -> mnesia:clear_table(temporarily_blocked). -spec list_temporarily_blocked_hosts() -> [temporarily_blocked()]. list_temporarily_blocked_hosts() -> ets:tab2list(temporarily_blocked). -spec external_host_overloaded(binary()) -> {aborted, any()} | {atomic, ok}. external_host_overloaded(Host) -> ?INFO_MSG("Disabling s2s connections to ~ts for ~p seconds", [Host, ?S2S_OVERLOAD_BLOCK_PERIOD]), mnesia:transaction(fun () -> Time = erlang:monotonic_time(), mnesia:write(#temporarily_blocked{host = Host, timestamp = Time}) end). -spec is_temporarly_blocked(binary()) -> boolean(). is_temporarly_blocked(Host) -> case mnesia:dirty_read(temporarily_blocked, Host) of [] -> false; [#temporarily_blocked{timestamp = T} = Entry] -> Diff = erlang:monotonic_time() - T, case erlang:convert_time_unit(Diff, native, microsecond) of N when N > (?S2S_OVERLOAD_BLOCK_PERIOD) * 1000 * 1000 -> mnesia:dirty_delete_object(Entry), false; _ -> true end end. -spec have_connection({binary(), binary()}) -> boolean(). have_connection(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of [_] -> true; _ -> false end. -spec get_connections_pids({binary(), binary()}) -> [pid()]. get_connections_pids(FromTo) -> case catch mnesia:dirty_read(s2s, FromTo) of L when is_list(L) -> [Connection#s2s.pid || Connection <- L]; _ -> [] end. -spec dirty_get_connections() -> [{binary(), binary()}]. dirty_get_connections() -> mnesia:dirty_all_keys(s2s). -spec tls_options(binary(), binary(), [proplists:property()]) -> [proplists:property()]. tls_options(LServer, ServerHost, DefaultOpts) -> TLSOpts1 = case ejabberd_pkix:get_certfile(LServer) of error -> DefaultOpts; {ok, CertFile} -> lists:keystore(certfile, 1, DefaultOpts, {certfile, CertFile}) end, TLSOpts2 = case ejabberd_option:s2s_ciphers(ServerHost) of undefined -> TLSOpts1; Ciphers -> lists:keystore(ciphers, 1, TLSOpts1, {ciphers, Ciphers}) end, TLSOpts3 = case ejabberd_option:s2s_protocol_options(ServerHost) of undefined -> TLSOpts2; ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2, {protocol_options, ProtoOpts}) end, TLSOpts4 = case ejabberd_option:s2s_dhfile(ServerHost) of undefined -> TLSOpts3; DHFile -> lists:keystore(dhfile, 1, TLSOpts3, {dhfile, DHFile}) end, TLSOpts5 = case lists:keymember(cafile, 1, TLSOpts4) of true -> TLSOpts4; false -> [{cafile, get_cafile(ServerHost)}|TLSOpts4] end, case ejabberd_option:s2s_tls_compression(ServerHost) of undefined -> TLSOpts5; false -> [compression_none | TLSOpts5]; true -> lists:delete(compression_none, TLSOpts5) end. -spec tls_required(binary()) -> boolean(). tls_required(LServer) -> TLS = use_starttls(LServer), TLS == required. -spec tls_enabled(binary()) -> boolean(). tls_enabled(LServer) -> TLS = use_starttls(LServer), TLS /= false. -spec zlib_enabled(binary()) -> boolean(). zlib_enabled(LServer) -> ejabberd_option:s2s_zlib(LServer). -spec use_starttls(binary()) -> boolean() | optional | required. use_starttls(LServer) -> ejabberd_option:s2s_use_starttls(LServer). -spec get_idle_timeout(binary()) -> non_neg_integer() | infinity. get_idle_timeout(LServer) -> ejabberd_option:s2s_timeout(LServer). -spec queue_type(binary()) -> ram | file. queue_type(LServer) -> ejabberd_option:s2s_queue_type(LServer). -spec get_cafile(binary()) -> file:filename_all() | undefined. get_cafile(LServer) -> case ejabberd_option:s2s_cafile(LServer) of undefined -> ejabberd_option:ca_file(); File -> File end. %%==================================================================== %% gen_server callbacks %%==================================================================== init([]) -> update_tables(), ejabberd_mnesia:create(?MODULE, s2s, [{ram_copies, [node()]}, {type, bag}, {attributes, record_info(fields, s2s)}]), case mnesia:subscribe(system) of {ok, _} -> ejabberd_commands:register_commands(get_commands_spec()), ejabberd_mnesia:create( ?MODULE, temporarily_blocked, [{ram_copies, [node()]}, {attributes, record_info(fields, temporarily_blocked)}]), ejabberd_hooks:add(host_up, ?MODULE, host_up, 50), ejabberd_hooks:add(host_down, ?MODULE, host_down, 60), lists:foreach(fun host_up/1, ejabberd_option:hosts()), {ok, #state{}}; {error, Reason} -> {stop, Reason} end. handle_call({new_connection, Args}, _From, State) -> {reply, erlang:apply(fun new_connection_int/7, Args), State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({mnesia_system_event, {mnesia_down, Node}}, State) -> clean_table_from_bad_node(Node), {noreply, State}; handle_info({route, Packet}, State) -> try route(Packet) catch ?EX_RULE(Class, Reason, St) -> StackTrace = ?EX_STACK(St), ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", [xmpp:pp(Packet), misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; handle_info({'DOWN', _Ref, process, Pid, _Reason}, State) -> case mnesia:dirty_match_object(s2s, #s2s{fromto = '_', pid = Pid}) of [#s2s{pid = Pid, fromto = {From, To}} = Obj] -> F = fun() -> mnesia:delete_object(Obj) end, case mnesia:transaction(F) of {atomic, _} -> ok; {aborted, Reason} -> ?ERROR_MSG("Failed to unregister s2s connection for pid ~p (~ts -> ~ts):" "Mnesia failure: ~p", [Pid, From, To, Reason]) end, {noreply, State}; _ -> {noreply, State} end; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_commands:unregister_commands(get_commands_spec()), stop_s2s_connections(stream_error()), lists:foreach(fun host_down/1, ejabberd_option:hosts()), ejabberd_hooks:delete(host_up, ?MODULE, host_up, 50), ejabberd_hooks:delete(host_down, ?MODULE, host_down, 60). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_s2s_in:host_up(Host), ejabberd_s2s_out:host_up(Host). -spec host_down(binary()) -> ok. host_down(Host) -> Err = stream_error(), lists:foreach( fun(#s2s{fromto = {From, _}, pid = Pid}) when node(Pid) == node() -> case ejabberd_router:host_of_route(From) of Host -> ejabberd_s2s_out:send(Pid, Err), ejabberd_s2s_out:stop_async(Pid); _ -> ok end; (_) -> ok end, ets:tab2list(s2s)), ejabberd_s2s_in:host_down(Host), ejabberd_s2s_out:host_down(Host). -spec clean_table_from_bad_node(node()) -> any(). clean_table_from_bad_node(Node) -> F = fun() -> Es = mnesia:select( s2s, ets:fun2ms( fun(#s2s{pid = Pid} = E) when node(Pid) == Node -> E end)), lists:foreach(fun mnesia:delete_object/1, Es) end, mnesia:async_dirty(F). -spec route(stanza()) -> ok. route(Packet) -> ?DEBUG("Local route:~n~ts", [xmpp:pp(Packet)]), From = xmpp:get_from(Packet), To = xmpp:get_to(Packet), case start_connection(From, To) of {ok, Pid} when is_pid(Pid) -> ?DEBUG("Sending to process ~p~n", [Pid]), #jid{lserver = MyServer} = From, case ejabberd_hooks:run_fold(s2s_send_packet, MyServer, Packet, []) of drop -> ok; Packet1 -> ejabberd_s2s_out:route(Pid, Packet1) end; {error, Reason} -> Lang = xmpp:get_lang(Packet), Err = case Reason of forbidden -> xmpp:err_forbidden(?T("Access denied by service policy"), Lang); internal_server_error -> xmpp:err_internal_server_error() end, ejabberd_router:route_error(Packet, Err) end. -spec start_connection(jid(), jid()) -> {ok, pid()} | {error, forbidden | internal_server_error}. start_connection(From, To) -> start_connection(From, To, []). -spec start_connection(jid(), jid(), [proplists:property()]) -> {ok, pid()} | {error, forbidden | internal_server_error}. start_connection(From, To, Opts) -> #jid{lserver = MyServer} = From, #jid{lserver = Server} = To, FromTo = {MyServer, Server}, MaxS2SConnectionsNumber = max_s2s_connections_number(FromTo), MaxS2SConnectionsNumberPerNode = max_s2s_connections_number_per_node(FromTo), ?DEBUG("Finding connection for ~p~n", [FromTo]), case mnesia:dirty_read(s2s, FromTo) of [] -> %% We try to establish all the connections if the host is not a %% service and if the s2s host is not blacklisted or %% is in whitelist: LServer = ejabberd_router:host_of_route(MyServer), case allow_host(LServer, Server) of true -> NeededConnections = needed_connections_number( [], MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), open_several_connections(NeededConnections, MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); false -> {error, forbidden} end; L when is_list(L) -> NeededConnections = needed_connections_number(L, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), if NeededConnections > 0 -> %% We establish the missing connections for this pair. open_several_connections(NeededConnections, MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); true -> %% We choose a connexion from the pool of opened ones. {ok, choose_connection(From, L)} end end. -spec choose_connection(jid(), [#s2s{}]) -> pid(). choose_connection(From, Connections) -> choose_pid(From, [C#s2s.pid || C <- Connections]). -spec choose_pid(jid(), [pid()]) -> pid(). choose_pid(From, Pids) -> Pids1 = case [P || P <- Pids, node(P) == node()] of [] -> Pids; Ps -> Ps end, Pid = lists:nth(erlang:phash2(jid:remove_resource(From), length(Pids1))+1, Pids1), ?DEBUG("Using ejabberd_s2s_out ~p~n", [Pid]), Pid. -spec open_several_connections(pos_integer(), binary(), binary(), jid(), {binary(), binary()}, integer(), integer(), [proplists:property()]) -> {ok, pid()} | {error, internal_server_error}. open_several_connections(N, MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> case lists:flatmap( fun(_) -> new_connection(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) end, lists:seq(1, N)) of [] -> {error, internal_server_error}; PIDs -> {ok, choose_pid(From, PIDs)} end. -spec new_connection(binary(), binary(), jid(), {binary(), binary()}, integer(), integer(), [proplists:property()]) -> [pid()]. new_connection(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> case whereis(ejabberd_s2s) == self() of true -> new_connection_int(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts); false -> gen_server:call(ejabberd_s2s, {new_connection, [MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts]}) end. new_connection_int(MyServer, Server, From, FromTo, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode, Opts) -> {ok, Pid} = ejabberd_s2s_out:start(MyServer, Server, Opts), F = fun() -> L = mnesia:read({s2s, FromTo}), NeededConnections = needed_connections_number(L, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode), if NeededConnections > 0 -> mnesia:write(#s2s{fromto = FromTo, pid = Pid}), Pid; true -> choose_connection(From, L) end end, TRes = mnesia:transaction(F), case TRes of {atomic, Pid1} -> if Pid1 == Pid -> erlang:monitor(process, Pid), ejabberd_s2s_out:connect(Pid); true -> ejabberd_s2s_out:stop_async(Pid) end, [Pid1]; {aborted, Reason} -> ?ERROR_MSG("Failed to register s2s connection ~ts -> ~ts: " "Mnesia failure: ~p", [MyServer, Server, Reason]), ejabberd_s2s_out:stop_async(Pid), [] end. -spec max_s2s_connections_number({binary(), binary()}) -> pos_integer(). max_s2s_connections_number({From, To}) -> case ejabberd_shaper:match(From, max_s2s_connections, jid:make(To)) of Max when is_integer(Max) -> Max; _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER end. -spec max_s2s_connections_number_per_node({binary(), binary()}) -> pos_integer(). max_s2s_connections_number_per_node({From, To}) -> case ejabberd_shaper:match(From, max_s2s_connections_per_node, jid:make(To)) of Max when is_integer(Max) -> Max; _ -> ?DEFAULT_MAX_S2S_CONNECTIONS_NUMBER_PER_NODE end. -spec needed_connections_number([#s2s{}], integer(), integer()) -> integer(). needed_connections_number(Ls, MaxS2SConnectionsNumber, MaxS2SConnectionsNumberPerNode) -> LocalLs = [L || L <- Ls, node(L#s2s.pid) == node()], lists:min([MaxS2SConnectionsNumber - length(Ls), MaxS2SConnectionsNumberPerNode - length(LocalLs)]). %%%---------------------------------------------------------------------- %%% ejabberd commands get_commands_spec() -> [#ejabberd_commands{ name = incoming_s2s_number, tags = [statistics, s2s], desc = "Number of incoming s2s connections on the node", policy = admin, module = ?MODULE, function = incoming_s2s_number, args = [], result = {s2s_incoming, integer}}, #ejabberd_commands{ name = outgoing_s2s_number, tags = [statistics, s2s], desc = "Number of outgoing s2s connections on the node", policy = admin, module = ?MODULE, function = outgoing_s2s_number, args = [], result = {s2s_outgoing, integer}}, #ejabberd_commands{ name = stop_s2s_connections, tags = [s2s], desc = "Stop all s2s outgoing and incoming connections", policy = admin, module = ?MODULE, function = stop_s2s_connections, args = [], result = {res, rescode}}]. %% TODO Move those stats commands to ejabberd stats command ? incoming_s2s_number() -> supervisor_count(ejabberd_s2s_in_sup). outgoing_s2s_number() -> supervisor_count(ejabberd_s2s_out_sup). -spec supervisor_count(atom()) -> non_neg_integer(). supervisor_count(Supervisor) -> try supervisor:count_children(Supervisor) of Props -> proplists:get_value(workers, Props, 0) catch _:_ -> 0 end. -spec stop_s2s_connections() -> ok. stop_s2s_connections() -> stop_s2s_connections(xmpp:serr_reset()). -spec stop_s2s_connections(stream_error()) -> ok. stop_s2s_connections(Err) -> lists:foreach( fun({_Id, Pid, _Type, _Module}) -> ejabberd_s2s_in:send(Pid, Err), ejabberd_s2s_in:stop_async(Pid), supervisor:terminate_child(ejabberd_s2s_in_sup, Pid) end, supervisor:which_children(ejabberd_s2s_in_sup)), lists:foreach( fun({_Id, Pid, _Type, _Module}) -> ejabberd_s2s_out:send(Pid, Err), ejabberd_s2s_out:stop_async(Pid), supervisor:terminate_child(ejabberd_s2s_out_sup, Pid) end, supervisor:which_children(ejabberd_s2s_out_sup)), _ = mnesia:clear_table(s2s), ok. -spec stream_error() -> stream_error(). stream_error() -> case ejabberd_cluster:get_nodes() of [Node] when Node == node() -> xmpp:serr_system_shutdown(); _ -> xmpp:serr_reset() end. %%%---------------------------------------------------------------------- %%% Update Mnesia tables update_tables() -> _ = mnesia:delete_table(local_s2s), ok. %% Check if host is in blacklist or white list -spec allow_host(binary(), binary()) -> boolean(). allow_host(MyServer, S2SHost) -> allow_host1(MyServer, S2SHost) andalso not is_temporarly_blocked(S2SHost). -spec allow_host1(binary(), binary()) -> boolean(). allow_host1(MyHost, S2SHost) -> Rule = ejabberd_option:s2s_access(MyHost), JID = jid:make(S2SHost), case acl:match_rule(MyHost, Rule, JID) of deny -> false; allow -> case ejabberd_hooks:run_fold(s2s_allow_host, MyHost, allow, [MyHost, S2SHost]) of deny -> false; allow -> true end end. %% Get information about S2S connections of the specified type. %% @spec (Type) -> [Info] %% where Type = in | out %% Info = [{InfoName::atom(), InfoValue::any()}] get_info_s2s_connections(Type) -> ChildType = case Type of in -> ejabberd_s2s_in_sup; out -> ejabberd_s2s_out_sup end, Connections = supervisor:which_children(ChildType), get_s2s_info(Connections, Type). get_s2s_info(Connections, Type) -> complete_s2s_info(Connections, Type, []). complete_s2s_info([], _, Result) -> Result; complete_s2s_info([Connection | T], Type, Result) -> {_, PID, _, _} = Connection, State = get_s2s_state(PID), complete_s2s_info(T, Type, [State | Result]). -spec get_s2s_state(pid()) -> [{status, open | closed | error} | {s2s_pid, pid()}]. get_s2s_state(S2sPid) -> Infos = case p1_fsm:sync_send_all_state_event(S2sPid, get_state_infos) of {state_infos, Is} -> [{status, open} | Is]; {noproc, _} -> [{status, closed}]; %% Connection closed {badrpc, _} -> [{status, error}] end, [{s2s_pid, S2sPid} | Infos]. ejabberd-21.12/src/ejabberd_c2s.erl0000644000232200023220000010663614154362354017446 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Created : 8 Dec 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%------------------------------------------------------------------- -module(ejabberd_c2s). -behaviour(xmpp_stream_in). -behaviour(ejabberd_listener). -protocol({rfc, 6121}). %% ejabberd_listener callbacks -export([start/3, start_link/3, accept/1, listen_opt_type/1, listen_options/0]). %% xmpp_stream_in callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([tls_options/1, tls_required/1, tls_enabled/1, compress_methods/1, bind/2, sasl_mechanisms/2, get_password_fun/2, check_password_fun/2, check_password_digest_fun/2, unauthenticated_stream_features/1, authenticated_stream_features/1, handle_stream_start/2, handle_stream_end/2, handle_unauthenticated_packet/2, handle_authenticated_packet/2, handle_auth_success/4, handle_auth_failure/4, handle_send/3, handle_recv/3, handle_cdata/2, handle_unbinded_packet/2]). %% Hooks -export([handle_unexpected_cast/2, handle_unexpected_call/3, process_auth_result/3, reject_unauthenticated_packet/2, process_closed/2, process_terminated/2, process_info/2]). %% API -export([get_presence/1, set_presence/2, resend_presence/1, resend_presence/2, open_session/1, call/3, cast/2, send/2, close/1, close/2, stop_async/1, reply/2, copy_state/2, set_timeout/2, route/2, format_reason/2, host_up/1, host_down/1, send_ws_ping/1, bounce_message_queue/2]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("mod_roster.hrl"). -include("translate.hrl"). -define(SETS, gb_sets). -type state() :: xmpp_stream_in:state(). -export_type([state/0]). %%%=================================================================== %%% ejabberd_listener API %%%=================================================================== start(SockMod, Socket, Opts) -> xmpp_stream_in:start(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). start_link(SockMod, Socket, Opts) -> xmpp_stream_in:start_link(?MODULE, [{SockMod, Socket}, Opts], ejabberd_config:fsm_limit_opts(Opts)). accept(Ref) -> xmpp_stream_in:accept(Ref). %%%=================================================================== %%% Common API %%%=================================================================== -spec call(pid(), term(), non_neg_integer() | infinity) -> term(). call(Ref, Msg, Timeout) -> xmpp_stream_in:call(Ref, Msg, Timeout). -spec cast(pid(), term()) -> ok. cast(Ref, Msg) -> xmpp_stream_in:cast(Ref, Msg). reply(Ref, Reply) -> xmpp_stream_in:reply(Ref, Reply). -spec get_presence(pid()) -> presence(). get_presence(Ref) -> call(Ref, get_presence, 1000). -spec set_presence(pid(), presence()) -> ok. set_presence(Ref, Pres) -> call(Ref, {set_presence, Pres}, 1000). -spec resend_presence(pid()) -> boolean(). resend_presence(Pid) -> resend_presence(Pid, undefined). -spec resend_presence(pid(), jid() | undefined) -> boolean(). resend_presence(Pid, To) -> route(Pid, {resend_presence, To}). -spec close(pid()) -> ok; (state()) -> state(). close(Ref) -> xmpp_stream_in:close(Ref). -spec close(pid(), atom()) -> ok. close(Ref, Reason) -> xmpp_stream_in:close(Ref, Reason). -spec stop_async(pid()) -> ok. stop_async(Pid) -> xmpp_stream_in:stop_async(Pid). -spec send(pid(), xmpp_element()) -> ok; (state(), xmpp_element()) -> state(). send(Pid, Pkt) when is_pid(Pid) -> xmpp_stream_in:send(Pid, Pkt); send(#{lserver := LServer} = State, Pkt) -> Pkt1 = fix_from_to(Pkt, State), case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt1, State}, []) of {drop, State1} -> State1; {Pkt2, State1} -> xmpp_stream_in:send(State1, Pkt2) end. -spec send_error(state(), xmpp_element(), stanza_error()) -> state(). send_error(#{lserver := LServer} = State, Pkt, Err) -> case ejabberd_hooks:run_fold(c2s_filter_send, LServer, {Pkt, State}, []) of {drop, State1} -> State1; {Pkt1, State1} -> xmpp_stream_in:send_error(State1, Pkt1, Err) end. -spec send_ws_ping(pid()) -> ok; (state()) -> state(). send_ws_ping(Ref) -> xmpp_stream_in:send_ws_ping(Ref). -spec route(pid(), term()) -> boolean(). route(Pid, Term) -> ejabberd_cluster:send(Pid, Term). -spec set_timeout(state(), timeout()) -> state(). set_timeout(State, Timeout) -> xmpp_stream_in:set_timeout(State, Timeout). -spec host_up(binary()) -> ok. host_up(Host) -> ejabberd_hooks:add(c2s_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:add(c2s_terminated, Host, ?MODULE, process_terminated, 100), ejabberd_hooks:add(c2s_unauthenticated_packet, Host, ?MODULE, reject_unauthenticated_packet, 100), ejabberd_hooks:add(c2s_handle_info, Host, ?MODULE, process_info, 100), ejabberd_hooks:add(c2s_auth_result, Host, ?MODULE, process_auth_result, 100), ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100), ejabberd_hooks:add(c2s_handle_call, Host, ?MODULE, handle_unexpected_call, 100). -spec host_down(binary()) -> ok. host_down(Host) -> ejabberd_hooks:delete(c2s_closed, Host, ?MODULE, process_closed, 100), ejabberd_hooks:delete(c2s_terminated, Host, ?MODULE, process_terminated, 100), ejabberd_hooks:delete(c2s_unauthenticated_packet, Host, ?MODULE, reject_unauthenticated_packet, 100), ejabberd_hooks:delete(c2s_handle_info, Host, ?MODULE, process_info, 100), ejabberd_hooks:delete(c2s_auth_result, Host, ?MODULE, process_auth_result, 100), ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, handle_unexpected_cast, 100), ejabberd_hooks:delete(c2s_handle_call, Host, ?MODULE, handle_unexpected_call, 100). %% Copies content of one c2s state to another. %% This is needed for session migration from one pid to another. -spec copy_state(state(), state()) -> state(). copy_state(NewState, #{jid := JID, resource := Resource, auth_module := AuthModule, lserver := LServer, pres_a := PresA} = OldState) -> State1 = case OldState of #{pres_last := Pres, pres_timestamp := PresTS} -> NewState#{pres_last => Pres, pres_timestamp => PresTS}; _ -> NewState end, Conn = get_conn_type(State1), State2 = State1#{jid => JID, resource => Resource, conn => Conn, auth_module => AuthModule, pres_a => PresA}, ejabberd_hooks:run_fold(c2s_copy_session, LServer, State2, [OldState]). -spec open_session(state()) -> {ok, state()} | state(). open_session(#{user := U, server := S, resource := R, sid := SID, ip := IP, auth_module := AuthModule} = State) -> JID = jid:make(U, S, R), State1 = change_shaper(State), Conn = get_conn_type(State1), State2 = State1#{conn => Conn, resource => R, jid => JID}, Prio = case maps:get(pres_last, State, undefined) of undefined -> undefined; Pres -> get_priority_from_presence(Pres) end, Info = [{ip, IP}, {conn, Conn}, {auth_module, AuthModule}], ejabberd_sm:open_session(SID, U, S, R, Prio, Info), xmpp_stream_in:establish(State2). %%%=================================================================== %%% Hooks %%%=================================================================== process_info(#{lserver := LServer} = State, {route, Packet}) -> {Pass, State1} = case Packet of #presence{} -> process_presence_in(State, Packet); #message{} -> process_message_in(State, Packet); #iq{} -> process_iq_in(State, Packet) end, if Pass -> {Packet1, State2} = ejabberd_hooks:run_fold( user_receive_packet, LServer, {Packet, State1}, []), case Packet1 of drop -> State2; _ -> send(State2, Packet1) end; true -> State1 end; process_info(#{jid := JID} = State, {resend_presence, To}) -> case maps:get(pres_last, State, error) of error -> State; Pres when To == undefined -> process_self_presence(State, Pres); Pres when To#jid.luser == JID#jid.luser andalso To#jid.lserver == JID#jid.lserver andalso To#jid.lresource == <<"">> -> process_self_presence(State, Pres); Pres -> process_presence_out(State, xmpp:set_to(Pres, To)) end; process_info(State, Info) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), State. handle_unexpected_call(State, From, Msg) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Msg]), State. handle_unexpected_cast(State, Msg) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), State. reject_unauthenticated_packet(State, _Pkt) -> Err = xmpp:serr_not_authorized(), send(State, Err). process_auth_result(#{sasl_mech := Mech, auth_module := AuthModule, socket := Socket, ip := IP, lserver := LServer} = State, true, User) -> ?INFO_MSG("(~ts) Accepted c2s ~ts authentication for ~ts@~ts by ~ts backend from ~ts", [xmpp_socket:pp(Socket), Mech, User, LServer, ejabberd_auth:backend_type(AuthModule), ejabberd_config:may_hide_data(misc:ip_to_list(IP))]), State; process_auth_result(#{sasl_mech := Mech, socket := Socket, ip := IP, lserver := LServer} = State, {false, Reason}, User) -> ?WARNING_MSG("(~ts) Failed c2s ~ts authentication ~tsfrom ~ts: ~ts", [xmpp_socket:pp(Socket), Mech, if User /= <<"">> -> ["for ", User, "@", LServer, " "]; true -> "" end, ejabberd_config:may_hide_data(misc:ip_to_list(IP)), Reason]), State. process_closed(State, Reason) -> stop_async(self()), State#{stop_reason => Reason}. process_terminated(#{sid := SID, socket := Socket, jid := JID, user := U, server := S, resource := R} = State, Reason) -> Status = format_reason(State, Reason), ?INFO_MSG("(~ts) Closing c2s session for ~ts: ~ts", [xmpp_socket:pp(Socket), jid:encode(JID), Status]), Pres = #presence{type = unavailable, from = JID, to = jid:remove_resource(JID)}, State1 = case maps:is_key(pres_last, State) of true -> ejabberd_sm:close_session_unset_presence(SID, U, S, R, Status), broadcast_presence_unavailable(State, Pres, true); false -> ejabberd_sm:close_session(SID, U, S, R), broadcast_presence_unavailable(State, Pres, false) end, bounce_message_queue(SID, JID), State1; process_terminated(#{socket := Socket, stop_reason := {tls, _}} = State, Reason) -> ?WARNING_MSG("(~ts) Failed to secure c2s connection: ~ts", [xmpp_socket:pp(Socket), format_reason(State, Reason)]), State; process_terminated(State, _Reason) -> State. %%%=================================================================== %%% xmpp_stream_in callbacks %%%=================================================================== tls_options(#{lserver := LServer, tls_options := DefaultOpts, stream_encrypted := Encrypted}) -> TLSOpts1 = case {Encrypted, proplists:get_value(certfile, DefaultOpts)} of {true, CertFile} when CertFile /= undefined -> DefaultOpts; {_, _} -> case ejabberd_pkix:get_certfile(LServer) of error -> DefaultOpts; {ok, CertFile} -> lists:keystore(certfile, 1, DefaultOpts, {certfile, CertFile}) end end, TLSOpts2 = case ejabberd_option:c2s_ciphers(LServer) of undefined -> TLSOpts1; Ciphers -> lists:keystore(ciphers, 1, TLSOpts1, {ciphers, Ciphers}) end, TLSOpts3 = case ejabberd_option:c2s_protocol_options(LServer) of undefined -> TLSOpts2; ProtoOpts -> lists:keystore(protocol_options, 1, TLSOpts2, {protocol_options, ProtoOpts}) end, TLSOpts4 = case ejabberd_option:c2s_dhfile(LServer) of undefined -> TLSOpts3; DHFile -> lists:keystore(dhfile, 1, TLSOpts3, {dhfile, DHFile}) end, TLSOpts5 = case ejabberd_option:c2s_cafile(LServer) of undefined -> TLSOpts4; CAFile -> lists:keystore(cafile, 1, TLSOpts4, {cafile, CAFile}) end, case ejabberd_option:c2s_tls_compression(LServer) of undefined -> TLSOpts5; false -> [compression_none | TLSOpts5]; true -> lists:delete(compression_none, TLSOpts5) end. tls_required(#{tls_required := TLSRequired}) -> TLSRequired. tls_enabled(#{tls_enabled := TLSEnabled, tls_required := TLSRequired, tls_verify := TLSVerify}) -> TLSEnabled or TLSRequired or TLSVerify. compress_methods(#{zlib := true}) -> [<<"zlib">>]; compress_methods(_) -> []. unauthenticated_stream_features(#{lserver := LServer}) -> ejabberd_hooks:run_fold(c2s_pre_auth_features, LServer, [], [LServer]). authenticated_stream_features(#{lserver := LServer}) -> ejabberd_hooks:run_fold(c2s_post_auth_features, LServer, [], [LServer]). sasl_mechanisms(Mechs, #{lserver := LServer, stream_encrypted := Encrypted} = State) -> Type = ejabberd_auth:store_type(LServer), Mechs1 = ejabberd_option:disable_sasl_mechanisms(LServer), ScramHash = ejabberd_option:auth_scram_hash(LServer), ShaAv = Type == plain orelse (Type == scram andalso ScramHash == sha), Sha256Av = Type == plain orelse (Type == scram andalso ScramHash == sha256), Sha512Av = Type == plain orelse (Type == scram andalso ScramHash == sha512), %% I re-created it from cyrsasl ets magic, but I think it's wrong %% TODO: need to check before 18.09 release lists:filter( fun(<<"ANONYMOUS">>) -> ejabberd_auth_anonymous:is_sasl_anonymous_enabled(LServer); (<<"DIGEST-MD5">>) -> Type == plain; (<<"SCRAM-SHA-1">>) -> ShaAv; (<<"SCRAM-SHA-1-PLUS">>) -> ShaAv andalso Encrypted; (<<"SCRAM-SHA-256">>) -> Sha256Av; (<<"SCRAM-SHA-256-PLUS">>) -> Sha256Av andalso Encrypted; (<<"SCRAM-SHA-512">>) -> Sha512Av; (<<"SCRAM-SHA-512-PLUS">>) -> Sha512Av andalso Encrypted; (<<"PLAIN">>) -> true; (<<"X-OAUTH2">>) -> [ejabberd_auth_anonymous] /= ejabberd_auth:auth_modules(LServer); (<<"EXTERNAL">>) -> maps:get(tls_verify, State, false); (_) -> false end, Mechs -- Mechs1). get_password_fun(_Mech, #{lserver := LServer}) -> fun(U) -> ejabberd_auth:get_password_with_authmodule(U, LServer) end. check_password_fun(<<"X-OAUTH2">>, #{lserver := LServer}) -> fun(User, _AuthzId, Token) -> case ejabberd_oauth:check_token( User, LServer, [<<"sasl_auth">>], Token) of true -> {true, ejabberd_oauth}; _ -> {false, ejabberd_oauth} end end; check_password_fun(_Mech, #{lserver := LServer}) -> fun(U, AuthzId, P) -> ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P) end. check_password_digest_fun(_Mech, #{lserver := LServer}) -> fun(U, AuthzId, P, D, DG) -> ejabberd_auth:check_password_with_authmodule(U, AuthzId, LServer, P, D, DG) end. bind(<<"">>, State) -> bind(new_uniq_id(), State); bind(R, #{user := U, server := S, access := Access, lang := Lang, lserver := LServer, socket := Socket, ip := IP} = State) -> case resource_conflict_action(U, S, R) of closenew -> {error, xmpp:err_conflict(), State}; {accept_resource, Resource} -> JID = jid:make(U, S, Resource), case acl:match_rule(LServer, Access, #{usr => jid:split(JID), ip => IP}) of allow -> State1 = open_session(State#{resource => Resource, sid => ejabberd_sm:make_sid()}), State2 = ejabberd_hooks:run_fold( c2s_session_opened, LServer, State1, []), ?INFO_MSG("(~ts) Opened c2s session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)]), {ok, State2}; deny -> ejabberd_hooks:run(forbidden_session_hook, LServer, [JID]), ?WARNING_MSG("(~ts) Forbidden c2s session for ~ts", [xmpp_socket:pp(Socket), jid:encode(JID)]), Txt = ?T("Access denied by service policy"), {error, xmpp:err_not_allowed(Txt, Lang), State} end end. handle_stream_start(StreamStart, #{lserver := LServer} = State) -> case ejabberd_router:is_my_host(LServer) of false -> send(State#{lserver => ejabberd_config:get_myname()}, xmpp:serr_host_unknown()); true -> State1 = change_shaper(State), Opts = ejabberd_config:codec_options(), State2 = State1#{codec_options => Opts}, ejabberd_hooks:run_fold( c2s_stream_started, LServer, State2, [StreamStart]) end. handle_stream_end(Reason, #{lserver := LServer} = State) -> State1 = State#{stop_reason => Reason}, ejabberd_hooks:run_fold(c2s_closed, LServer, State1, [Reason]). handle_auth_success(User, _Mech, AuthModule, #{lserver := LServer} = State) -> State1 = State#{auth_module => AuthModule}, ejabberd_hooks:run_fold(c2s_auth_result, LServer, State1, [true, User]). handle_auth_failure(User, _Mech, Reason, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_auth_result, LServer, State, [{false, Reason}, User]). handle_unbinded_packet(Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_unbinded_packet, LServer, State, [Pkt]). handle_unauthenticated_packet(Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_unauthenticated_packet, LServer, State, [Pkt]). handle_authenticated_packet(Pkt, #{lserver := LServer} = State) when not ?is_stanza(Pkt) -> ejabberd_hooks:run_fold(c2s_authenticated_packet, LServer, State, [Pkt]); handle_authenticated_packet(Pkt, #{lserver := LServer, jid := JID, ip := {IP, _}} = State) -> Pkt1 = xmpp:put_meta(Pkt, ip, IP), State1 = ejabberd_hooks:run_fold(c2s_authenticated_packet, LServer, State, [Pkt1]), #jid{luser = LUser} = JID, {Pkt2, State2} = ejabberd_hooks:run_fold( user_send_packet, LServer, {Pkt1, State1}, []), case Pkt2 of drop -> State2; #iq{type = set, sub_els = [_]} -> try xmpp:try_subtag(Pkt2, #xmpp_session{}) of #xmpp_session{} -> % It seems that some client are expecting to have response % to session request be sent from server jid, let's make % sure it is that. Pkt3 = xmpp:set_to(Pkt2, jid:make(<<>>, LServer, <<>>)), send(State2, xmpp:make_iq_result(Pkt3)); _ -> check_privacy_then_route(State2, Pkt2) catch _:{xmpp_codec, Why} -> Txt = xmpp:io_format_error(Why), Lang = maps:get(lang, State), Err = xmpp:err_bad_request(Txt, Lang), send_error(State2, Pkt2, Err) end; #presence{to = #jid{luser = LUser, lserver = LServer, lresource = <<"">>}} -> process_self_presence(State2, Pkt2); #presence{} -> process_presence_out(State2, Pkt2); _ -> check_privacy_then_route(State2, Pkt2) end. handle_cdata(Data, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_cdata, LServer, State, [Data]). handle_recv(El, Pkt, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_recv, LServer, State, [El, Pkt]). handle_send(Pkt, Result, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_send, LServer, State, [Pkt, Result]). init([State, Opts]) -> Access = proplists:get_value(access, Opts, all), Shaper = proplists:get_value(shaper, Opts, none), TLSOpts1 = lists:filter( fun({certfile, _}) -> true; ({ciphers, _}) -> true; ({dhfile, _}) -> true; ({cafile, _}) -> true; ({protocol_options, _}) -> true; (_) -> false end, Opts), TLSOpts2 = case proplists:get_bool(tls_compression, Opts) of false -> [compression_none | TLSOpts1]; true -> TLSOpts1 end, TLSEnabled = proplists:get_bool(starttls, Opts), TLSRequired = proplists:get_bool(starttls_required, Opts), TLSVerify = proplists:get_bool(tls_verify, Opts), Zlib = proplists:get_bool(zlib, Opts), Timeout = ejabberd_option:negotiation_timeout(), State1 = State#{tls_options => TLSOpts2, tls_required => TLSRequired, tls_enabled => TLSEnabled, tls_verify => TLSVerify, pres_a => ?SETS:new(), zlib => Zlib, lang => ejabberd_option:language(), server => ejabberd_config:get_myname(), lserver => ejabberd_config:get_myname(), access => Access, shaper => Shaper}, State2 = xmpp_stream_in:set_timeout(State1, Timeout), ejabberd_hooks:run_fold(c2s_init, {ok, State2}, [Opts]). handle_call(get_presence, From, #{jid := JID} = State) -> Pres = case maps:get(pres_last, State, error) of error -> BareJID = jid:remove_resource(JID), #presence{from = JID, to = BareJID, type = unavailable}; P -> P end, reply(From, Pres), State; handle_call({set_presence, Pres}, From, State) -> reply(From, ok), process_self_presence(State, Pres); handle_call(Request, From, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold( c2s_handle_call, LServer, State, [Request, From]). handle_cast(Msg, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_cast, LServer, State, [Msg]). handle_info(Info, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_handle_info, LServer, State, [Info]). terminate(Reason, #{lserver := LServer} = State) -> ejabberd_hooks:run_fold(c2s_terminated, LServer, State, [Reason]). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec process_iq_in(state(), iq()) -> {boolean(), state()}. process_iq_in(State, #iq{} = IQ) -> case privacy_check_packet(State, IQ, in) of allow -> {true, State}; deny -> ejabberd_router:route_error(IQ, xmpp:err_service_unavailable()), {false, State} end. -spec process_message_in(state(), message()) -> {boolean(), state()}. process_message_in(State, #message{type = T} = Msg) -> %% This function should be as simple as process_iq_in/2, %% however, we don't route errors to MUC rooms in order %% to avoid kicking us, because having a MUC room's JID blocked %% most likely means having only some particular participant %% blocked, i.e. room@conference.server.org/participant. case privacy_check_packet(State, Msg, in) of allow -> {true, State}; deny when T == groupchat; T == headline -> {false, State}; deny -> case xmpp:has_subtag(Msg, #muc_user{}) of true -> ok; false -> ejabberd_router:route_error( Msg, xmpp:err_service_unavailable()) end, {false, State} end. -spec process_presence_in(state(), presence()) -> {boolean(), state()}. process_presence_in(#{lserver := LServer, pres_a := PresA} = State0, #presence{from = From, type = T} = Pres) -> State = ejabberd_hooks:run_fold(c2s_presence_in, LServer, State0, [Pres]), case T of probe -> route_probe_reply(From, State), {false, State}; error -> A = ?SETS:del_element(jid:tolower(From), PresA), {true, State#{pres_a => A}}; _ -> case privacy_check_packet(State, Pres, in) of allow -> {true, State}; deny -> {false, State} end end. -spec route_probe_reply(jid(), state()) -> ok. route_probe_reply(From, #{jid := To, pres_last := LastPres, pres_timestamp := TS} = State) -> {LUser, LServer, LResource} = jid:tolower(To), IsAnotherResource = case jid:tolower(From) of {LUser, LServer, R} when R /= LResource -> true; _ -> false end, Subscription = get_subscription(To, From), if IsAnotherResource orelse Subscription == both orelse Subscription == from -> Packet = xmpp:set_from_to(LastPres, To, From), Packet2 = misc:add_delay_info(Packet, To, TS), case privacy_check_packet(State, Packet2, out) of deny -> ok; allow -> ejabberd_hooks:run(presence_probe_hook, LServer, [From, To, self()]), ejabberd_router:route(Packet2) end; true -> ok end; route_probe_reply(_, _) -> ok. -spec process_presence_out(state(), presence()) -> state(). process_presence_out(#{lserver := LServer, jid := JID, lang := Lang, pres_a := PresA} = State0, #presence{from = From, to = To, type = Type} = Pres) -> State1 = if Type == subscribe; Type == subscribed; Type == unsubscribe; Type == unsubscribed -> Access = mod_roster_opt:access(LServer), MyBareJID = jid:remove_resource(JID), case acl:match_rule(LServer, Access, MyBareJID) of deny -> AccessErrTxt = ?T("Access denied by service policy"), AccessErr = xmpp:err_forbidden(AccessErrTxt, Lang), send_error(State0, Pres, AccessErr); allow -> ejabberd_hooks:run(roster_out_subscription, LServer, [Pres]), State0 end; true -> State0 end, case privacy_check_packet(State1, Pres, out) of deny -> PrivErrTxt = ?T("Your active privacy list has denied " "the routing of this stanza."), PrivErr = xmpp:err_not_acceptable(PrivErrTxt, Lang), send_error(State1, Pres, PrivErr); allow when Type == subscribe; Type == subscribed; Type == unsubscribe; Type == unsubscribed -> BareFrom = jid:remove_resource(From), ejabberd_router:route(xmpp:set_from_to(Pres, BareFrom, To)), State1; allow when Type == error; Type == probe -> ejabberd_router:route(Pres), State1; allow -> ejabberd_router:route(Pres), LTo = jid:tolower(To), LBareTo = jid:remove_resource(LTo), LBareFrom = jid:remove_resource(jid:tolower(From)), if LBareTo /= LBareFrom -> Subscription = get_subscription(From, To), if Subscription /= both andalso Subscription /= from -> A = case Type of available -> ?SETS:add_element(LTo, PresA); unavailable -> ?SETS:del_element(LTo, PresA) end, State1#{pres_a => A}; true -> State1 end; true -> State1 end end. -spec process_self_presence(state(), presence()) -> state(). process_self_presence(#{lserver := LServer, sid := SID, user := U, server := S, resource := R} = State, #presence{type = unavailable} = Pres) -> Status = xmpp:get_text(Pres#presence.status), _ = ejabberd_sm:unset_presence(SID, U, S, R, Status), {Pres1, State1} = ejabberd_hooks:run_fold( c2s_self_presence, LServer, {Pres, State}, []), State2 = broadcast_presence_unavailable(State1, Pres1, true), maps:remove(pres_last, maps:remove(pres_timestamp, State2)); process_self_presence(#{lserver := LServer} = State, #presence{type = available} = Pres) -> PreviousPres = maps:get(pres_last, State, undefined), _ = update_priority(State, Pres), {Pres1, State1} = ejabberd_hooks:run_fold( c2s_self_presence, LServer, {Pres, State}, []), State2 = State1#{pres_last => Pres1, pres_timestamp => erlang:timestamp()}, FromUnavailable = PreviousPres == undefined, broadcast_presence_available(State2, Pres1, FromUnavailable); process_self_presence(State, _Pres) -> State. -spec update_priority(state(), presence()) -> ok | {error, notfound}. update_priority(#{sid := SID, user := U, server := S, resource := R}, Pres) -> Priority = get_priority_from_presence(Pres), ejabberd_sm:set_presence(SID, U, S, R, Priority, Pres). -spec broadcast_presence_unavailable(state(), presence(), boolean()) -> state(). broadcast_presence_unavailable(#{jid := JID, pres_a := PresA} = State, Pres, BroadcastToRoster) -> #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:tolower(jid:remove_resource(JID)), Items1 = case BroadcastToRoster of true -> Roster = ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]), lists:foldl( fun(#roster{jid = LJID, subscription = Sub}, Acc) when Sub == both; Sub == from -> maps:put(LJID, 1, Acc); (_, Acc) -> Acc end, #{BareJID => 1}, Roster); _ -> #{BareJID => 1} end, Items2 = ?SETS:fold( fun(LJID, Acc) -> maps:put(LJID, 1, Acc) end, Items1, PresA), JIDs = lists:filtermap( fun(LJid) -> To = jid:make(LJid), P = xmpp:set_to(Pres, To), case privacy_check_packet(State, P, out) of allow -> {true, To}; deny -> false end end, maps:keys(Items2)), route_multiple(State, JIDs, Pres), State#{pres_a => ?SETS:new()}. -spec broadcast_presence_available(state(), presence(), boolean()) -> state(). broadcast_presence_available(#{jid := JID} = State, Pres, _FromUnavailable = true) -> Probe = #presence{from = JID, type = probe}, #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:remove_resource(JID), Items = ejabberd_hooks:run_fold(roster_get, LServer, [], [{LUser, LServer}]), {FJIDs, TJIDs} = lists:foldl( fun(#roster{jid = LJID, subscription = Sub}, {F, T}) -> To = jid:make(LJID), F1 = if Sub == both orelse Sub == from -> Pres1 = xmpp:set_to(Pres, To), case privacy_check_packet(State, Pres1, out) of allow -> [To|F]; deny -> F end; true -> F end, T1 = if Sub == both orelse Sub == to -> Probe1 = xmpp:set_to(Probe, To), case privacy_check_packet(State, Probe1, out) of allow -> [To|T]; deny -> T end; true -> T end, {F1, T1} end, {[BareJID], [BareJID]}, Items), route_multiple(State, TJIDs, Probe), route_multiple(State, FJIDs, Pres), State; broadcast_presence_available(#{jid := JID} = State, Pres, _FromUnavailable = false) -> #jid{luser = LUser, lserver = LServer} = JID, BareJID = jid:remove_resource(JID), Items = ejabberd_hooks:run_fold( roster_get, LServer, [], [{LUser, LServer}]), JIDs = lists:foldl( fun(#roster{jid = LJID, subscription = Sub}, Tos) when Sub == both orelse Sub == from -> To = jid:make(LJID), P = xmpp:set_to(Pres, jid:make(LJID)), case privacy_check_packet(State, P, out) of allow -> [To|Tos]; deny -> Tos end; (_, Tos) -> Tos end, [BareJID], Items), route_multiple(State, JIDs, Pres), State. -spec check_privacy_then_route(state(), stanza()) -> state(). check_privacy_then_route(#{lang := Lang} = State, Pkt) -> case privacy_check_packet(State, Pkt, out) of deny -> ErrText = ?T("Your active privacy list has denied " "the routing of this stanza."), Err = xmpp:err_not_acceptable(ErrText, Lang), send_error(State, Pkt, Err); allow -> ejabberd_router:route(Pkt), State end. -spec privacy_check_packet(state(), stanza(), in | out) -> allow | deny. privacy_check_packet(#{lserver := LServer} = State, Pkt, Dir) -> ejabberd_hooks:run_fold(privacy_check_packet, LServer, allow, [State, Pkt, Dir]). -spec get_priority_from_presence(presence()) -> integer(). get_priority_from_presence(#presence{priority = Prio}) -> case Prio of undefined -> 0; _ -> Prio end. -spec route_multiple(state(), [jid()], stanza()) -> ok. route_multiple(#{lserver := LServer}, JIDs, Pkt) -> From = xmpp:get_from(Pkt), ejabberd_router_multicast:route_multicast(From, LServer, JIDs, Pkt, false). get_subscription(#jid{luser = LUser, lserver = LServer}, JID) -> {Subscription, _, _} = ejabberd_hooks:run_fold( roster_get_jid_info, LServer, {none, none, []}, [LUser, LServer, JID]), Subscription. -spec resource_conflict_action(binary(), binary(), binary()) -> {accept_resource, binary()} | closenew. resource_conflict_action(U, S, R) -> OptionRaw = case ejabberd_sm:is_existing_resource(U, S, R) of true -> ejabberd_option:resource_conflict(S); false -> acceptnew end, Option = case OptionRaw of setresource -> setresource; closeold -> acceptnew; %% ejabberd_sm will close old session closenew -> closenew; acceptnew -> acceptnew end, case Option of acceptnew -> {accept_resource, R}; closenew -> closenew; setresource -> Rnew = new_uniq_id(), {accept_resource, Rnew} end. -spec bounce_message_queue(ejabberd_sm:sid(), jid:jid()) -> ok. bounce_message_queue({_, Pid} = SID, JID) -> {U, S, R} = jid:tolower(JID), SIDs = ejabberd_sm:get_session_sids(U, S, R), case lists:member(SID, SIDs) of true -> ?WARNING_MSG("The session for ~ts@~ts/~ts is supposed to " "be unregistered, but session identifier ~p " "still presents in the 'session' table", [U, S, R, Pid]); false -> receive {route, Pkt} -> ejabberd_router:route(Pkt), bounce_message_queue(SID, JID) after 100 -> ok end end. -spec new_uniq_id() -> binary(). new_uniq_id() -> iolist_to_binary( [p1_rand:get_string(), integer_to_binary(erlang:unique_integer([positive]))]). -spec get_conn_type(state()) -> c2s | c2s_tls | c2s_compressed | websocket | c2s_compressed_tls | http_bind. get_conn_type(State) -> case xmpp_stream_in:get_transport(State) of tcp -> c2s; tls -> c2s_tls; tcp_zlib -> c2s_compressed; tls_zlib -> c2s_compressed_tls; http_bind -> http_bind; websocket -> websocket end. -spec fix_from_to(xmpp_element(), state()) -> stanza(). fix_from_to(Pkt, #{jid := JID}) when ?is_stanza(Pkt) -> #jid{luser = U, lserver = S, lresource = R} = JID, case xmpp:get_from(Pkt) of undefined -> Pkt; From -> From1 = case jid:tolower(From) of {U, S, R} -> JID; {U, S, _} -> jid:replace_resource(JID, From#jid.resource); _ -> From end, To1 = case xmpp:get_to(Pkt) of #jid{lresource = <<>>} = To2 -> To2; _ -> JID end, xmpp:set_from_to(Pkt, From1, To1) end; fix_from_to(Pkt, _State) -> Pkt. -spec change_shaper(state()) -> state(). change_shaper(#{shaper := ShaperName, ip := {IP, _}, lserver := LServer, user := U, server := S, resource := R} = State) -> JID = jid:make(U, S, R), Shaper = ejabberd_shaper:match(LServer, ShaperName, #{usr => jid:split(JID), ip => IP}), xmpp_stream_in:change_shaper(State, ejabberd_shaper:new(Shaper)). -spec format_reason(state(), term()) -> binary(). format_reason(#{stop_reason := Reason}, _) -> xmpp_stream_in:format_error(Reason); format_reason(_, normal) -> <<"unknown reason">>; format_reason(_, shutdown) -> <<"stopped by supervisor">>; format_reason(_, {shutdown, _}) -> <<"stopped by supervisor">>; format_reason(_, _) -> <<"internal server error">>. listen_opt_type(starttls) -> econf:bool(); listen_opt_type(starttls_required) -> econf:bool(); listen_opt_type(tls_verify) -> econf:bool(); listen_opt_type(zlib) -> econf:and_then( econf:bool(), fun(false) -> false; (true) -> ejabberd:start_app(ezlib), true end). listen_options() -> [{access, all}, {shaper, none}, {ciphers, undefined}, {dhfile, undefined}, {cafile, undefined}, {protocol_options, undefined}, {tls, false}, {tls_compression, false}, {starttls, false}, {starttls_required, false}, {tls_verify, false}, {zlib, false}, {max_stanza_size, infinity}, {max_fsm_queue, 10000}]. ejabberd-21.12/src/mod_admin_extra.erl0000644000232200023220000017511014154362354020264 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_admin_extra.erl %%% Author : Badlop %%% Purpose : Contributed administrative functions and commands %%% Created : 10 Aug 2008 by Badlop %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_admin_extra). -author('badlop@process-one.net'). -behaviour(gen_mod). -include("logger.hrl"). -include("translate.hrl"). -export([start/2, stop/1, reload/3, mod_options/1, get_commands_spec/0, depends/2, mod_doc/0]). % Commands API -export([ % Adminsys compile/1, get_cookie/0, restart_module/2, % Sessions num_resources/2, resource_num/3, kick_session/4, status_num/2, status_num/1, status_list/2, status_list/1, connected_users_info/0, connected_users_vhost/1, set_presence/7, get_presence/2, user_sessions_info/2, get_last/2, set_last/4, % Accounts set_password/3, check_password_hash/4, delete_old_users/1, delete_old_users_vhost/2, ban_account/3, check_password/3, % vCard set_nickname/3, get_vcard/3, get_vcard/4, get_vcard_multi/4, set_vcard/4, set_vcard/5, % Roster add_rosteritem/7, delete_rosteritem/4, get_roster/2, push_roster/3, push_roster_all/1, push_alltoall/2, push_roster_item/5, build_roster_item/3, % Private storage private_get/4, private_set/3, % Shared roster srg_create/5, srg_delete/2, srg_list/1, srg_get_info/2, srg_get_members/2, srg_user_add/4, srg_user_del/4, % Send message send_message/5, send_stanza/3, send_stanza_c2s/4, % Privacy list privacy_set/3, % Stats stats/1, stats/2 ]). -include("ejabberd_commands.hrl"). -include("mod_roster.hrl"). -include("mod_privacy.hrl"). -include("ejabberd_sm.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%% %%% gen_mod %%% start(_Host, _Opts) -> ejabberd_commands:register_commands(?MODULE, get_commands_spec()). stop(Host) -> case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end. reload(_Host, _NewOpts, _OldOpts) -> ok. depends(_Host, _Opts) -> []. %%% %%% Register commands %%% get_commands_spec() -> Vcard1FieldsString = "Some vcard field names in get/set_vcard are:\n\n" "* FN - Full Name\n" "* NICKNAME - Nickname\n" "* BDAY - Birthday\n" "* TITLE - Work: Position\n" "* ROLE - Work: Role\n", Vcard2FieldsString = "Some vcard field names and subnames in get/set_vcard2 are:\n\n" "* N FAMILY - Family name\n" "* N GIVEN - Given name\n" "* N MIDDLE - Middle name\n" "* ADR CTRY - Address: Country\n" "* ADR LOCALITY - Address: City\n" "* TEL HOME - Telephone: Home\n" "* TEL CELL - Telephone: Cellphone\n" "* TEL WORK - Telephone: Work\n" "* TEL VOICE - Telephone: Voice\n" "* EMAIL USERID - E-Mail Address\n" "* ORG ORGNAME - Work: Company\n" "* ORG ORGUNIT - Work: Department\n", VcardXEP = "For a full list of vCard fields check XEP-0054: vcard-temp at " "https://xmpp.org/extensions/xep-0054.html", [ #ejabberd_commands{name = compile, tags = [erlang], desc = "Recompile and reload Erlang source code file", module = ?MODULE, function = compile, args = [{file, string}], args_example = ["/home/me/srcs/ejabberd/mod_example.erl"], args_desc = ["Filename of erlang source file to compile"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = get_cookie, tags = [erlang], desc = "Get the Erlang cookie of this node", module = ?MODULE, function = get_cookie, args = [], result = {cookie, string}, result_example = "MWTAVMODFELNLSMYXPPD", result_desc = "Erlang cookie used for authentication by ejabberd"}, #ejabberd_commands{name = restart_module, tags = [erlang], desc = "Stop an ejabberd module, reload code and start", module = ?MODULE, function = restart_module, args = [{host, binary}, {module, binary}], args_example = ["myserver.com","mod_admin_extra"], args_desc = ["Server name", "Module to restart"], result = {res, integer}, result_example = 0, result_desc = "Returns integer code:\n" " - 0: code reloaded, module restarted\n" " - 1: error: module not loaded\n" " - 2: code not reloaded, but module restarted"}, #ejabberd_commands{name = delete_old_users, tags = [accounts, purge], desc = "Delete users that didn't log in last days, or that never logged", longdesc = "To protect admin accounts, configure this for example:\n" "access_rules:\n" " protect_old_users:\n" " - allow: admin\n" " - deny: all\n", module = ?MODULE, function = delete_old_users, args = [{days, integer}], args_example = [30], args_desc = ["Last login age in days of accounts that should be removed"], result = {res, restuple}, result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, result_desc = "Result tuple"}, #ejabberd_commands{name = delete_old_users_vhost, tags = [accounts, purge], desc = "Delete users that didn't log in last days in vhost, or that never logged", longdesc = "To protect admin accounts, configure this for example:\n" "access_rules:\n" " delete_old_users:\n" " - deny: admin\n" " - allow: all\n", module = ?MODULE, function = delete_old_users_vhost, args = [{host, binary}, {days, integer}], args_example = [<<"myserver.com">>, 30], args_desc = ["Server name", "Last login age in days of accounts that should be removed"], result = {res, restuple}, result_example = {ok, <<"Deleted 2 users: [\"oldman@myserver.com\", \"test@myserver.com\"]">>}, result_desc = "Result tuple"}, #ejabberd_commands{name = check_account, tags = [accounts], desc = "Check if an account exists or not", module = ejabberd_auth, function = user_exists, args = [{user, binary}, {host, binary}], args_example = [<<"peter">>, <<"myserver.com">>], args_desc = ["User name to check", "Server to check"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password, tags = [accounts], desc = "Check if a password is correct", module = ?MODULE, function = check_password, args = [{user, binary}, {host, binary}, {password, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"secret">>], args_desc = ["User name to check", "Server to check", "Password to check"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = check_password_hash, tags = [accounts], desc = "Check if the password hash is correct", longdesc = "Allows hash methods from crypto application", module = ?MODULE, function = check_password_hash, args = [{user, binary}, {host, binary}, {passwordhash, binary}, {hashmethod, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"5ebe2294ecd0e0f08eab7690d2a6ee69">>, <<"md5">>], args_desc = ["User name to check", "Server to check", "Password's hash value", "Name of hash method"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = change_password, tags = [accounts], desc = "Change the password of an account", module = ?MODULE, function = set_password, args = [{user, binary}, {host, binary}, {newpass, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"blank">>], args_desc = ["User name", "Server name", "New password for user"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = ban_account, tags = [accounts], desc = "Ban an account: kick sessions and set random password", module = ?MODULE, function = ban_account, args = [{user, binary}, {host, binary}, {reason, binary}], args_example = [<<"attacker">>, <<"myserver.com">>, <<"Spaming other users">>], args_desc = ["User name to ban", "Server name", "Reason for banning user"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = num_resources, tags = [session], desc = "Get the number of resources of a user", module = ?MODULE, function = num_resources, args = [{user, binary}, {host, binary}], args_example = [<<"peter">>, <<"myserver.com">>], args_desc = ["User name", "Server name"], result = {resources, integer}, result_example = 5, result_desc = "Number of active resources for a user"}, #ejabberd_commands{name = resource_num, tags = [session], desc = "Resource string of a session number", module = ?MODULE, function = resource_num, args = [{user, binary}, {host, binary}, {num, integer}], args_example = [<<"peter">>, <<"myserver.com">>, 2], args_desc = ["User name", "Server name", "ID of resource to return"], result = {resource, string}, result_example = <<"Psi">>, result_desc = "Name of user resource"}, #ejabberd_commands{name = kick_session, tags = [session], desc = "Kick a user session", module = ?MODULE, function = kick_session, args = [{user, binary}, {host, binary}, {resource, binary}, {reason, binary}], args_example = [<<"peter">>, <<"myserver.com">>, <<"Psi">>, <<"Stuck connection">>], args_desc = ["User name", "Server name", "User's resource", "Reason for closing session"], result = {res, rescode}, result_example = ok, result_desc = "Status code: 0 on success, 1 otherwise"}, #ejabberd_commands{name = status_num_host, tags = [session, statistics], desc = "Number of logged users with this status in host", policy = admin, module = ?MODULE, function = status_num, args = [{host, binary}, {status, binary}], args_example = [<<"myserver.com">>, <<"dnd">>], args_desc = ["Server name", "Status type to check"], result = {users, integer}, result_example = 23, result_desc = "Number of connected sessions with given status type"}, #ejabberd_commands{name = status_num, tags = [session, statistics], desc = "Number of logged users with this status", policy = admin, module = ?MODULE, function = status_num, args = [{status, binary}], args_example = [<<"dnd">>], args_desc = ["Status type to check"], result = {users, integer}, result_example = 23, result_desc = "Number of connected sessions with given status type"}, #ejabberd_commands{name = status_list_host, tags = [session], desc = "List of users logged in host with their statuses", module = ?MODULE, function = status_list, args = [{host, binary}, {status, binary}], args_example = [<<"myserver.com">>, <<"dnd">>], args_desc = ["Server name", "Status type to check"], result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], result = {users, {list, {userstatus, {tuple, [ {user, string}, {host, string}, {resource, string}, {priority, integer}, {status, string} ]}} }}}, #ejabberd_commands{name = status_list, tags = [session], desc = "List of logged users with this status", module = ?MODULE, function = status_list, args = [{status, binary}], args_example = [<<"dnd">>], args_desc = ["Status type to check"], result_example = [{<<"peter">>,<<"myserver.com">>,<<"tka">>,6,<<"Busy">>}], result = {users, {list, {userstatus, {tuple, [ {user, string}, {host, string}, {resource, string}, {priority, integer}, {status, string} ]}} }}}, #ejabberd_commands{name = connected_users_info, tags = [session], desc = "List all established sessions and their information", module = ?MODULE, function = connected_users_info, args = [], result_example = [{"user1@myserver.com/tka", "c2s", "127.0.0.1", 42656,8, "ejabberd@localhost", 231, <<"dnd">>, <<"tka">>, <<>>}], result = {connected_users_info, {list, {session, {tuple, [{jid, string}, {connection, string}, {ip, string}, {port, integer}, {priority, integer}, {node, string}, {uptime, integer}, {status, string}, {resource, string}, {statustext, string} ]}} }}}, #ejabberd_commands{name = connected_users_vhost, tags = [session], desc = "Get the list of established sessions in a vhost", module = ?MODULE, function = connected_users_vhost, args_example = [<<"myexample.com">>], args_desc = ["Server name"], result_example = [<<"user1@myserver.com/tka">>, <<"user2@localhost/tka">>], args = [{host, binary}], result = {connected_users_vhost, {list, {sessions, string}}}}, #ejabberd_commands{name = user_sessions_info, tags = [session], desc = "Get information about all sessions of a user", module = ?MODULE, function = user_sessions_info, args = [{user, binary}, {host, binary}], args_example = [<<"peter">>, <<"myserver.com">>], args_desc = ["User name", "Server name"], result_example = [{"c2s", "127.0.0.1", 42656,8, "ejabberd@localhost", 231, <<"dnd">>, <<"tka">>, <<>>}], result = {sessions_info, {list, {session, {tuple, [{connection, string}, {ip, string}, {port, integer}, {priority, integer}, {node, string}, {uptime, integer}, {status, string}, {resource, string}, {statustext, string} ]}} }}}, #ejabberd_commands{name = get_presence, tags = [session], desc = "Retrieve the resource with highest priority, " "and its presence (show and status message) " "for a given user.", longdesc = "The 'jid' value contains the user jid " "with resource.\nThe 'show' value contains " "the user presence flag. It can take " "limited values:\n - available\n - chat " "(Free for chat)\n - away\n - dnd (Do " "not disturb)\n - xa (Not available, " "extended away)\n - unavailable (Not " "connected)\n\n'status' is a free text " "defined by the user client.", module = ?MODULE, function = get_presence, args = [{user, binary}, {host, binary}], args_rename = [{server, host}], args_example = [<<"peter">>, <<"myexample.com">>], args_desc = ["User name", "Server name"], result_example = {<<"user1@myserver.com/tka">>, <<"dnd">>, <<"Busy">>}, result = {presence, {tuple, [{jid, string}, {show, string}, {status, string}]}}}, #ejabberd_commands{name = set_presence, tags = [session], desc = "Set presence of a session", module = ?MODULE, function = set_presence, args = [{user, binary}, {host, binary}, {resource, binary}, {type, binary}, {show, binary}, {status, binary}, {priority, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"tka1">>, <<"available">>,<<"away">>,<<"BB">>, <<"7">>], args_desc = ["User name", "Server name", "Resource", "Type: available, error, probe...", "Show: away, chat, dnd, xa.", "Status text", "Priority, provide this value as an integer"], result = {res, rescode}}, #ejabberd_commands{name = set_nickname, tags = [vcard], desc = "Set nickname in a user's vCard", module = ?MODULE, function = set_nickname, args = [{user, binary}, {host, binary}, {nickname, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"User 1">>], args_desc = ["User name", "Server name", "Nickname"], result = {res, rescode}}, #ejabberd_commands{name = get_vcard, tags = [vcard], desc = "Get content from a vCard field", longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, args = [{user, binary}, {host, binary}, {name, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"NICKNAME">>], args_desc = ["User name", "Server name", "Field name"], result_example = "User 1", result_desc = "Field content", result = {content, string}}, #ejabberd_commands{name = get_vcard2, tags = [vcard], desc = "Get content from a vCard subfield", longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"N">>, <<"FAMILY">>], args_desc = ["User name", "Server name", "Field name", "Subfield name"], result_example = "Schubert", result_desc = "Field content", result = {content, string}}, #ejabberd_commands{name = get_vcard2_multi, tags = [vcard], desc = "Get multiple contents from a vCard field", longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = get_vcard_multi, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}], result = {contents, {list, {value, string}}}}, #ejabberd_commands{name = set_vcard, tags = [vcard], desc = "Set content in a vCard field", longdesc = Vcard1FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {content, binary}], args_example = [<<"user1">>,<<"myserver.com">>, <<"URL">>, <<"www.example.com">>], args_desc = ["User name", "Server name", "Field name", "Value"], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2, tags = [vcard], desc = "Set content in a vCard subfield", longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {content, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"TEL">>, <<"NUMBER">>, <<"123456">>], args_desc = ["User name", "Server name", "Field name", "Subfield name", "Value"], result = {res, rescode}}, #ejabberd_commands{name = set_vcard2_multi, tags = [vcard], desc = "Set multiple contents in a vCard subfield", longdesc = Vcard2FieldsString ++ "\n" ++ VcardXEP, module = ?MODULE, function = set_vcard, args = [{user, binary}, {host, binary}, {name, binary}, {subname, binary}, {contents, {list, {value, binary}}}], result = {res, rescode}}, #ejabberd_commands{name = add_rosteritem, tags = [roster], desc = "Add an item to a user's roster (supports ODBC)", longdesc = "Group can be several groups separated by ; for example: \"g1;g2;g3\"", module = ?MODULE, function = add_rosteritem, args = [{localuser, binary}, {localhost, binary}, {user, binary}, {host, binary}, {nick, binary}, {group, binary}, {subs, binary}], args_rename = [{localserver, localhost}, {server, host}], args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>, <<"User 2">>, <<"Friends">>, <<"both">>], args_desc = ["User name", "Server name", "Contact user name", "Contact server name", "Nickname", "Group", "Subscription"], result = {res, rescode}}, %%{"", "subs= none, from, to or both"}, %%{"", "example: add-roster peter localhost mike server.com MiKe Employees both"}, %%{"", "will add mike@server.com to peter@localhost roster"}, #ejabberd_commands{name = delete_rosteritem, tags = [roster], desc = "Delete an item from a user's roster (supports ODBC)", module = ?MODULE, function = delete_rosteritem, args = [{localuser, binary}, {localhost, binary}, {user, binary}, {host, binary}], args_rename = [{localserver, localhost}, {server, host}], args_example = [<<"user1">>,<<"myserver.com">>,<<"user2">>, <<"myserver.com">>], args_desc = ["User name", "Server name", "Contact user name", "Contact server name"], result = {res, rescode}}, #ejabberd_commands{name = process_rosteritems, tags = [roster], desc = "List/delete rosteritems that match filter", longdesc = "Explanation of each argument:\n" " - action: what to do with each rosteritem that " "matches all the filtering options\n" " - subs: subscription type\n" " - asks: pending subscription\n" " - users: the JIDs of the local user\n" " - contacts: the JIDs of the contact in the roster\n" "\n" " *** Mnesia: \n" "\n" "Allowed values in the arguments:\n" " ACTION = list | delete\n" " SUBS = SUB[:SUB]* | any\n" " SUB = none | from | to | both\n" " ASKS = ASK[:ASK]* | any\n" " ASK = none | out | in\n" " USERS = JID[:JID]* | any\n" " CONTACTS = JID[:JID]* | any\n" " JID = characters valid in a JID, and can use the " "globs: *, ?, ! and [...]\n" "\n" "This example will list roster items with subscription " "'none', 'from' or 'to' that have any ask property, of " "local users which JID is in the virtual host " "'example.org' and that the contact JID is either a " "bare server name (without user part) or that has a " "user part and the server part contains the word 'icq'" ":\n list none:from:to any *@example.org *:*@*icq*" "\n\n" " *** SQL:\n" "\n" "Allowed values in the arguments:\n" " ACTION = list | delete\n" " SUBS = any | none | from | to | both\n" " ASKS = any | none | out | in\n" " USERS = JID\n" " CONTACTS = JID\n" " JID = characters valid in a JID, and can use the " "globs: _ and %\n" "\n" "This example will list roster items with subscription " "'to' that have any ask property, of " "local users which JID is in the virtual host " "'example.org' and that the contact JID's " "server part contains the word 'icq'" ":\n list to any %@example.org %@%icq%", module = mod_roster, function = process_rosteritems, args = [{action, string}, {subs, string}, {asks, string}, {users, string}, {contacts, string}], result = {response, {list, {pairs, {tuple, [{user, string}, {contact, string} ]}} }}}, #ejabberd_commands{name = get_roster, tags = [roster], desc = "Get roster of a local user", policy = user, module = ?MODULE, function = get_roster, args = [], args_rename = [{server, host}], result = {contacts, {list, {contact, {tuple, [ {jid, string}, {nick, string}, {subscription, string}, {ask, string}, {group, string} ]}}}}}, #ejabberd_commands{name = push_roster, tags = [roster], desc = "Push template roster from file to a user", longdesc = "The text file must contain an erlang term: a list " "of tuples with username, servername, group and nick. Example:\n" "[{<<\"user1\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 1\">>},\n" " {<<\"user2\">>, <<\"localhost\">>, <<\"Workers\">>, <<\"User 2\">>}].\n" "When using UTF8 character encoding add /utf8 to certain string. Example:\n" "[{<<\"user2\">>, <<\"localhost\">>, <<\"Workers\"/utf8>>, <<\"User 2\"/utf8>>}].", module = ?MODULE, function = push_roster, args = [{file, binary}, {user, binary}, {host, binary}], args_example = [<<"/home/ejabberd/roster.txt">>, <<"user1">>, <<"localhost">>], args_desc = ["File path", "User name", "Server name"], result = {res, rescode}}, #ejabberd_commands{name = push_roster_all, tags = [roster], desc = "Push template roster from file to all those users", longdesc = "The text file must contain an erlang term: a list " "of tuples with username, servername, group and nick. Example:\n" "[{\"user1\", \"localhost\", \"Workers\", \"User 1\"},\n" " {\"user2\", \"localhost\", \"Workers\", \"User 2\"}].", module = ?MODULE, function = push_roster_all, args = [{file, binary}], args_example = [<<"/home/ejabberd/roster.txt">>], args_desc = ["File path"], result = {res, rescode}}, #ejabberd_commands{name = push_alltoall, tags = [roster], desc = "Add all the users to all the users of Host in Group", module = ?MODULE, function = push_alltoall, args = [{host, binary}, {group, binary}], args_example = [<<"myserver.com">>,<<"Everybody">>], args_desc = ["Server name", "Group name"], result = {res, rescode}}, #ejabberd_commands{name = get_last, tags = [last], desc = "Get last activity information", longdesc = "Timestamp is UTC and XEP-0082 format, for example: " "2017-02-23T22:25:28.063062Z ONLINE", module = ?MODULE, function = get_last, args = [{user, binary}, {host, binary}], args_example = [<<"user1">>,<<"myserver.com">>], args_desc = ["User name", "Server name"], result_example = {<<"2017-06-30T14:32:16.060684Z">>, "ONLINE"}, result_desc = "Last activity timestamp and status", result = {last_activity, {tuple, [{timestamp, string}, {status, string} ]}}}, #ejabberd_commands{name = set_last, tags = [last], desc = "Set last activity information", longdesc = "Timestamp is the seconds since " "1970-01-01 00:00:00 UTC, for example: date +%s", module = ?MODULE, function = set_last, args = [{user, binary}, {host, binary}, {timestamp, integer}, {status, binary}], args_example = [<<"user1">>,<<"myserver.com">>, 1500045311, <<"GoSleeping">>], args_desc = ["User name", "Server name", "Number of seconds since epoch", "Status message"], result = {res, rescode}}, #ejabberd_commands{name = private_get, tags = [private], desc = "Get some information from a user private storage", module = ?MODULE, function = private_get, args = [{user, binary}, {host, binary}, {element, binary}, {ns, binary}], args_example = [<<"user1">>,<<"myserver.com">>,<<"storage">>, <<"storage:rosternotes">>], args_desc = ["User name", "Server name", "Element name", "Namespace"], result = {res, string}}, #ejabberd_commands{name = private_set, tags = [private], desc = "Set to the user private storage", module = ?MODULE, function = private_set, args = [{user, binary}, {host, binary}, {element, binary}], args_example = [<<"user1">>,<<"myserver.com">>, <<"">>], args_desc = ["User name", "Server name", "XML storage element"], result = {res, rescode}}, #ejabberd_commands{name = srg_create, tags = [shared_roster_group], desc = "Create a Shared Roster Group", longdesc = "If you want to specify several group " "identifiers in the Display argument,\n" "put \\ \" around the argument and\nseparate the " "identifiers with \\ \\ n\n" "For example:\n" " ejabberdctl srg_create group3 myserver.com " "name desc \\\"group1\\\\ngroup2\\\"", note = "changed in 21.07", module = ?MODULE, function = srg_create, args = [{group, binary}, {host, binary}, {label, binary}, {description, binary}, {display, binary}], args_rename = [{name, label}], args_example = [<<"group3">>, <<"myserver.com">>, <<"Group3">>, <<"Third group">>, <<"group1\\\\ngroup2">>], args_desc = ["Group identifier", "Group server name", "Group name", "Group description", "Groups to display"], result = {res, rescode}}, #ejabberd_commands{name = srg_delete, tags = [shared_roster_group], desc = "Delete a Shared Roster Group", module = ?MODULE, function = srg_delete, args = [{group, binary}, {host, binary}], args_example = [<<"group3">>, <<"myserver.com">>], args_desc = ["Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = srg_list, tags = [shared_roster_group], desc = "List the Shared Roster Groups in Host", module = ?MODULE, function = srg_list, args = [{host, binary}], args_example = [<<"myserver.com">>], args_desc = ["Server name"], result_example = [<<"group1">>, <<"group2">>], result_desc = "List of group identifiers", result = {groups, {list, {id, string}}}}, #ejabberd_commands{name = srg_get_info, tags = [shared_roster_group], desc = "Get info of a Shared Roster Group", module = ?MODULE, function = srg_get_info, args = [{group, binary}, {host, binary}], args_example = [<<"group3">>, <<"myserver.com">>], args_desc = ["Group identifier", "Group server name"], result_example = [{<<"name">>, "Group 3"}, {<<"displayed_groups">>, "group1"}], result_desc = "List of group informations, as key and value", result = {informations, {list, {information, {tuple, [{key, string}, {value, string}]}}}}}, #ejabberd_commands{name = srg_get_members, tags = [shared_roster_group], desc = "Get members of a Shared Roster Group", module = ?MODULE, function = srg_get_members, args = [{group, binary}, {host, binary}], args_example = [<<"group3">>, <<"myserver.com">>], args_desc = ["Group identifier", "Group server name"], result_example = [<<"user1@localhost">>, <<"user2@localhost">>], result_desc = "List of group identifiers", result = {members, {list, {member, string}}}}, #ejabberd_commands{name = srg_user_add, tags = [shared_roster_group], desc = "Add the JID user@host to the Shared Roster Group", module = ?MODULE, function = srg_user_add, args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], args_desc = ["Username", "User server name", "Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = srg_user_del, tags = [shared_roster_group], desc = "Delete this JID user@host from the Shared Roster Group", module = ?MODULE, function = srg_user_del, args = [{user, binary}, {host, binary}, {group, binary}, {grouphost, binary}], args_example = [<<"user1">>, <<"myserver.com">>, <<"group3">>, <<"myserver.com">>], args_desc = ["Username", "User server name", "Group identifier", "Group server name"], result = {res, rescode}}, #ejabberd_commands{name = get_offline_count, tags = [offline], desc = "Get the number of unread offline messages", policy = user, module = mod_offline, function = count_offline_messages, args = [], args_rename = [{server, host}], result_example = 5, result_desc = "Number", result = {value, integer}}, #ejabberd_commands{name = send_message, tags = [stanza], desc = "Send a message to a local or remote bare of full JID", longdesc = "When sending a groupchat message to a MUC room, " "FROM must be the full JID of a room occupant, " "or the bare JID of a MUC service admin, " "or the bare JID of a MUC/Sub subscribed user.", module = ?MODULE, function = send_message, args = [{type, binary}, {from, binary}, {to, binary}, {subject, binary}, {body, binary}], args_example = [<<"headline">>, <<"admin@localhost">>, <<"user1@localhost">>, <<"Restart">>, <<"In 5 minutes">>], args_desc = ["Message type: normal, chat, headline, groupchat", "Sender JID", "Receiver JID", "Subject, or empty string", "Body"], result = {res, rescode}}, #ejabberd_commands{name = send_stanza_c2s, tags = [stanza], desc = "Send a stanza from an existing C2S session", longdesc = "USER@HOST/RESOURCE must be an existing C2S session." " As an alternative, use send_stanza instead.", module = ?MODULE, function = send_stanza_c2s, args = [{user, binary}, {host, binary}, {resource, binary}, {stanza, binary}], args_example = [<<"admin">>, <<"myserver.com">>, <<"bot">>, <<"">>], args_desc = ["Username", "Server name", "Resource", "Stanza"], result = {res, rescode}}, #ejabberd_commands{name = send_stanza, tags = [stanza], desc = "Send a stanza; provide From JID and valid To JID", module = ?MODULE, function = send_stanza, args = [{from, binary}, {to, binary}, {stanza, binary}], args_example = [<<"admin@localhost">>, <<"user1@localhost">>, <<"">>], args_desc = ["Sender JID", "Destination JID", "Stanza"], result = {res, rescode}}, #ejabberd_commands{name = privacy_set, tags = [stanza], desc = "Send a IQ set privacy stanza for a local account", module = ?MODULE, function = privacy_set, args = [{user, binary}, {host, binary}, {xmlquery, binary}], args_example = [<<"user1">>, <<"myserver.com">>, <<"...">>], args_desc = ["Username", "Server name", "Query XML element"], result = {res, rescode}}, #ejabberd_commands{name = stats, tags = [statistics], desc = "Get statistical value: registeredusers onlineusers onlineusersnode uptimeseconds processes", policy = admin, module = ?MODULE, function = stats, args = [{name, binary}], args_example = [<<"registeredusers">>], args_desc = ["Statistic name"], result_example = 6, result_desc = "Integer statistic value", result = {stat, integer}}, #ejabberd_commands{name = stats_host, tags = [statistics], desc = "Get statistical value for this host: registeredusers onlineusers", policy = admin, module = ?MODULE, function = stats, args = [{name, binary}, {host, binary}], args_example = [<<"registeredusers">>, <<"example.com">>], args_desc = ["Statistic name", "Server JID"], result_example = 6, result_desc = "Integer statistic value", result = {stat, integer}} ]. %%% %%% Adminsys %%% compile(File) -> Ebin = filename:join(code:lib_dir(ejabberd), "ebin"), case ext_mod:compile_erlang_file(Ebin, File) of {ok, Module} -> code:purge(Module), code:load_file(Module), ok; _ -> error end. get_cookie() -> atom_to_list(erlang:get_cookie()). restart_module(Host, Module) when is_binary(Module) -> restart_module(Host, misc:binary_to_atom(Module)); restart_module(Host, Module) when is_atom(Module) -> case gen_mod:is_loaded(Host, Module) of false -> % not a running module, force code reload anyway code:purge(Module), code:delete(Module), code:load_file(Module), 1; true -> gen_mod:stop_module(Host, Module), case code:soft_purge(Module) of true -> code:delete(Module), code:load_file(Module), gen_mod:start_module(Host, Module), 0; false -> gen_mod:start_module(Host, Module), 2 end end. %%% %%% Accounts %%% set_password(User, Host, Password) -> Fun = fun () -> ejabberd_auth:set_password(User, Host, Password) end, user_action(User, Host, Fun, ok). check_password(User, Host, Password) -> ejabberd_auth:check_password(User, <<>>, Host, Password). %% Copied some code from ejabberd_commands.erl check_password_hash(User, Host, PasswordHash, HashMethod) -> AccountPass = ejabberd_auth:get_password_s(User, Host), Methods = lists:map(fun(A) -> atom_to_binary(A, latin1) end, proplists:get_value(hashs, crypto:supports())), MethodAllowed = lists:member(HashMethod, Methods), AccountPassHash = case {AccountPass, MethodAllowed} of {A, _} when is_tuple(A) -> scrammed; {_, true} -> get_hash(AccountPass, HashMethod); {_, false} -> ?ERROR_MSG("Check_password_hash called " "with hash method: ~p", [HashMethod]), undefined end, case AccountPassHash of scrammed -> ?ERROR_MSG("Passwords are scrammed, and check_password_hash cannot work.", []), throw(passwords_scrammed_command_cannot_work); undefined -> throw(unkown_hash_method); PasswordHash -> ok; _ -> false end. get_hash(AccountPass, Method) -> iolist_to_binary([io_lib:format("~2.16.0B", [X]) || X <- binary_to_list( crypto:hash(binary_to_atom(Method, latin1), AccountPass))]). delete_old_users(Days) -> %% Get the list of registered users Users = ejabberd_auth:get_users(), {removed, N, UR} = delete_old_users(Days, Users), {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. delete_old_users_vhost(Host, Days) -> %% Get the list of registered users Users = ejabberd_auth:get_users(Host), {removed, N, UR} = delete_old_users(Days, Users), {ok, io_lib:format("Deleted ~p users: ~p", [N, UR])}. delete_old_users(Days, Users) -> SecOlder = Days*24*60*60, TimeStamp_now = erlang:system_time(second), TimeStamp_oldest = TimeStamp_now - SecOlder, F = fun({LUser, LServer}) -> case catch delete_or_not(LUser, LServer, TimeStamp_oldest) of true -> ejabberd_auth:remove_user(LUser, LServer), true; _ -> false end end, Users_removed = lists:filter(F, Users), {removed, length(Users_removed), Users_removed}. delete_or_not(LUser, LServer, TimeStamp_oldest) -> deny = acl:match_rule(LServer, protect_old_users, jid:make(LUser, LServer)), [] = ejabberd_sm:get_user_resources(LUser, LServer), case mod_last:get_last_info(LUser, LServer) of {ok, TimeStamp, _Status} -> if TimeStamp_oldest < TimeStamp -> false; true -> true end; not_found -> true end. %% %% Ban account ban_account(User, Host, ReasonText) -> Reason = prepare_reason(ReasonText), kick_sessions(User, Host, Reason), set_random_password(User, Host, Reason), ok. kick_sessions(User, Server, Reason) -> lists:map( fun(Resource) -> kick_this_session(User, Server, Resource, Reason) end, ejabberd_sm:get_user_resources(User, Server)). set_random_password(User, Server, Reason) -> NewPass = build_random_password(Reason), set_password_auth(User, Server, NewPass). build_random_password(Reason) -> {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:universal_time(), Date = str:format("~4..0B~2..0B~2..0BT~2..0B:~2..0B:~2..0B", [Year, Month, Day, Hour, Minute, Second]), RandomString = p1_rand:get_string(), <<"BANNED_ACCOUNT--", Date/binary, "--", RandomString/binary, "--", Reason/binary>>. set_password_auth(User, Server, Password) -> ok = ejabberd_auth:set_password(User, Server, Password). prepare_reason([]) -> <<"Kicked by administrator">>; prepare_reason([Reason]) -> Reason; prepare_reason(Reason) when is_binary(Reason) -> Reason. %%% %%% Sessions %%% num_resources(User, Host) -> length(ejabberd_sm:get_user_resources(User, Host)). resource_num(User, Host, Num) -> Resources = ejabberd_sm:get_user_resources(User, Host), case (0 lists:nth(Num, Resources); false -> throw({bad_argument, lists:flatten(io_lib:format("Wrong resource number: ~p", [Num]))}) end. kick_session(User, Server, Resource, ReasonText) -> kick_this_session(User, Server, Resource, prepare_reason(ReasonText)), ok. kick_this_session(User, Server, Resource, Reason) -> ejabberd_sm:route(jid:make(User, Server, Resource), {exit, Reason}). status_num(Host, Status) -> length(get_status_list(Host, Status)). status_num(Status) -> status_num(<<"all">>, Status). status_list(Host, Status) -> Res = get_status_list(Host, Status), [{U, S, R, num_prio(P), St} || {U, S, R, P, St} <- Res]. status_list(Status) -> status_list(<<"all">>, Status). get_status_list(Host, Status_required) -> %% Get list of all logged users Sessions = ejabberd_sm:dirty_get_my_sessions_list(), %% Reformat the list Sessions2 = [ {Session#session.usr, Session#session.sid, Session#session.priority} || Session <- Sessions], Fhost = case Host of <<"all">> -> %% All hosts are requested, so don't filter at all fun(_, _) -> true end; _ -> %% Filter the list, only Host is interesting fun(A, B) -> A == B end end, Sessions3 = [ {Pid, Server, Priority} || {{_User, Server, _Resource}, {_, Pid}, Priority} <- Sessions2, apply(Fhost, [Server, Host])], %% For each Pid, get its presence Sessions4 = [ {catch get_presence(Pid), Server, Priority} || {Pid, Server, Priority} <- Sessions3], %% Filter by status Fstatus = case Status_required of <<"all">> -> fun(_, _) -> true end; _ -> fun(A, B) -> A == B end end, [{User, Server, Resource, num_prio(Priority), stringize(Status_text)} || {{User, Resource, Status, Status_text}, Server, Priority} <- Sessions4, apply(Fstatus, [Status, Status_required])]. connected_users_info() -> lists:filtermap( fun({U, S, R}) -> case user_session_info(U, S, R) of offline -> false; Info -> Jid = jid:encode(jid:make(U, S, R)), {true, erlang:insert_element(1, Info, Jid)} end end, ejabberd_sm:dirty_get_sessions_list()). connected_users_vhost(Host) -> USRs = ejabberd_sm:get_vh_session_list(Host), [ jid:encode(jid:make(USR)) || USR <- USRs]. %% Make string more print-friendly stringize(String) -> %% Replace newline characters with other code ejabberd_regexp:greplace(String, <<"\n">>, <<"\\n">>). get_presence(Pid) -> try get_presence2(Pid) of {_, _, _, _} = Res -> Res catch _:_ -> {<<"">>, <<"">>, <<"offline">>, <<"">>} end. get_presence2(Pid) -> Pres = #presence{from = From} = ejabberd_c2s:get_presence(Pid), Show = case Pres of #presence{type = unavailable} -> <<"unavailable">>; #presence{show = undefined} -> <<"available">>; #presence{show = S} -> atom_to_binary(S, utf8) end, Status = xmpp:get_text(Pres#presence.status), {From#jid.user, From#jid.resource, Show, Status}. get_presence(U, S) -> Pids = [ejabberd_sm:get_session_pid(U, S, R) || R <- ejabberd_sm:get_user_resources(U, S)], OnlinePids = [Pid || Pid <- Pids, Pid=/=none], case OnlinePids of [] -> {jid:encode({U, S, <<>>}), <<"unavailable">>, <<"">>}; [SessionPid|_] -> {_User, Resource, Show, Status} = get_presence(SessionPid), FullJID = jid:encode({U, S, Resource}), {FullJID, Show, Status} end. set_presence(User, Host, Resource, Type, Show, Status, Priority) when is_integer(Priority) -> BPriority = integer_to_binary(Priority), set_presence(User, Host, Resource, Type, Show, Status, BPriority); set_presence(User, Host, Resource, Type, Show, Status, Priority0) -> Priority = if is_integer(Priority0) -> Priority0; true -> binary_to_integer(Priority0) end, Pres = #presence{ from = jid:make(User, Host, Resource), to = jid:make(User, Host), type = misc:binary_to_atom(Type), status = xmpp:mk_text(Status), show = misc:binary_to_atom(Show), priority = Priority, sub_els = []}, Ref = ejabberd_sm:get_session_pid(User, Host, Resource), ejabberd_c2s:set_presence(Ref, Pres). user_sessions_info(User, Host) -> lists:filtermap(fun(Resource) -> case user_session_info(User, Host, Resource) of offline -> false; Info -> {true, Info} end end, ejabberd_sm:get_user_resources(User, Host)). user_session_info(User, Host, Resource) -> CurrentSec = calendar:datetime_to_gregorian_seconds({date(), time()}), case ejabberd_sm:get_user_info(User, Host, Resource) of offline -> offline; Info -> Now = proplists:get_value(ts, Info), Pid = proplists:get_value(pid, Info), {_U, _Resource, Status, StatusText} = get_presence(Pid), Priority = proplists:get_value(priority, Info), Conn = proplists:get_value(conn, Info), {Ip, Port} = proplists:get_value(ip, Info), IPS = inet_parse:ntoa(Ip), NodeS = atom_to_list(node(Pid)), Uptime = CurrentSec - calendar:datetime_to_gregorian_seconds( calendar:now_to_local_time(Now)), {atom_to_list(Conn), IPS, Port, num_prio(Priority), NodeS, Uptime, Status, Resource, StatusText} end. %%% %%% Vcard %%% set_nickname(User, Host, Nickname) -> VCard = xmpp:encode(#vcard_temp{nickname = Nickname}), case mod_vcard:set_vcard(User, jid:nameprep(Host), VCard) of {error, badarg} -> error; ok -> ok end. get_vcard(User, Host, Name) -> [Res | _] = get_vcard_content(User, Host, [Name]), Res. get_vcard(User, Host, Name, Subname) -> [Res | _] = get_vcard_content(User, Host, [Name, Subname]), Res. get_vcard_multi(User, Host, Name, Subname) -> get_vcard_content(User, Host, [Name, Subname]). set_vcard(User, Host, Name, SomeContent) -> set_vcard_content(User, Host, [Name], SomeContent). set_vcard(User, Host, Name, Subname, SomeContent) -> set_vcard_content(User, Host, [Name, Subname], SomeContent). %% %% Internal vcard get_vcard_content(User, Server, Data) -> case mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) of [El|_] -> case get_vcard(Data, El) of [false] -> throw(error_no_value_found_in_vcard); ElemList -> ?DEBUG("ELS ~p", [ElemList]), [fxml:get_tag_cdata(Elem) || Elem <- ElemList] end; [] -> throw(error_no_vcard_found); error -> throw(database_failure) end. get_vcard([<<"TEL">>, TelType], {_, _, _, OldEls}) -> {TakenEl, _NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), [TakenEl]; get_vcard([Data1, Data2], A1) -> case get_subtag(A1, Data1) of [false] -> [false]; A2List -> lists:flatten([get_vcard([Data2], A2) || A2 <- A2List]) end; get_vcard([Data], A1) -> get_subtag(A1, Data). get_subtag(Xmlelement, Name) -> [fxml:get_subtag(Xmlelement, Name)]. set_vcard_content(User, Server, Data, SomeContent) -> ContentList = case SomeContent of [Bin | _] when is_binary(Bin) -> SomeContent; Bin when is_binary(Bin) -> [SomeContent] end, %% Get old vcard A4 = case mod_vcard:get_vcard(jid:nodeprep(User), jid:nameprep(Server)) of [A1] -> {_, _, _, A2} = A1, update_vcard_els(Data, ContentList, A2); [] -> update_vcard_els(Data, ContentList, []); error -> throw(database_failure) end, %% Build new vcard SubEl = {xmlel, <<"vCard">>, [{<<"xmlns">>,<<"vcard-temp">>}], A4}, mod_vcard:set_vcard(User, jid:nameprep(Server), SubEl). take_vcard_tel(TelType, [{xmlel, <<"TEL">>, _, SubEls}=OldEl | OldEls], NewEls, Taken) -> {Taken2, NewEls2} = case lists:keymember(TelType, 2, SubEls) of true -> {fxml:get_subtag(OldEl, <<"NUMBER">>), NewEls}; false -> {Taken, [OldEl | NewEls]} end, take_vcard_tel(TelType, OldEls, NewEls2, Taken2); take_vcard_tel(TelType, [OldEl | OldEls], NewEls, Taken) -> take_vcard_tel(TelType, OldEls, [OldEl | NewEls], Taken); take_vcard_tel(_TelType, [], NewEls, Taken) -> {Taken, NewEls}. update_vcard_els([<<"TEL">>, TelType], [TelValue], OldEls) -> {_, NewEls} = take_vcard_tel(TelType, OldEls, [], not_found), NewEl = {xmlel,<<"TEL">>,[], [{xmlel,TelType,[],[]}, {xmlel,<<"NUMBER">>,[],[{xmlcdata,TelValue}]}]}, [NewEl | NewEls]; update_vcard_els(Data, ContentList, Els1) -> Els2 = lists:keysort(2, Els1), [Data1 | Data2] = Data, NewEls = case Data2 of [] -> [{xmlel, Data1, [], [{xmlcdata,Content}]} || Content <- ContentList]; [D2] -> OldEl = case lists:keysearch(Data1, 2, Els2) of {value, A} -> A; false -> {xmlel, Data1, [], []} end, {xmlel, _, _, ContentOld1} = OldEl, Content2 = [{xmlel, D2, [], [{xmlcdata,Content}]} || Content <- ContentList], ContentOld2 = [A || {_, X, _, _} = A <- ContentOld1, X/=D2], ContentOld3 = lists:keysort(2, ContentOld2), ContentNew = lists:keymerge(2, Content2, ContentOld3), [{xmlel, Data1, [], ContentNew}] end, Els3 = lists:keydelete(Data1, 2, Els2), lists:keymerge(2, NewEls, Els3). %%% %%% Roster %%% add_rosteritem(LocalUser, LocalServer, User, Server, Nick, Group, Subs) -> case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of {error, _} -> throw({error, "Invalid 'localuser'/'localserver'"}); {_, error} -> throw({error, "Invalid 'user'/'server'"}); {Jid, _Jid2} -> RosterItem = build_roster_item(User, Server, {add, Nick, Subs, Group}), case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of ok -> ok; _ -> error end end. subscribe(LU, LS, User, Server, Nick, Group, Subscription, _Xattrs) -> case {jid:make(LU, LS), jid:make(User, Server)} of {error, _} -> throw({error, "Invalid 'localuser'/'localserver'"}); {_, error} -> throw({error, "Invalid 'user'/'server'"}); {_Jid, _Jid2} -> ItemEl = build_roster_item(User, Server, {add, Nick, Subscription, Group}), mod_roster:set_items(LU, LS, #roster_query{items = [ItemEl]}) end. delete_rosteritem(LocalUser, LocalServer, User, Server) -> case {jid:make(LocalUser, LocalServer), jid:make(User, Server)} of {error, _} -> throw({error, "Invalid 'localuser'/'localserver'"}); {_, error} -> throw({error, "Invalid 'user'/'server'"}); {Jid, _Jid2} -> RosterItem = build_roster_item(User, Server, remove), case mod_roster:set_item_and_notify_clients(Jid, RosterItem, true) of ok -> ok; _ -> error end end. %% ----------------------------- %% Get Roster %% ----------------------------- get_roster(User, Server) -> case jid:make(User, Server) of error -> throw({error, "Invalid 'user'/'server'"}); #jid{luser = U, lserver = S} -> Items = ejabberd_hooks:run_fold(roster_get, S, [], [{U, S}]), make_roster_xmlrpc(Items) end. %% Note: if a contact is in several groups, the contact is returned %% several times, each one in a different group. make_roster_xmlrpc(Roster) -> lists:foldl( fun(Item, Res) -> JIDS = jid:encode(Item#roster.jid), Nick = Item#roster.name, Subs = atom_to_list(Item#roster.subscription), Ask = atom_to_list(Item#roster.ask), Groups = case Item#roster.groups of [] -> [<<>>]; Gs -> Gs end, ItemsX = [{JIDS, Nick, Subs, Ask, Group} || Group <- Groups], ItemsX ++ Res end, [], Roster). %%----------------------------- %% Push Roster from file %%----------------------------- push_roster(File, User, Server) -> {ok, [Roster]} = file:consult(File), subscribe_roster({User, Server, <<>>, User}, Roster). push_roster_all(File) -> {ok, [Roster]} = file:consult(File), subscribe_all(Roster). subscribe_all(Roster) -> subscribe_all(Roster, Roster). subscribe_all([], _) -> ok; subscribe_all([User1 | Users], Roster) -> subscribe_roster(User1, Roster), subscribe_all(Users, Roster). subscribe_roster(_, []) -> ok; %% Do not subscribe a user to itself subscribe_roster({Name, Server, Group, Nick}, [{Name, Server, _, _} | Roster]) -> subscribe_roster({Name, Server, Group, Nick}, Roster); %% Subscribe Name2 to Name1 subscribe_roster({Name1, Server1, Group1, Nick1}, [{Name2, Server2, Group2, Nick2} | Roster]) -> subscribe(iolist_to_binary(Name1), iolist_to_binary(Server1), iolist_to_binary(Name2), iolist_to_binary(Server2), iolist_to_binary(Nick2), iolist_to_binary(Group2), <<"both">>, []), subscribe_roster({Name1, Server1, Group1, Nick1}, Roster). push_alltoall(S, G) -> Users = ejabberd_auth:get_users(S), Users2 = build_list_users(G, Users, []), subscribe_all(Users2), ok. build_list_users(_Group, [], Res) -> Res; build_list_users(Group, [{User, Server}|Users], Res) -> build_list_users(Group, Users, [{User, Server, Group, User}|Res]). %% @spec(LU, LS, U, S, Action) -> ok %% Action = {add, Nick, Subs, Group} | remove %% @doc Push to the roster of account LU@LS the contact U@S. %% The specific action to perform is defined in Action. push_roster_item(LU, LS, U, S, Action) -> lists:foreach(fun(R) -> push_roster_item(LU, LS, R, U, S, Action) end, ejabberd_sm:get_user_resources(LU, LS)). push_roster_item(LU, LS, R, U, S, Action) -> LJID = jid:make(LU, LS, R), BroadcastEl = build_broadcast(U, S, Action), ejabberd_sm:route(LJID, BroadcastEl), Item = build_roster_item(U, S, Action), ResIQ = build_iq_roster_push(Item), ejabberd_router:route( xmpp:set_from_to(ResIQ, jid:remove_resource(LJID), LJID)). build_roster_item(U, S, {add, Nick, Subs, Group}) -> Groups = binary:split(Group,<<";">>, [global]), #roster_item{jid = jid:make(U, S), name = Nick, subscription = misc:binary_to_atom(Subs), groups = Groups}; build_roster_item(U, S, remove) -> #roster_item{jid = jid:make(U, S), subscription = remove}. build_iq_roster_push(Item) -> #iq{type = set, id = <<"push">>, sub_els = [#roster_query{items = [Item]}]}. build_broadcast(U, S, {add, _Nick, Subs, _Group}) -> build_broadcast(U, S, list_to_atom(binary_to_list(Subs))); build_broadcast(U, S, remove) -> build_broadcast(U, S, none); %% @spec (U::binary(), S::binary(), Subs::atom()) -> any() %% Subs = both | from | to | none build_broadcast(U, S, SubsAtom) when is_atom(SubsAtom) -> {item, {U, S, <<>>}, SubsAtom}. %%% %%% Last Activity %%% get_last(User, Server) -> {Now, Status} = case ejabberd_sm:get_user_resources(User, Server) of [] -> case mod_last:get_last_info(User, Server) of not_found -> {erlang:timestamp(), "NOT FOUND"}; {ok, Shift, Status1} -> {{Shift div 1000000, Shift rem 1000000, 0}, Status1} end; _ -> {erlang:timestamp(), "ONLINE"} end, {xmpp_util:encode_timestamp(Now), Status}. set_last(User, Server, Timestamp, Status) -> case mod_last:store_last_info(User, Server, Timestamp, Status) of {ok, _} -> ok; Error -> Error end. %%% %%% Private Storage %%% %% Example usage: %% $ ejabberdctl private_set badlop localhost "\Cluth\" %% $ ejabberdctl private_get badlop localhost aa bb %% Cluth private_get(Username, Host, Element, Ns) -> ElementXml = #xmlel{name = Element, attrs = [{<<"xmlns">>, Ns}]}, Els = mod_private:get_data(jid:nodeprep(Username), jid:nameprep(Host), [{Ns, ElementXml}]), binary_to_list(fxml:element_to_binary(xmpp:encode(#private{sub_els = Els}))). private_set(Username, Host, ElementString) -> case fxml_stream:parse_element(ElementString) of {error, Error} -> io:format("Error found parsing the element:~n ~p~nError: ~p~n", [ElementString, Error]), error; Xml -> private_set2(Username, Host, Xml) end. private_set2(Username, Host, Xml) -> NS = fxml:get_tag_attr_s(<<"xmlns">>, Xml), JID = jid:make(Username, Host), mod_private:set_data(JID, [{NS, Xml}]). %%% %%% Shared Roster Groups %%% srg_create(Group, Host, Label, Description, Display) -> DisplayList = case Display of <<>> -> []; _ -> ejabberd_regexp:split(Display, <<"\\\\n">>) end, Opts = [{label, Label}, {displayed_groups, DisplayList}, {description, Description}], {atomic, _} = mod_shared_roster:create_group(Host, Group, Opts), ok. srg_delete(Group, Host) -> {atomic, _} = mod_shared_roster:delete_group(Host, Group), ok. srg_list(Host) -> lists:sort(mod_shared_roster:list_groups(Host)). srg_get_info(Group, Host) -> Opts = case mod_shared_roster:get_group_opts(Host,Group) of Os when is_list(Os) -> Os; error -> [] end, [{misc:atom_to_binary(Title), to_list(Value)} || {Title, Value} <- Opts]. to_list([]) -> []; to_list([H|T]) -> [to_list(H)|to_list(T)]; to_list(E) when is_atom(E) -> atom_to_list(E); to_list(E) -> binary_to_list(E). srg_get_members(Group, Host) -> Members = mod_shared_roster:get_group_explicit_users(Host,Group), [jid:encode(jid:make(MUser, MServer)) || {MUser, MServer} <- Members]. srg_user_add(User, Host, Group, GroupHost) -> mod_shared_roster:add_user_to_group(GroupHost, {User, Host}, Group), ok. srg_user_del(User, Host, Group, GroupHost) -> mod_shared_roster:remove_user_from_group(GroupHost, {User, Host}, Group), ok. %%% %%% Stanza %%% %% @doc Send a message to an XMPP account. %% @spec (Type::binary(), From::binary(), To::binary(), Subject::binary(), Body::binary()) -> ok send_message(Type, From, To, Subject, Body) -> CodecOpts = ejabberd_config:codec_options(), try xmpp:decode( #xmlel{name = <<"message">>, attrs = [{<<"to">>, To}, {<<"from">>, From}, {<<"type">>, Type}, {<<"id">>, p1_rand:get_string()}], children = [#xmlel{name = <<"subject">>, children = [{xmlcdata, Subject}]}, #xmlel{name = <<"body">>, children = [{xmlcdata, Body}]}]}, ?NS_CLIENT, CodecOpts) of #message{from = JID, subject = SubjectEl, body = BodyEl} = Msg -> Msg2 = case {xmpp:get_text(SubjectEl), xmpp:get_text(BodyEl)} of {Subject, <<>>} -> Msg; {<<>>, Body} -> Msg#message{subject = []}; _ -> Msg end, State = #{jid => JID}, ejabberd_hooks:run_fold(user_send_packet, JID#jid.lserver, {Msg2, State}, []), ejabberd_router:route(Msg2) catch _:{xmpp_codec, Why} -> {error, xmpp:format_error(Why)} end. send_stanza(FromString, ToString, Stanza) -> try #xmlel{} = El = fxml_stream:parse_element(Stanza), From = jid:decode(FromString), To = jid:decode(ToString), CodecOpts = ejabberd_config:codec_options(), Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts), Pkt2 = xmpp:set_from_to(Pkt, From, To), State = #{jid => From}, ejabberd_hooks:run_fold(user_send_packet, From#jid.lserver, {Pkt2, State}, []), ejabberd_router:route(Pkt2) catch _:{xmpp_codec, Why} -> io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]), {error, Why}; _:{badmatch, {error, {Code, Why}}} when is_integer(Code) -> io:format("invalid xml: ~p~n", [Why]), {error, Why}; _:{badmatch, {error, Why}} -> io:format("invalid xml: ~p~n", [Why]), {error, Why}; _:{bad_jid, S} -> io:format("malformed JID: ~ts~n", [S]), {error, "JID malformed"} end. -spec send_stanza_c2s(binary(), binary(), binary(), binary()) -> ok | {error, any()}. send_stanza_c2s(Username, Host, Resource, Stanza) -> try #xmlel{} = El = fxml_stream:parse_element(Stanza), CodecOpts = ejabberd_config:codec_options(), Pkt = xmpp:decode(El, ?NS_CLIENT, CodecOpts), case ejabberd_sm:get_session_pid(Username, Host, Resource) of Pid when is_pid(Pid) -> ejabberd_c2s:send(Pid, Pkt); _ -> {error, no_session} end catch _:{badmatch, {error, Why} = Err} -> io:format("invalid xml: ~p~n", [Why]), Err; _:{xmpp_codec, Why} -> io:format("incorrect stanza: ~ts~n", [xmpp:format_error(Why)]), {error, Why} end. privacy_set(Username, Host, QueryS) -> Jid = jid:make(Username, Host), QueryEl = fxml_stream:parse_element(QueryS), SubEl = xmpp:decode(QueryEl), IQ = #iq{type = set, id = <<"push">>, sub_els = [SubEl], from = Jid, to = Jid}, Result = mod_privacy:process_iq(IQ), Result#iq.type == result. %%% %%% Stats %%% stats(Name) -> case Name of <<"uptimeseconds">> -> trunc(element(1, erlang:statistics(wall_clock))/1000); <<"processes">> -> length(erlang:processes()); <<"registeredusers">> -> lists:foldl(fun(Host, Sum) -> ejabberd_auth:count_users(Host) + Sum end, 0, ejabberd_option:hosts()); <<"onlineusersnode">> -> length(ejabberd_sm:dirty_get_my_sessions_list()); <<"onlineusers">> -> length(ejabberd_sm:dirty_get_sessions_list()) end. stats(Name, Host) -> case Name of <<"registeredusers">> -> ejabberd_auth:count_users(Host); <<"onlineusers">> -> length(ejabberd_sm:get_vh_session_list(Host)) end. user_action(User, Server, Fun, OK) -> case ejabberd_auth:user_exists(User, Server) of true -> case catch Fun() of OK -> ok; {error, Error} -> throw(Error); Error -> ?ERROR_MSG("Command returned: ~p", [Error]), 1 end; false -> throw({not_found, "unknown_user"}) end. num_prio(Priority) when is_integer(Priority) -> Priority; num_prio(_) -> -1. mod_options(_) -> []. mod_doc() -> #{desc => [?T("This module provides additional administrative commands."), "", ?T("Details for some commands:"), "", ?T("- 'ban-acount':"), ?T("This command kicks all the connected sessions of the account " "from the server. It also changes their password to a randomly " "generated one, so they can't login anymore unless a server " "administrator changes their password again. It is possible to " "define the reason of the ban. The new password also includes " "the reason and the date and time of the ban. See an example below."), ?T("- 'pushroster': (and 'pushroster-all')"), ?T("The roster file must be placed, if using Windows, on the " "directory where you installed ejabberd: " "C:/Program Files/ejabberd or similar. If you use other " "Operating System, place the file on the same directory where " "the .beam files are installed. See below an example roster file."), ?T("- 'srg-create':"), ?T("If you want to put a group Name with blankspaces, use the " "characters \"\' and \'\" to define when the Name starts and " "ends. See an example below.")], example => [{?T("With this configuration, vCards can only be modified with " "mod_admin_extra commands:"), ["acl:", " adminextraresource:", " - resource: \"modadminextraf8x,31ad\"", "access_rules:", " vcard_set:", " - allow: adminextraresource", "modules:", " mod_admin_extra: {}", " mod_vcard:", " access_set: vcard_set"]}, {?T("Content of roster file for 'pushroster' command:"), ["[{<<\"bob\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Bob\">>},", "{<<\"mart\">>, <<\"example.org\">>, <<\"workers\">>, <<\"Mart\">>},", "{<<\"Rich\">>, <<\"example.org\">>, <<\"bosses\">>, <<\"Rich\">>}]."]}, {?T("With this call, the sessions of the local account which JID is " "boby@example.org will be kicked, and its password will be set " "to something like " "'BANNED_ACCOUNT--20080425T21:45:07--2176635--Spammed_rooms'"), ["ejabberdctl vhost example.org ban-account boby \"Spammed rooms\""]}, {?T("Call to srg-create using double-quotes and single-quotes:"), ["ejabberdctl srg-create g1 example.org \"\'Group number 1\'\" this_is_g1 g1"]}]}. ejabberd-21.12/src/mod_last_mnesia.erl0000644000232200023220000000564314154362354020273 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_last_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_last_mnesia). -behaviour(mod_last). %% API -export([init/2, import/2, get_last/2, store_last_info/4, remove_user/2, use_cache/1]). -export([need_transform/1, transform/1]). -include("mod_last.hrl"). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, last_activity, [{disc_only_copies, [node()]}, {attributes, record_info(fields, last_activity)}]). use_cache(Host) -> case mnesia:table_info(last_activity, storage_type) of disc_only_copies -> mod_last_opt:use_cache(Host); _ -> false end. get_last(LUser, LServer) -> case mnesia:dirty_read(last_activity, {LUser, LServer}) of [] -> error; [#last_activity{timestamp = TimeStamp, status = Status}] -> {ok, {TimeStamp, Status}} end. store_last_info(LUser, LServer, TimeStamp, Status) -> mnesia:dirty_write(#last_activity{us = {LUser, LServer}, timestamp = TimeStamp, status = Status}). remove_user(LUser, LServer) -> US = {LUser, LServer}, mnesia:dirty_delete({last_activity, US}). import(_LServer, #last_activity{} = LA) -> mnesia:dirty_write(LA). need_transform({last_activity, {U, S}, _, Status}) when is_list(U) orelse is_list(S) orelse is_list(Status) -> ?INFO_MSG("Mnesia table 'last_activity' will be converted to binary", []), true; need_transform(_) -> false. transform(#last_activity{us = {U, S}, status = Status} = R) -> R#last_activity{us = {iolist_to_binary(U), iolist_to_binary(S)}, status = iolist_to_binary(Status)}. %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/mod_shared_roster_sql.erl0000644000232200023220000001515214154362354021513 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_shared_roster_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 14 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_shared_roster_sql). -behaviour(mod_shared_roster). %% API -export([init/2, list_groups/1, groups_with_opts/1, create_group/3, delete_group/2, get_group_opts/2, set_group_opts/3, get_user_groups/2, get_group_explicit_users/2, get_user_displayed_groups/3, is_user_in_group/3, add_user_to_group/3, remove_user_from_group/3, import/3, export/1]). -include_lib("xmpp/include/jid.hrl"). -include("mod_roster.hrl"). -include("mod_shared_roster.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ok. list_groups(Host) -> case ejabberd_sql:sql_query( Host, ?SQL("select @(name)s from sr_group where %(Host)H")) of {selected, Rs} -> [G || {G} <- Rs]; _ -> [] end. groups_with_opts(Host) -> case ejabberd_sql:sql_query( Host, ?SQL("select @(name)s, @(opts)s from sr_group where %(Host)H")) of {selected, Rs} -> [{G, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(Opts))} || {G, Opts} <- Rs]; _ -> [] end. create_group(Host, Group, Opts) -> SOpts = misc:term_to_expr(Opts), F = fun () -> ?SQL_UPSERT_T( "sr_group", ["!name=%(Group)s", "!server_host=%(Host)s", "opts=%(SOpts)s"]) end, ejabberd_sql:sql_transaction(Host, F). delete_group(Host, Group) -> F = fun () -> ejabberd_sql:sql_query_t( ?SQL("delete from sr_group where name=%(Group)s and %(Host)H")), ejabberd_sql:sql_query_t( ?SQL("delete from sr_user where grp=%(Group)s and %(Host)H")) end, case ejabberd_sql:sql_transaction(Host, F) of {atomic,{updated,_}} -> {atomic, ok}; Res -> Res end. get_group_opts(Host, Group) -> case catch ejabberd_sql:sql_query( Host, ?SQL("select @(opts)s from sr_group" " where name=%(Group)s and %(Host)H")) of {selected, [{SOpts}]} -> {ok, mod_shared_roster:opts_to_binary(ejabberd_sql:decode_term(SOpts))}; _ -> error end. set_group_opts(Host, Group, Opts) -> SOpts = misc:term_to_expr(Opts), F = fun () -> ?SQL_UPSERT_T( "sr_group", ["!name=%(Group)s", "!server_host=%(Host)s", "opts=%(SOpts)s"]) end, ejabberd_sql:sql_transaction(Host, F). get_user_groups(US, Host) -> SJID = make_jid_s(US), case catch ejabberd_sql:sql_query( Host, ?SQL("select @(grp)s from sr_user" " where jid=%(SJID)s and %(Host)H")) of {selected, Rs} -> [G || {G} <- Rs]; _ -> [] end. get_group_explicit_users(Host, Group) -> case catch ejabberd_sql:sql_query( Host, ?SQL("select @(jid)s from sr_user" " where grp=%(Group)s and %(Host)H")) of {selected, Rs} -> lists:map( fun({JID}) -> {U, S, _} = jid:tolower(jid:decode(JID)), {U, S} end, Rs); _ -> [] end. get_user_displayed_groups(LUser, LServer, GroupsOpts) -> SJID = make_jid_s(LUser, LServer), case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(grp)s from sr_user" " where jid=%(SJID)s and %(LServer)H")) of {selected, Rs} -> [{Group, proplists:get_value(Group, GroupsOpts, [])} || {Group} <- Rs]; _ -> [] end. is_user_in_group(US, Group, Host) -> SJID = make_jid_s(US), case catch ejabberd_sql:sql_query( Host, ?SQL("select @(jid)s from sr_user where jid=%(SJID)s" " and %(Host)H and grp=%(Group)s")) of {selected, []} -> false; _ -> true end. add_user_to_group(Host, US, Group) -> SJID = make_jid_s(US), ejabberd_sql:sql_query( Host, ?SQL_INSERT( "sr_user", ["jid=%(SJID)s", "server_host=%(Host)s", "grp=%(Group)s"])). remove_user_from_group(Host, US, Group) -> SJID = make_jid_s(US), F = fun () -> ejabberd_sql:sql_query_t( ?SQL("delete from sr_user where jid=%(SJID)s and %(Host)H" " and grp=%(Group)s")), ok end, ejabberd_sql:sql_transaction(Host, F). export(_Server) -> [{sr_group, fun(Host, #sr_group{group_host = {Group, LServer}, opts = Opts}) when LServer == Host -> SOpts = misc:term_to_expr(Opts), [?SQL("delete from sr_group where name=%(Group)s and %(Host)H;"), ?SQL_INSERT( "sr_group", ["name=%(Group)s", "server_host=%(Host)s", "opts=%(SOpts)s"])]; (_Host, _R) -> [] end}, {sr_user, fun(Host, #sr_user{us = {U, S}, group_host = {Group, LServer}}) when LServer == Host -> SJID = make_jid_s(U, S), [?SQL("select @(jid)s from sr_user where jid=%(SJID)s" " and %(Host)H and grp=%(Group)s;"), ?SQL_INSERT( "sr_user", ["jid=%(SJID)s", "server_host=%(Host)s", "grp=%(Group)s"])]; (_Host, _R) -> [] end}]. import(_, _, _) -> ok. %%%=================================================================== %%% Internal functions %%%=================================================================== make_jid_s(U, S) -> jid:encode(jid:tolower(jid:make(U, S))). make_jid_s({U, S}) -> make_jid_s(U, S). ejabberd-21.12/src/mod_push_opt.erl0000644000232200023220000000357214154362354017634 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_push_opt). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([db_type/1]). -export([include_body/1]). -export([include_sender/1]). -export([use_cache/1]). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_push, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_push, db_type). -spec include_body(gen_mod:opts() | global | binary()) -> boolean() | binary(). include_body(Opts) when is_map(Opts) -> gen_mod:get_opt(include_body, Opts); include_body(Host) -> gen_mod:get_module_opt(Host, mod_push, include_body). -spec include_sender(gen_mod:opts() | global | binary()) -> boolean(). include_sender(Opts) when is_map(Opts) -> gen_mod:get_opt(include_sender, Opts); include_sender(Host) -> gen_mod:get_module_opt(Host, mod_push, include_sender). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_push, use_cache). ejabberd-21.12/src/mod_muc_sql.erl0000644000232200023220000003701114154362354017431 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_muc_sql.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_muc_sql). -behaviour(mod_muc). -behaviour(mod_muc_room). %% API -export([init/2, store_room/5, store_changes/4, restore_room/3, forget_room/3, can_use_nick/4, get_rooms/2, get_nick/3, set_nick/4, import/3, export/1]). -export([register_online_room/4, unregister_online_room/4, find_online_room/3, get_online_rooms/3, count_online_rooms/2, rsm_supported/0, register_online_user/4, unregister_online_user/4, count_online_rooms_by_user/3, get_online_rooms_by_user/3, get_subscribed_rooms/3, get_rooms_without_subscribers/2, find_online_room_by_pid/2]). -export([set_affiliation/6, set_affiliations/4, get_affiliation/5, get_affiliations/3, search_affiliation/4]). -include_lib("xmpp/include/jid.hrl"). -include("mod_muc.hrl"). -include("logger.hrl"). -include("ejabberd_sql_pt.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(Host, Opts) -> case gen_mod:ram_db_mod(Opts, mod_muc) of ?MODULE -> clean_tables(Host); _ -> ok end. store_room(LServer, Host, Name, Opts, ChangesHints) -> {Subs, Opts2} = case lists:keytake(subscribers, 1, Opts) of {value, {subscribers, S}, OptN} -> {S, OptN}; _ -> {[], Opts} end, SOpts = misc:term_to_expr(Opts2), F = fun () -> ?SQL_UPSERT_T( "muc_room", ["!name=%(Name)s", "!host=%(Host)s", "server_host=%(LServer)s", "opts=%(SOpts)s"]), case ChangesHints of Changes when is_list(Changes) -> [change_room(Host, Name, Change) || Change <- Changes]; _ -> ejabberd_sql:sql_query_t( ?SQL("delete from muc_room_subscribers where " "room=%(Name)s and host=%(Host)s")), [change_room(Host, Name, {add_subscription, JID, Nick, Nodes}) || {JID, Nick, Nodes} <- Subs] end end, ejabberd_sql:sql_transaction(LServer, F). store_changes(LServer, Host, Name, Changes) -> F = fun () -> [change_room(Host, Name, Change) || Change <- Changes] end, ejabberd_sql:sql_transaction(LServer, F). change_room(Host, Room, {add_subscription, JID, Nick, Nodes}) -> SJID = jid:encode(JID), SNodes = misc:term_to_expr(Nodes), ?SQL_UPSERT_T( "muc_room_subscribers", ["!jid=%(SJID)s", "!host=%(Host)s", "!room=%(Room)s", "nick=%(Nick)s", "nodes=%(SNodes)s"]); change_room(Host, Room, {del_subscription, JID}) -> SJID = jid:encode(JID), ejabberd_sql:sql_query_t(?SQL("delete from muc_room_subscribers where " "room=%(Room)s and host=%(Host)s and jid=%(SJID)s")); change_room(Host, Room, Change) -> ?ERROR_MSG("Unsupported change on room ~ts@~ts: ~p", [Room, Host, Change]). restore_room(LServer, Host, Name) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(opts)s from muc_room where name=%(Name)s" " and host=%(Host)s")) of {selected, [{Opts}]} -> OptsD = ejabberd_sql:decode_term(Opts), case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers where room=%(Name)s" " and host=%(Host)s")) of {selected, []} -> OptsR = mod_muc:opts_to_binary(OptsD), case lists:keymember(subscribers, 1, OptsD) of true -> store_room(LServer, Host, Name, OptsR, undefined); _ -> ok end, OptsR; {selected, Subs} -> SubData = lists:map( fun({Jid, Nick, Nodes}) -> {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)} end, Subs), Opts2 = lists:keystore(subscribers, 1, OptsD, {subscribers, SubData}), mod_muc:opts_to_binary(Opts2); _ -> error end; _ -> error end. forget_room(LServer, Host, Name) -> F = fun () -> ejabberd_sql:sql_query_t( ?SQL("delete from muc_room where name=%(Name)s" " and host=%(Host)s")), ejabberd_sql:sql_query_t( ?SQL("delete from muc_room_subscribers where room=%(Name)s" " and host=%(Host)s")) end, ejabberd_sql:sql_transaction(LServer, F). can_use_nick(LServer, Host, JID, Nick) -> SJID = jid:encode(jid:tolower(jid:remove_resource(JID))), case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(jid)s from muc_registered " "where nick=%(Nick)s" " and host=%(Host)s")) of {selected, [{SJID1}]} -> SJID == SJID1; _ -> true end. get_rooms_without_subscribers(LServer, Host) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s, @(opts)s from muc_room" " where host=%(Host)s")) of {selected, RoomOpts} -> lists:map( fun({Room, Opts}) -> OptsD = ejabberd_sql:decode_term(Opts), #muc_room{name_host = {Room, Host}, opts = mod_muc:opts_to_binary(OptsD)} end, RoomOpts); _Err -> [] end. get_rooms(LServer, Host) -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(name)s, @(opts)s from muc_room" " where host=%(Host)s")) of {selected, RoomOpts} -> case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(room)s, @(jid)s, @(nick)s, @(nodes)s from muc_room_subscribers" " where host=%(Host)s")) of {selected, Subs} -> SubsD = lists:foldl( fun({Room, Jid, Nick, Nodes}, Dict) -> Sub = {jid:decode(Jid), Nick, ejabberd_sql:decode_term(Nodes)}, maps:update_with( Room, fun(SubAcc) -> [Sub | SubAcc] end, [Sub], Dict) end, maps:new(), Subs), lists:map( fun({Room, Opts}) -> OptsD = ejabberd_sql:decode_term(Opts), OptsD2 = case {maps:find(Room, SubsD), lists:keymember(subscribers, 1, OptsD)} of {_, true} -> store_room(LServer, Host, Room, mod_muc:opts_to_binary(OptsD), undefined), OptsD; {{ok, SubsI}, false} -> lists:keystore(subscribers, 1, OptsD, {subscribers, SubsI}); _ -> OptsD end, #muc_room{name_host = {Room, Host}, opts = mod_muc:opts_to_binary(OptsD2)} end, RoomOpts); _Err -> [] end; _Err -> [] end. get_nick(LServer, Host, From) -> SJID = jid:encode(jid:tolower(jid:remove_resource(From))), case catch ejabberd_sql:sql_query( LServer, ?SQL("select @(nick)s from muc_registered where" " jid=%(SJID)s and host=%(Host)s")) of {selected, [{Nick}]} -> Nick; _ -> error end. set_nick(LServer, Host, From, Nick) -> JID = jid:encode(jid:tolower(jid:remove_resource(From))), F = fun () -> case Nick of <<"">> -> ejabberd_sql:sql_query_t( ?SQL("delete from muc_registered where" " jid=%(JID)s and host=%(Host)s")), ok; _ -> Allow = case ejabberd_sql:sql_query_t( ?SQL("select @(jid)s from muc_registered" " where nick=%(Nick)s" " and host=%(Host)s")) of {selected, [{J}]} -> J == JID; _ -> true end, if Allow -> ?SQL_UPSERT_T( "muc_registered", ["!jid=%(JID)s", "!host=%(Host)s", "server_host=%(LServer)s", "nick=%(Nick)s"]), ok; true -> false end end end, ejabberd_sql:sql_transaction(LServer, F). set_affiliation(_ServerHost, _Room, _Host, _JID, _Affiliation, _Reason) -> {error, not_implemented}. set_affiliations(_ServerHost, _Room, _Host, _Affiliations) -> {error, not_implemented}. get_affiliation(_ServerHost, _Room, _Host, _LUser, _LServer) -> {error, not_implemented}. get_affiliations(_ServerHost, _Room, _Host) -> {error, not_implemented}. search_affiliation(_ServerHost, _Room, _Host, _Affiliation) -> {error, not_implemented}. register_online_room(ServerHost, Room, Host, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), case ?SQL_UPSERT(ServerHost, "muc_online_room", ["!name=%(Room)s", "!host=%(Host)s", "server_host=%(ServerHost)s", "node=%(NodeS)s", "pid=%(PidS)s"]) of ok -> ok; Err -> Err end. unregister_online_room(ServerHost, Room, Host, Pid) -> %% TODO: report errors PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_room where name=%(Room)s and " "host=%(Host)s and node=%(NodeS)s and pid=%(PidS)s")). find_online_room(ServerHost, Room, Host) -> case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(pid)s, @(node)s from muc_online_room where " "name=%(Room)s and host=%(Host)s")) of {selected, [{PidS, NodeS}]} -> try {ok, misc:decode_pid(PidS, NodeS)} catch _:{bad_node, _} -> error end; {selected, []} -> error; _Err -> error end. find_online_room_by_pid(ServerHost, Pid) -> PidS = misc:encode_pid(Pid), NodeS = erlang:atom_to_binary(node(Pid), latin1), case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(name)s, @(host)s from muc_online_room where " "node=%(NodeS)s and pid=%(PidS)s")) of {selected, [{Room, Host}]} -> {ok, Room, Host}; {selected, []} -> error; _Err -> error end. count_online_rooms(ServerHost, Host) -> case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(count(*))d from muc_online_room " "where host=%(Host)s")) of {selected, [{Num}]} -> Num; _Err -> 0 end. get_online_rooms(ServerHost, Host, _RSM) -> case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(name)s, @(pid)s, @(node)s from muc_online_room " "where host=%(Host)s")) of {selected, Rows} -> lists:flatmap( fun({Room, PidS, NodeS}) -> try [{Room, Host, misc:decode_pid(PidS, NodeS)}] catch _:{bad_node, _} -> [] end end, Rows); _Err -> [] end. rsm_supported() -> false. register_online_user(ServerHost, {U, S, R}, Room, Host) -> NodeS = erlang:atom_to_binary(node(), latin1), case ?SQL_UPSERT(ServerHost, "muc_online_users", ["!username=%(U)s", "!server=%(S)s", "!resource=%(R)s", "!name=%(Room)s", "!host=%(Host)s", "server_host=%(ServerHost)s", "node=%(NodeS)s"]) of ok -> ok; Err -> Err end. unregister_online_user(ServerHost, {U, S, R}, Room, Host) -> %% TODO: report errors ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_users where username=%(U)s and " "server=%(S)s and resource=%(R)s and name=%(Room)s and " "host=%(Host)s")). count_online_rooms_by_user(ServerHost, U, S) -> case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(count(*))d from muc_online_users where " "username=%(U)s and server=%(S)s")) of {selected, [{Num}]} -> Num; _Err -> 0 end. get_online_rooms_by_user(ServerHost, U, S) -> case ejabberd_sql:sql_query( ServerHost, ?SQL("select @(name)s, @(host)s from muc_online_users where " "username=%(U)s and server=%(S)s")) of {selected, Rows} -> Rows; _Err -> [] end. export(_Server) -> [{muc_room, fun(Host, #muc_room{name_host = {Name, RoomHost}, opts = Opts}) -> case str:suffix(Host, RoomHost) of true -> SOpts = misc:term_to_expr(Opts), [?SQL("delete from muc_room where name=%(Name)s" " and host=%(RoomHost)s;"), ?SQL_INSERT( "muc_room", ["name=%(Name)s", "host=%(RoomHost)s", "server_host=%(Host)s", "opts=%(SOpts)s"])]; false -> [] end end}, {muc_registered, fun(Host, #muc_registered{us_host = {{U, S}, RoomHost}, nick = Nick}) -> case str:suffix(Host, RoomHost) of true -> SJID = jid:encode(jid:make(U, S)), [?SQL("delete from muc_registered where" " jid=%(SJID)s and host=%(RoomHost)s;"), ?SQL_INSERT( "muc_registered", ["jid=%(SJID)s", "host=%(RoomHost)s", "server_host=%(Host)s", "nick=%(Nick)s"])]; false -> [] end end}]. import(_, _, _) -> ok. get_subscribed_rooms(LServer, Host, Jid) -> JidS = jid:encode(Jid), case ejabberd_sql:sql_query( LServer, ?SQL("select @(room)s, @(nick)s, @(nodes)s from muc_room_subscribers " "where jid=%(JidS)s and host=%(Host)s")) of {selected, Subs} -> {ok, [{jid:make(Room, Host), Nick, ejabberd_sql:decode_term(Nodes)} || {Room, Nick, Nodes} <- Subs]}; _Error -> {error, db_failure} end. %%%=================================================================== %%% Internal functions %%%=================================================================== clean_tables(ServerHost) -> NodeS = erlang:atom_to_binary(node(), latin1), ?DEBUG("Cleaning SQL muc_online_room table...", []), case ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_room where node=%(NodeS)s")) of {updated, _} -> ok; Err1 -> ?ERROR_MSG("Failed to clean 'muc_online_room' table: ~p", [Err1]), Err1 end, ?DEBUG("Cleaning SQL muc_online_users table...", []), case ejabberd_sql:sql_query( ServerHost, ?SQL("delete from muc_online_users where node=%(NodeS)s")) of {updated, _} -> ok; Err2 -> ?ERROR_MSG("Failed to clean 'muc_online_users' table: ~p", [Err2]), Err2 end. ejabberd-21.12/src/mod_offline_opt.erl0000644000232200023220000000516514154362354020277 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_offline_opt). -export([access_max_user_messages/1]). -export([bounce_groupchat/1]). -export([cache_life_time/1]). -export([cache_size/1]). -export([db_type/1]). -export([store_empty_body/1]). -export([store_groupchat/1]). -export([use_cache/1]). -export([use_mam_for_storage/1]). -spec access_max_user_messages(gen_mod:opts() | global | binary()) -> atom() | [ejabberd_shaper:shaper_rule()]. access_max_user_messages(Opts) when is_map(Opts) -> gen_mod:get_opt(access_max_user_messages, Opts); access_max_user_messages(Host) -> gen_mod:get_module_opt(Host, mod_offline, access_max_user_messages). -spec bounce_groupchat(gen_mod:opts() | global | binary()) -> boolean(). bounce_groupchat(Opts) when is_map(Opts) -> gen_mod:get_opt(bounce_groupchat, Opts); bounce_groupchat(Host) -> gen_mod:get_module_opt(Host, mod_offline, bounce_groupchat). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_offline, cache_life_time). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_offline, cache_size). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_offline, db_type). -spec store_empty_body(gen_mod:opts() | global | binary()) -> 'false' | 'true' | 'unless_chat_state'. store_empty_body(Opts) when is_map(Opts) -> gen_mod:get_opt(store_empty_body, Opts); store_empty_body(Host) -> gen_mod:get_module_opt(Host, mod_offline, store_empty_body). -spec store_groupchat(gen_mod:opts() | global | binary()) -> boolean(). store_groupchat(Opts) when is_map(Opts) -> gen_mod:get_opt(store_groupchat, Opts); store_groupchat(Host) -> gen_mod:get_module_opt(Host, mod_offline, store_groupchat). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_offline, use_cache). -spec use_mam_for_storage(gen_mod:opts() | global | binary()) -> boolean(). use_mam_for_storage(Opts) when is_map(Opts) -> gen_mod:get_opt(use_mam_for_storage, Opts); use_mam_for_storage(Host) -> gen_mod:get_module_opt(Host, mod_offline, use_mam_for_storage). ejabberd-21.12/src/mod_delegation.erl0000644000232200023220000003665214154362354020113 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_delegation.erl %%% Author : Anna Mukharram %%% Purpose : XEP-0355: Namespace Delegation %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_delegation). -author('amuhar3@gmail.com'). -protocol({xep, 0355, '0.4.1'}). -behaviour(gen_server). -behaviour(gen_mod). %% API -export([start/2, stop/1, reload/3, mod_opt_type/1, depends/2, mod_options/1]). -export([mod_doc/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([component_connected/1, component_disconnected/2, ejabberd_local/1, ejabberd_sm/1, decode_iq_subel/1, disco_local_features/5, disco_sm_features/5, disco_local_identity/5, disco_sm_identity/5]). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -type route_type() :: ejabberd_sm | ejabberd_local. -type delegations() :: #{{binary(), route_type()} => {binary(), disco_info()}}. -record(state, {server_host = <<"">> :: binary()}). %%%=================================================================== %%% API %%%=================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(_Host, _NewOpts, _OldOpts) -> ok. mod_opt_type(namespaces) -> econf:and_then( econf:map( econf:binary(), econf:options( #{filtering => econf:list(econf:binary()), access => econf:acl()})), fun(L) -> lists:map( fun({NS, Opts}) -> Attrs = proplists:get_value(filtering, Opts, []), Access = proplists:get_value(access, Opts, none), {NS, Attrs, Access} end, L) end). -spec mod_options(binary()) -> [{namespaces, [{binary(), [binary()], acl:acl()}]} | {atom(), term()}]. mod_options(_Host) -> [{namespaces, []}]. mod_doc() -> #{desc => [?T("This module is an implementation of " "https://xmpp.org/extensions/xep-0355.html" "[XEP-0355: Namespace Delegation]. " "Only admin mode has been implemented by now. " "Namespace delegation allows external services to " "handle IQ using specific namespace. This may be applied " "for external PEP service."), "", ?T("WARNING: Security issue: Namespace delegation gives components " "access to sensitive data, so permission should be granted " "carefully, only if you trust the component."), "", ?T("NOTE: This module is complementary to _`mod_privilege`_ but can " "also be used separately.")], opts => [{namespaces, #{value => "{Namespace: Options}", desc => ?T("If you want to delegate namespaces to a component, " "specify them in this option, and associate them " "to an access rule. The 'Options' are:")}, [{filtering, #{value => ?T("Attributes"), desc => ?T("The list of attributes. Currently not used.")}}, {access, #{value => ?T("AccessName"), desc => ?T("The option defines which components are allowed " "for namespace delegation. The default value is 'none'.")}}]}], example => [{?T("Make sure you do not delegate the same namespace to several " "services at the same time. As in the example provided later, " "to have the 'sat-pubsub.example.org' component perform " "correctly disable the 'mod_pubsub' module."), ["access_rules:", " external_pubsub:", " allow: external_component", " external_mam:", " allow: external_component", "", "acl:", " external_component:", " server: sat-pubsub.example.org", "", "modules:", " ...", " mod_delegation:", " namespaces:", " urn:xmpp:mam:1:", " access: external_mam", " http://jabber.org/protocol/pubsub:", " access: external_pubsub"]}]}. depends(_, _) -> []. -spec decode_iq_subel(xmpp_element() | xmlel()) -> xmpp_element() | xmlel(). %% Tell gen_iq_handler not to auto-decode IQ payload decode_iq_subel(El) -> El. -spec component_connected(binary()) -> ok. component_connected(Host) -> lists:foreach( fun(ServerHost) -> Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), gen_server:cast(Proc, {component_connected, Host}) end, ejabberd_option:hosts()). -spec component_disconnected(binary(), binary()) -> ok. component_disconnected(Host, _Reason) -> lists:foreach( fun(ServerHost) -> Proc = gen_mod:get_module_proc(ServerHost, ?MODULE), gen_server:cast(Proc, {component_disconnected, Host}) end, ejabberd_option:hosts()). -spec ejabberd_local(iq()) -> iq(). ejabberd_local(IQ) -> process_iq(IQ, ejabberd_local). -spec ejabberd_sm(iq()) -> iq(). ejabberd_sm(IQ) -> process_iq(IQ, ejabberd_sm). -spec disco_local_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). disco_local_features(Acc, From, To, Node, Lang) -> disco_features(Acc, From, To, Node, Lang, ejabberd_local). -spec disco_sm_features(mod_disco:features_acc(), jid(), jid(), binary(), binary()) -> mod_disco:features_acc(). disco_sm_features(Acc, From, To, Node, Lang) -> disco_features(Acc, From, To, Node, Lang, ejabberd_sm). -spec disco_local_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_local_identity(Acc, From, To, Node, Lang) -> disco_identity(Acc, From, To, Node, Lang, ejabberd_local). -spec disco_sm_identity([identity()], jid(), jid(), binary(), binary()) -> [identity()]. disco_sm_identity(Acc, From, To, Node, Lang) -> disco_identity(Acc, From, To, Node, Lang, ejabberd_sm). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([Host|_]) -> process_flag(trap_exit, true), catch ets:new(?MODULE, [named_table, public, {heir, erlang:group_leader(), none}]), ejabberd_hooks:add(component_connected, ?MODULE, component_connected, 50), ejabberd_hooks:add(component_disconnected, ?MODULE, component_disconnected, 50), ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_local_features, 50), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_local_identity, 50), ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_sm_identity, 50), {ok, #state{server_host = Host}}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({component_connected, Host}, State) -> ServerHost = State#state.server_host, To = jid:make(Host), NSAttrsAccessList = mod_delegation_opt:namespaces(ServerHost), lists:foreach( fun({NS, _Attrs, Access}) -> case acl:match_rule(ServerHost, Access, To) of allow -> send_disco_queries(ServerHost, Host, NS); deny -> ?DEBUG("Denied delegation for ~ts on ~ts", [Host, NS]) end end, NSAttrsAccessList), {noreply, State}; handle_cast({component_disconnected, Host}, State) -> ServerHost = State#state.server_host, Delegations = maps:filter( fun({NS, Type}, {H, _}) when H == Host -> ?INFO_MSG("Remove delegation of namespace '~ts' " "from external component '~ts'", [NS, Host]), gen_iq_handler:remove_iq_handler(Type, ServerHost, NS), false; (_, _) -> true end, get_delegations(ServerHost)), set_delegations(ServerHost, Delegations), {noreply, State}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({iq_reply, ResIQ, {disco_info, Type, Host, NS}}, State) -> case ResIQ of #iq{type = result, sub_els = [SubEl]} -> try xmpp:decode(SubEl) of #disco_info{} = Info -> ServerHost = State#state.server_host, process_disco_info(ServerHost, Type, Host, NS, Info) catch _:{xmpp_codec, _} -> ok end; _ -> ok end, {noreply, State}; handle_info({iq_reply, ResIQ, #iq{} = IQ}, State) -> process_iq_result(IQ, ResIQ), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, State) -> ServerHost = State#state.server_host, case gen_mod:is_loaded_elsewhere(ServerHost, ?MODULE) of false -> ejabberd_hooks:delete(component_connected, ?MODULE, component_connected, 50), ejabberd_hooks:delete(component_disconnected, ?MODULE, component_disconnected, 50); true -> ok end, ejabberd_hooks:delete(disco_local_features, ServerHost, ?MODULE, disco_local_features, 50), ejabberd_hooks:delete(disco_sm_features, ServerHost, ?MODULE, disco_sm_features, 50), ejabberd_hooks:delete(disco_local_identity, ServerHost, ?MODULE, disco_local_identity, 50), ejabberd_hooks:delete(disco_sm_identity, ServerHost, ?MODULE, disco_sm_identity, 50), lists:foreach( fun({NS, Type}) -> gen_iq_handler:remove_iq_handler(Type, ServerHost, NS) end, maps:keys(get_delegations(ServerHost))), ets:delete(?MODULE, ServerHost). code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec get_delegations(binary()) -> delegations(). get_delegations(Host) -> try ets:lookup_element(?MODULE, Host, 2) catch _:badarg -> #{} end. -spec set_delegations(binary(), delegations()) -> true. set_delegations(ServerHost, Delegations) -> case maps:size(Delegations) of 0 -> ets:delete(?MODULE, ServerHost); _ -> ets:insert(?MODULE, {ServerHost, Delegations}) end. -spec process_iq(iq(), route_type()) -> ignore | iq(). process_iq(#iq{to = To, lang = Lang, sub_els = [SubEl]} = IQ, Type) -> LServer = To#jid.lserver, NS = xmpp:get_ns(SubEl), Delegations = get_delegations(LServer), case maps:find({NS, Type}, Delegations) of {ok, {Host, _}} -> Delegation = #delegation{ forwarded = #forwarded{sub_els = [IQ]}}, NewFrom = jid:make(LServer), NewTo = jid:make(Host), ejabberd_router:route_iq( #iq{type = set, from = NewFrom, to = NewTo, sub_els = [Delegation]}, IQ, gen_mod:get_module_proc(LServer, ?MODULE)), ignore; error -> Txt = ?T("Failed to map delegated namespace to external component"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)) end. -spec process_iq_result(iq(), iq()) -> ok. process_iq_result(#iq{from = From, to = To, id = ID, lang = Lang} = IQ, #iq{type = result} = ResIQ) -> try CodecOpts = ejabberd_config:codec_options(), #delegation{forwarded = #forwarded{sub_els = [SubEl]}} = xmpp:get_subtag(ResIQ, #delegation{}), case xmpp:decode(SubEl, ?NS_CLIENT, CodecOpts) of #iq{from = To, to = From, type = Type, id = ID} = Reply when Type == error; Type == result -> ejabberd_router:route(Reply) end catch _:_ -> ?ERROR_MSG("Got iq-result with invalid delegated " "payload:~n~ts", [xmpp:pp(ResIQ)]), Txt = ?T("External component failure"), Err = xmpp:err_internal_server_error(Txt, Lang), ejabberd_router:route_error(IQ, Err) end; process_iq_result(#iq{from = From, to = To}, #iq{type = error} = ResIQ) -> Err = xmpp:set_from_to(ResIQ, To, From), ejabberd_router:route(Err); process_iq_result(#iq{lang = Lang} = IQ, timeout) -> Txt = ?T("External component timeout"), Err = xmpp:err_internal_server_error(Txt, Lang), ejabberd_router:route_error(IQ, Err). -spec process_disco_info(binary(), route_type(), binary(), binary(), disco_info()) -> ok. process_disco_info(ServerHost, Type, Host, NS, Info) -> From = jid:make(ServerHost), To = jid:make(Host), Delegations = get_delegations(ServerHost), case maps:find({NS, Type}, Delegations) of error -> Msg = #message{from = From, to = To, sub_els = [#delegation{delegated = [#delegated{ns = NS}]}]}, Delegations1 = maps:put({NS, Type}, {Host, Info}, Delegations), gen_iq_handler:add_iq_handler(Type, ServerHost, NS, ?MODULE, Type), ejabberd_router:route(Msg), set_delegations(ServerHost, Delegations1), ?INFO_MSG("Namespace '~ts' is delegated to external component '~ts'", [NS, Host]); {ok, {AnotherHost, _}} -> ?WARNING_MSG("Failed to delegate namespace '~ts' to " "external component '~ts' because it's already " "delegated to '~ts'", [NS, Host, AnotherHost]) end. -spec send_disco_queries(binary(), binary(), binary()) -> ok. send_disco_queries(LServer, Host, NS) -> From = jid:make(LServer), To = jid:make(Host), lists:foreach( fun({Type, Node}) -> ejabberd_router:route_iq( #iq{type = get, from = From, to = To, sub_els = [#disco_info{node = Node}]}, {disco_info, Type, Host, NS}, gen_mod:get_module_proc(LServer, ?MODULE)) end, [{ejabberd_local, <<(?NS_DELEGATION)/binary, "::", NS/binary>>}, {ejabberd_sm, <<(?NS_DELEGATION)/binary, ":bare:", NS/binary>>}]). -spec disco_features(mod_disco:features_acc(), jid(), jid(), binary(), binary(), route_type()) -> mod_disco:features_acc(). disco_features(Acc, _From, To, <<"">>, _Lang, Type) -> Delegations = get_delegations(To#jid.lserver), Features = my_features(Type) ++ lists:flatmap( fun({{_, T}, {_, Info}}) when T == Type -> Info#disco_info.features; (_) -> [] end, maps:to_list(Delegations)), case Acc of empty when Features /= [] -> {result, Features}; {result, Fs} -> {result, Fs ++ Features}; _ -> Acc end; disco_features(Acc, _, _, _, _, _) -> Acc. -spec disco_identity([identity()], jid(), jid(), binary(), binary(), route_type()) -> [identity()]. disco_identity(Acc, _From, To, <<"">>, _Lang, Type) -> Delegations = get_delegations(To#jid.lserver), Identities = lists:flatmap( fun({{_, T}, {_, Info}}) when T == Type -> Info#disco_info.identities; (_) -> [] end, maps:to_list(Delegations)), Acc ++ Identities; disco_identity(Acc, _From, _To, _Node, _Lang, _Type) -> Acc. my_features(ejabberd_local) -> [?NS_DELEGATION]; my_features(ejabberd_sm) -> []. ejabberd-21.12/src/mod_roster_mnesia.erl0000644000232200023220000002341514154362354020643 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : mod_roster_mnesia.erl %%% Author : Evgeny Khramtsov %%% Created : 13 Apr 2016 by Evgeny Khramtsov %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_roster_mnesia). -behaviour(mod_roster). %% API -export([init/2, read_roster_version/2, write_roster_version/4, get_roster/2, get_roster_item/3, roster_subscribe/4, remove_user/2, update_roster/4, del_roster/3, transaction/2, read_subscription_and_groups/3, import/3, create_roster/1, process_rosteritems/5, use_cache/2]). -export([need_transform/1, transform/1]). -include("mod_roster.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). %%%=================================================================== %%% API %%%=================================================================== init(_Host, _Opts) -> ejabberd_mnesia:create(?MODULE, roster, [{disc_only_copies, [node()]}, {attributes, record_info(fields, roster)}, {index, [us]}]), ejabberd_mnesia:create(?MODULE, roster_version, [{disc_only_copies, [node()]}, {attributes, record_info(fields, roster_version)}]). use_cache(Host, Table) -> case mnesia:table_info(Table, storage_type) of disc_only_copies -> mod_roster_opt:use_cache(Host); _ -> false end. read_roster_version(LUser, LServer) -> US = {LUser, LServer}, case mnesia:dirty_read(roster_version, US) of [#roster_version{version = V}] -> {ok, V}; [] -> error end. write_roster_version(LUser, LServer, InTransaction, Ver) -> US = {LUser, LServer}, if InTransaction -> mnesia:write(#roster_version{us = US, version = Ver}); true -> mnesia:dirty_write(#roster_version{us = US, version = Ver}) end. get_roster(LUser, LServer) -> {ok, mnesia:dirty_index_read(roster, {LUser, LServer}, #roster.us)}. get_roster_item(LUser, LServer, LJID) -> case mnesia:read({roster, {LUser, LServer, LJID}}) of [I] -> {ok, I}; [] -> error end. roster_subscribe(_LUser, _LServer, _LJID, Item) -> mnesia:write(Item). remove_user(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> lists:foreach( fun (R) -> mnesia:delete_object(R) end, mnesia:index_read(roster, US, #roster.us)) end, mnesia:transaction(F). update_roster(_LUser, _LServer, _LJID, Item) -> mnesia:write(Item). del_roster(LUser, LServer, LJID) -> mnesia:delete({roster, {LUser, LServer, LJID}}). read_subscription_and_groups(LUser, LServer, LJID) -> case mnesia:dirty_read(roster, {LUser, LServer, LJID}) of [#roster{subscription = Subscription, ask = Ask, groups = Groups}] -> {ok, {Subscription, Ask, Groups}}; _ -> error end. transaction(_LServer, F) -> mnesia:transaction(F). create_roster(RItem) -> mnesia:dirty_write(RItem). import(_LServer, <<"rosterusers">>, #roster{} = R) -> mnesia:dirty_write(R); import(LServer, <<"roster_version">>, [LUser, Ver]) -> RV = #roster_version{us = {LUser, LServer}, version = Ver}, mnesia:dirty_write(RV). need_transform({roster, {U, S, _}, _, _, _, _, _, _, _, _}) when is_list(U) orelse is_list(S) -> ?INFO_MSG("Mnesia table 'roster' will be converted to binary", []), true; need_transform({roster_version, {U, S}, Ver}) when is_list(U) orelse is_list(S) orelse is_list(Ver) -> ?INFO_MSG("Mnesia table 'roster_version' will be converted to binary", []), true; need_transform(_) -> false. transform(#roster{usj = {U, S, {LU, LS, LR}}, us = {U1, S1}, jid = {U2, S2, R2}, name = Name, groups = Gs, askmessage = Ask, xs = Xs} = R) -> R#roster{usj = {iolist_to_binary(U), iolist_to_binary(S), {iolist_to_binary(LU), iolist_to_binary(LS), iolist_to_binary(LR)}}, us = {iolist_to_binary(U1), iolist_to_binary(S1)}, jid = {iolist_to_binary(U2), iolist_to_binary(S2), iolist_to_binary(R2)}, name = iolist_to_binary(Name), groups = [iolist_to_binary(G) || G <- Gs], askmessage = try iolist_to_binary(Ask) catch _:_ -> <<"">> end, xs = [fxml:to_xmlel(X) || X <- Xs]}; transform(#roster_version{us = {U, S}, version = Ver} = R) -> R#roster_version{us = {iolist_to_binary(U), iolist_to_binary(S)}, version = iolist_to_binary(Ver)}. %%%=================================================================== process_rosteritems(ActionS, SubsS, AsksS, UsersS, ContactsS) -> Action = case ActionS of "list" -> list; "delete" -> delete end, Subs = lists:foldl( fun(any, _) -> [none, from, to, both]; (Sub, Subs) -> [Sub | Subs] end, [], [list_to_atom(S) || S <- string:tokens(SubsS, ":")] ), Asks = lists:foldl( fun(any, _) -> [none, out, in]; (Ask, Asks) -> [Ask | Asks] end, [], [list_to_atom(S) || S <- string:tokens(AsksS, ":")] ), Users = lists:foldl( fun("any", _) -> ["*", "*@*"]; (U, Us) -> [U | Us] end, [], [S || S <- string:tokens(UsersS, ":")] ), Contacts = lists:foldl( fun("any", _) -> ["*", "*@*"]; (U, Us) -> [U | Us] end, [], [S || S <- string:tokens(ContactsS, ":")] ), rosteritem_purge({Action, Subs, Asks, Users, Contacts}). %% @spec ({Action::atom(), Subs::[atom()], Asks::[atom()], User::string(), Contact::string()}) -> {atomic, ok} rosteritem_purge(Options) -> Num_rosteritems = mnesia:table_info(roster, size), io:format("There are ~p roster items in total.~n", [Num_rosteritems]), Key = mnesia:dirty_first(roster), rip(Key, Options, {0, Num_rosteritems, 0, 0}, []). rip('$end_of_table', _Options, Counters, Res) -> print_progress_line(Counters), Res; rip(Key, Options, {Pr, NT, NV, ND}, Res) -> Key_next = mnesia:dirty_next(roster, Key), {Action, _, _, _, _} = Options, {ND2, Res2} = case decide_rip(Key, Options) of true -> Jids = apply_action(Action, Key), {ND+1, [Jids | Res]}; false -> {ND, Res} end, NV2 = NV+1, Pr2 = print_progress_line({Pr, NT, NV2, ND2}), rip(Key_next, Options, {Pr2, NT, NV2, ND2}, Res2). apply_action(list, Key) -> {User, Server, JID} = Key, {RUser, RServer, _} = JID, Jid1string = <>, Jid2string = <>, io:format("Matches: ~ts ~ts~n", [Jid1string, Jid2string]), {Jid1string, Jid2string}; apply_action(delete, Key) -> R = apply_action(list, Key), mnesia:dirty_delete(roster, Key), R. print_progress_line({_Pr, 0, _NV, _ND}) -> ok; print_progress_line({Pr, NT, NV, ND}) -> Pr2 = trunc((NV/NT)*100), case Pr == Pr2 of true -> ok; false -> io:format("Progress ~p% - visited ~p - deleted ~p~n", [Pr2, NV, ND]) end, Pr2. decide_rip(Key, {_Action, Subs, Asks, User, Contact}) -> case catch mnesia:dirty_read(roster, Key) of [RI] -> lists:member(RI#roster.subscription, Subs) andalso lists:member(RI#roster.ask, Asks) andalso decide_rip_jid(RI#roster.us, User) andalso decide_rip_jid(RI#roster.jid, Contact); _ -> false end. %% Returns true if the server of the JID is included in the servers decide_rip_jid({UName, UServer, _UResource}, Match_list) -> decide_rip_jid({UName, UServer}, Match_list); decide_rip_jid({UName, UServer}, Match_list) -> lists:any( fun(Match_string) -> MJID = jid:decode(list_to_binary(Match_string)), MName = MJID#jid.luser, MServer = MJID#jid.lserver, Is_server = is_glob_match(UServer, MServer), case MName of <<>> when UName == <<>> -> Is_server; <<>> -> false; _ -> Is_server andalso is_glob_match(UName, MName) end end, Match_list). %% Copied from ejabberd-2.0.0/src/acl.erl is_regexp_match(String, RegExp) -> case ejabberd_regexp:run(String, RegExp) of nomatch -> false; match -> true; {error, ErrDesc} -> io:format( "Wrong regexp ~p in ACL: ~p", [RegExp, ErrDesc]), false end. is_glob_match(String, <<"!", Glob/binary>>) -> not is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)); is_glob_match(String, Glob) -> is_regexp_match(String, ejabberd_regexp:sh_to_awk(Glob)). %%%=================================================================== %%% Internal functions %%%=================================================================== ejabberd-21.12/src/ejabberd_old_config.erl0000644000232200023220000005620714154362354021060 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% Purpose: Transform old-style Erlang config to YAML config %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_old_config). %% API -export([read_file/1]). -include("logger.hrl"). %%%=================================================================== %%% API %%%=================================================================== read_file(File) -> case consult(File) of {ok, Terms1} -> ?INFO_MSG("Converting from old configuration format", []), Terms2 = strings_to_binary(Terms1), Terms3 = transform(Terms2), Terms4 = transform_certfiles(Terms3), Terms5 = transform_host_config(Terms4), {ok, collect_options(Terms5)}; {error, Reason} -> {error, {old_config, File, Reason}} end. %%%=================================================================== %%% Internal functions %%%=================================================================== collect_options(Opts) -> {D, InvalidOpts} = lists:foldl( fun({K, V}, {D, Os}) when is_list(V) -> {orddict:append_list(K, V, D), Os}; ({K, V}, {D, Os}) -> {orddict:store(K, V, D), Os}; (Opt, {D, Os}) -> {D, [Opt|Os]} end, {orddict:new(), []}, Opts), InvalidOpts ++ orddict:to_list(D). transform(Opts) -> Opts1 = transform_register(Opts), Opts2 = transform_s2s(Opts1), Opts3 = transform_listeners(Opts2), Opts5 = transform_sql(Opts3), Opts6 = transform_shaper(Opts5), Opts7 = transform_s2s_out(Opts6), Opts8 = transform_acl(Opts7), Opts9 = transform_modules(Opts8), Opts10 = transform_globals(Opts9), collect_options(Opts10). %%%=================================================================== %%% mod_register %%%=================================================================== transform_register(Opts) -> try {value, {modules, ModOpts}, Opts1} = lists:keytake(modules, 1, Opts), {value, {?MODULE, RegOpts}, ModOpts1} = lists:keytake(?MODULE, 1, ModOpts), {value, {ip_access, L}, RegOpts1} = lists:keytake(ip_access, 1, RegOpts), true = is_list(L), ?WARNING_MSG("Old 'ip_access' format detected. " "The old format is still supported " "but it is better to fix your config: " "use access rules instead.", []), ACLs = lists:flatmap( fun({Action, S}) -> ACLName = misc:binary_to_atom( iolist_to_binary( ["ip_", S])), [{Action, ACLName}, {acl, ACLName, {ip, S}}] end, L), Access = {access, mod_register_networks, [{Action, ACLName} || {Action, ACLName} <- ACLs]}, [ACL || {acl, _, _} = ACL <- ACLs] ++ [Access, {modules, [{mod_register, [{ip_access, mod_register_networks}|RegOpts1]} | ModOpts1]}|Opts1] catch error:{badmatch, false} -> Opts end. %%%=================================================================== %%% ejabberd_s2s %%%=================================================================== transform_s2s(Opts) -> lists:foldl(fun transform_s2s/2, [], Opts). transform_s2s({{s2s_host, Host}, Action}, Opts) -> ?WARNING_MSG("Option 's2s_host' is deprecated.", []), ACLName = misc:binary_to_atom( iolist_to_binary(["s2s_access_", Host])), [{acl, ACLName, {server, Host}}, {access, s2s, [{Action, ACLName}]}, {s2s_access, s2s} | Opts]; transform_s2s({s2s_default_policy, Action}, Opts) -> ?WARNING_MSG("Option 's2s_default_policy' is deprecated. " "The option is still supported but it is better to " "fix your config: " "use 's2s_access' with an access rule.", []), [{access, s2s, [{Action, all}]}, {s2s_access, s2s} | Opts]; transform_s2s(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% ejabberd_s2s_out %%%=================================================================== transform_s2s_out(Opts) -> lists:foldl(fun transform_s2s_out/2, [], Opts). transform_s2s_out({outgoing_s2s_options, Families, Timeout}, Opts) -> ?WARNING_MSG("Option 'outgoing_s2s_options' is deprecated. " "The option is still supported " "but it is better to fix your config: " "use 'outgoing_s2s_timeout' and " "'outgoing_s2s_families' instead.", []), [{outgoing_s2s_families, Families}, {outgoing_s2s_timeout, Timeout} | Opts]; transform_s2s_out({s2s_dns_options, S2SDNSOpts}, AllOpts) -> ?WARNING_MSG("Option 's2s_dns_options' is deprecated. " "The option is still supported " "but it is better to fix your config: " "use 's2s_dns_timeout' and " "'s2s_dns_retries' instead", []), lists:foldr( fun({timeout, T}, AccOpts) -> [{s2s_dns_timeout, T}|AccOpts]; ({retries, R}, AccOpts) -> [{s2s_dns_retries, R}|AccOpts]; (_, AccOpts) -> AccOpts end, AllOpts, S2SDNSOpts); transform_s2s_out(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% ejabberd_listener %%%=================================================================== transform_listeners(Opts) -> lists:foldl(fun transform_listeners/2, [], Opts). transform_listeners({listen, LOpts}, Opts) -> [{listen, lists:map(fun transform_listener/1, LOpts)} | Opts]; transform_listeners(Opt, Opts) -> [Opt|Opts]. transform_listener({{Port, IP, Transport}, Mod, Opts}) -> IPStr = if is_tuple(IP) -> list_to_binary(inet_parse:ntoa(IP)); true -> IP end, Opts1 = lists:map( fun({ip, IPT}) when is_tuple(IPT) -> {ip, list_to_binary(inet_parse:ntoa(IP))}; (ssl) -> {tls, true}; (A) when is_atom(A) -> {A, true}; (Opt) -> Opt end, Opts), Opts2 = lists:foldl( fun(Opt, Acc) -> transform_listen_option(Mod, Opt, Acc) end, [], Opts1), TransportOpt = if Transport == tcp -> []; true -> [{transport, Transport}] end, IPOpt = if IPStr == <<"0.0.0.0">> -> []; true -> [{ip, IPStr}] end, IPOpt ++ TransportOpt ++ [{port, Port}, {module, Mod} | Opts2]; transform_listener({{Port, Transport}, Mod, Opts}) when Transport == tcp orelse Transport == udp -> transform_listener({{Port, all_zero_ip(Opts), Transport}, Mod, Opts}); transform_listener({{Port, IP}, Mod, Opts}) -> transform_listener({{Port, IP, tcp}, Mod, Opts}); transform_listener({Port, Mod, Opts}) -> transform_listener({{Port, all_zero_ip(Opts), tcp}, Mod, Opts}); transform_listener(Opt) -> Opt. transform_listen_option(ejabberd_http, captcha, Opts) -> [{captcha, true}|Opts]; transform_listen_option(ejabberd_http, register, Opts) -> [{register, true}|Opts]; transform_listen_option(ejabberd_http, web_admin, Opts) -> [{web_admin, true}|Opts]; transform_listen_option(ejabberd_http, http_bind, Opts) -> [{http_bind, true}|Opts]; transform_listen_option(ejabberd_http, http_poll, Opts) -> [{http_poll, true}|Opts]; transform_listen_option(ejabberd_http, {request_handlers, Hs}, Opts) -> Hs1 = lists:map( fun({PList, Mod}) when is_list(PList) -> Path = iolist_to_binary([[$/, P] || P <- PList]), {Path, Mod}; (Opt) -> Opt end, Hs), [{request_handlers, Hs1} | Opts]; transform_listen_option(ejabberd_service, {hosts, Hosts, O}, Opts) -> case lists:keyfind(hosts, 1, Opts) of {_, PrevHostOpts} -> NewHostOpts = lists:foldl( fun(H, Acc) -> dict:append_list(H, O, Acc) end, dict:from_list(PrevHostOpts), Hosts), [{hosts, dict:to_list(NewHostOpts)}| lists:keydelete(hosts, 1, Opts)]; _ -> [{hosts, [{H, O} || H <- Hosts]}|Opts] end; transform_listen_option(ejabberd_service, {host, Host, Os}, Opts) -> transform_listen_option(ejabberd_service, {hosts, [Host], Os}, Opts); transform_listen_option(ejabberd_xmlrpc, {access_commands, ACOpts}, Opts) -> NewACOpts = lists:map( fun({AName, ACmds, AOpts}) -> {AName, [{commands, ACmds}, {options, AOpts}]}; (Opt) -> Opt end, ACOpts), [{access_commands, NewACOpts}|Opts]; transform_listen_option(_, Opt, Opts) -> [Opt|Opts]. -spec all_zero_ip([proplists:property()]) -> inet:ip_address(). all_zero_ip(Opts) -> case proplists:get_bool(inet6, Opts) of true -> {0,0,0,0,0,0,0,0}; false -> {0,0,0,0} end. %%%=================================================================== %%% ejabberd_shaper %%%=================================================================== transform_shaper(Opts) -> lists:foldl(fun transform_shaper/2, [], Opts). transform_shaper({shaper, Name, {maxrate, N}}, Opts) -> [{shaper, [{Name, N}]} | Opts]; transform_shaper({shaper, Name, none}, Opts) -> [{shaper, [{Name, none}]} | Opts]; transform_shaper({shaper, List}, Opts) when is_list(List) -> R = lists:map( fun({Name, Args}) when is_list(Args) -> MaxRate = proplists:get_value(rate, Args, 1000), BurstSize = proplists:get_value(burst_size, Args, MaxRate), {Name, MaxRate, BurstSize}; ({Name, Val}) -> {Name, Val, Val} end, List), [{shaper, R} | Opts]; transform_shaper(Opt, Opts) -> [Opt | Opts]. %%%=================================================================== %%% acl %%%=================================================================== transform_acl(Opts) -> Opts1 = lists:foldl(fun transform_acl/2, [], Opts), {ACLOpts, Opts2} = lists:mapfoldl( fun({acl, Os}, Acc) -> {Os, Acc}; (O, Acc) -> {[], [O|Acc]} end, [], Opts1), {AccessOpts, Opts3} = lists:mapfoldl( fun({access, Os}, Acc) -> {Os, Acc}; (O, Acc) -> {[], [O|Acc]} end, [], Opts2), {NewAccessOpts, Opts4} = lists:mapfoldl( fun({access_rules, Os}, Acc) -> {Os, Acc}; (O, Acc) -> {[], [O|Acc]} end, [], Opts3), {ShaperOpts, Opts5} = lists:mapfoldl( fun({shaper_rules, Os}, Acc) -> {Os, Acc}; (O, Acc) -> {[], [O|Acc]} end, [], Opts4), ACLOpts1 = collect_options(lists:flatten(ACLOpts)), AccessOpts1 = case collect_options(lists:flatten(AccessOpts)) of [] -> []; L1 -> [{access, L1}] end, ACLOpts2 = case lists:map( fun({ACLName, Os}) -> {ACLName, collect_options(Os)} end, ACLOpts1) of [] -> []; L2 -> [{acl, L2}] end, NewAccessOpts1 = case lists:map( fun({NAName, Os}) -> {NAName, transform_access_rules_config(Os)} end, lists:flatten(NewAccessOpts)) of [] -> []; L3 -> [{access_rules, L3}] end, ShaperOpts1 = case lists:map( fun({SName, Ss}) -> {SName, transform_access_rules_config(Ss)} end, lists:flatten(ShaperOpts)) of [] -> []; L4 -> [{shaper_rules, L4}] end, ACLOpts2 ++ AccessOpts1 ++ NewAccessOpts1 ++ ShaperOpts1 ++ Opts5. transform_acl({acl, Name, Type}, Opts) -> T = case Type of all -> all; none -> none; {user, U} -> {user, [b(U)]}; {user, U, S} -> {user, [[{b(U), b(S)}]]}; {shared_group, G} -> {shared_group, [b(G)]}; {shared_group, G, H} -> {shared_group, [[{b(G), b(H)}]]}; {user_regexp, UR} -> {user_regexp, [b(UR)]}; {user_regexp, UR, S} -> {user_regexp, [[{b(UR), b(S)}]]}; {node_regexp, UR, SR} -> {node_regexp, [[{b(UR), b(SR)}]]}; {user_glob, UR} -> {user_glob, [b(UR)]}; {user_glob, UR, S} -> {user_glob, [[{b(UR), b(S)}]]}; {node_glob, UR, SR} -> {node_glob, [[{b(UR), b(SR)}]]}; {server, S} -> {server, [b(S)]}; {resource, R} -> {resource, [b(R)]}; {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_regexp, R} -> {resource_regexp, [b(R)]} end, [{acl, [{Name, [T]}]}|Opts]; transform_acl({access, Name, Rules}, Opts) -> NewRules = [{ACL, Action} || {Action, ACL} <- Rules], [{access, [{Name, NewRules}]}|Opts]; transform_acl(Opt, Opts) -> [Opt|Opts]. transform_access_rules_config(Config) when is_list(Config) -> lists:map(fun transform_access_rules_config2/1, lists:flatten(Config)); transform_access_rules_config(Config) -> transform_access_rules_config([Config]). transform_access_rules_config2(Type) when is_integer(Type); is_atom(Type) -> {Type, [all]}; transform_access_rules_config2({Type, ACL}) when is_atom(ACL) -> {Type, [{acl, ACL}]}; transform_access_rules_config2({Res, Rules}) when is_list(Rules) -> T = lists:map(fun({Type, Args}) when is_list(Args) -> {Type, hd(lists:flatten(Args))}; (V) -> V end, lists:flatten(Rules)), {Res, T}; transform_access_rules_config2({Res, Rule}) -> {Res, [Rule]}. %%%=================================================================== %%% SQL %%%=================================================================== -define(PGSQL_PORT, 5432). -define(MYSQL_PORT, 3306). transform_sql(Opts) -> lists:foldl(fun transform_sql/2, [], Opts). transform_sql({odbc_server, {Type, Server, Port, DB, User, Pass}}, Opts) -> [{sql_type, Type}, {sql_server, Server}, {sql_port, Port}, {sql_database, DB}, {sql_username, User}, {sql_password, Pass}|Opts]; transform_sql({odbc_server, {mysql, Server, DB, User, Pass}}, Opts) -> transform_sql({odbc_server, {mysql, Server, ?MYSQL_PORT, DB, User, Pass}}, Opts); transform_sql({odbc_server, {pgsql, Server, DB, User, Pass}}, Opts) -> transform_sql({odbc_server, {pgsql, Server, ?PGSQL_PORT, DB, User, Pass}}, Opts); transform_sql({odbc_server, {sqlite, DB}}, Opts) -> [{sql_type, sqlite}, {sql_database, DB}|Opts]; transform_sql({odbc_pool_size, N}, Opts) -> [{sql_pool_size, N}|Opts]; transform_sql(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% modules %%%=================================================================== transform_modules(Opts) -> lists:foldl(fun transform_modules/2, [], Opts). transform_modules({modules, ModOpts}, Opts) -> [{modules, lists:map( fun({Mod, Opts1}) -> {Mod, transform_module(Mod, Opts1)}; (Other) -> Other end, ModOpts)}|Opts]; transform_modules(Opt, Opts) -> [Opt|Opts]. transform_module(mod_disco, Opts) -> lists:map( fun({server_info, Infos}) -> NewInfos = lists:map( fun({Modules, Name, URLs}) -> [[{modules, Modules}, {name, Name}, {urls, URLs}]]; (Opt) -> Opt end, Infos), {server_info, NewInfos}; (Opt) -> Opt end, Opts); transform_module(mod_muc_log, Opts) -> lists:map( fun({top_link, {S1, S2}}) -> {top_link, [{S1, S2}]}; (Opt) -> Opt end, Opts); transform_module(mod_proxy65, Opts) -> lists:map( fun({ip, IP}) when is_tuple(IP) -> {ip, misc:ip_to_list(IP)}; ({hostname, IP}) when is_tuple(IP) -> {hostname, misc:ip_to_list(IP)}; (Opt) -> Opt end, Opts); transform_module(mod_register, Opts) -> lists:flatmap( fun({welcome_message, {Subj, Body}}) -> [{welcome_message, [{subject, Subj}, {body, Body}]}]; (Opt) -> [Opt] end, Opts); transform_module(_Mod, Opts) -> Opts. %%%=================================================================== %%% Host config %%%=================================================================== transform_host_config(Opts) -> Opts1 = lists:foldl(fun transform_host_config/2, [], Opts), {HOpts, Opts2} = lists:mapfoldl( fun({host_config, O}, Os) -> {[O], Os}; (O, Os) -> {[], [O|Os]} end, [], Opts1), {AHOpts, Opts3} = lists:mapfoldl( fun({append_host_config, O}, Os) -> {[O], Os}; (O, Os) -> {[], [O|Os]} end, [], Opts2), HOpts1 = case collect_options(lists:flatten(HOpts)) of [] -> []; HOs -> [{host_config, [{H, transform(O)} || {H, O} <- HOs]}] end, AHOpts1 = case collect_options(lists:flatten(AHOpts)) of [] -> []; AHOs -> [{append_host_config, [{H, transform(O)} || {H, O} <- AHOs]}] end, HOpts1 ++ AHOpts1 ++ Opts3. transform_host_config({host_config, Host, HOpts}, Opts) -> {AddOpts, HOpts1} = lists:mapfoldl( fun({{add, Opt}, Val}, Os) -> {[{Opt, Val}], Os}; (O, Os) -> {[], [O|Os]} end, [], HOpts), [{append_host_config, [{Host, lists:flatten(AddOpts)}]}, {host_config, [{Host, HOpts1}]}|Opts]; transform_host_config(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% Top-level options %%%=================================================================== transform_globals(Opts) -> lists:foldl(fun transform_globals/2, [], Opts). transform_globals(Opt, Opts) when Opt == override_global; Opt == override_local; Opt == override_acls -> ?WARNING_MSG("Option '~ts' has no effect anymore", [Opt]), Opts; transform_globals({node_start, _}, Opts) -> ?WARNING_MSG("Option 'node_start' has no effect anymore", []), Opts; transform_globals({iqdisc, {queues, N}}, Opts) -> [{iqdisc, N}|Opts]; transform_globals({define_macro, Macro, Val}, Opts) -> [{define_macro, [{Macro, Val}]}|Opts]; transform_globals(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% Certfiles %%%=================================================================== transform_certfiles(Opts) -> lists:foldl(fun transform_certfiles/2, [], Opts). transform_certfiles({domain_certfile, Domain, CertFile}, Opts) -> [{host_config, [{Domain, [{domain_certfile, CertFile}]}]}|Opts]; transform_certfiles(Opt, Opts) -> [Opt|Opts]. %%%=================================================================== %%% Consult file %%%=================================================================== consult(File) -> case file:consult(File) of {ok, Terms} -> include_config_files(Terms); Err -> Err end. include_config_files(Terms) -> include_config_files(Terms, []). include_config_files([], Res) -> {ok, Res}; include_config_files([{include_config_file, Filename} | Terms], Res) -> include_config_files([{include_config_file, Filename, []} | Terms], Res); include_config_files([{include_config_file, Filename, Options} | Terms], Res) -> case consult(Filename) of {ok, Included_terms} -> Disallow = proplists:get_value(disallow, Options, []), Included_terms2 = delete_disallowed(Disallow, Included_terms), Allow_only = proplists:get_value(allow_only, Options, all), Included_terms3 = keep_only_allowed(Allow_only, Included_terms2), include_config_files(Terms, Res ++ Included_terms3); Err -> Err end; include_config_files([Term | Terms], Res) -> include_config_files(Terms, Res ++ [Term]). delete_disallowed(Disallowed, Terms) -> lists:foldl( fun(Dis, Ldis) -> delete_disallowed2(Dis, Ldis) end, Terms, Disallowed). delete_disallowed2(Disallowed, [H|T]) -> case element(1, H) of Disallowed -> delete_disallowed2(Disallowed, T); _ -> [H|delete_disallowed2(Disallowed, T)] end; delete_disallowed2(_, []) -> []. keep_only_allowed(all, Terms) -> Terms; keep_only_allowed(Allowed, Terms) -> {As, _NAs} = lists:partition( fun(Term) -> lists:member(element(1, Term), Allowed) end, Terms), As. %%%=================================================================== %%% Aux functions %%%=================================================================== strings_to_binary([]) -> []; strings_to_binary(L) when is_list(L) -> case is_string(L) of true -> list_to_binary(L); false -> strings_to_binary1(L) end; strings_to_binary({A, B, C, D}) when is_integer(A), is_integer(B), is_integer(C), is_integer(D) -> {A, B, C ,D}; strings_to_binary(T) when is_tuple(T) -> list_to_tuple(strings_to_binary1(tuple_to_list(T))); strings_to_binary(X) -> X. strings_to_binary1([El|L]) -> [strings_to_binary(El)|strings_to_binary1(L)]; strings_to_binary1([]) -> []; strings_to_binary1(T) -> T. is_string([C|T]) when (C >= 0) and (C =< 255) -> is_string(T); is_string([]) -> true; is_string(_) -> false. b(S) -> iolist_to_binary(S). ejabberd-21.12/src/mod_ping.erl0000644000232200023220000002726414154362354016734 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_ping.erl %%% Author : Brian Cully %%% Purpose : Support XEP-0199 XMPP Ping and periodic keepalives %%% Created : 11 Jul 2009 by Brian Cully %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mod_ping). -author('bjc@kublai.com'). -protocol({xep, 199, '2.0'}). -behaviour(gen_mod). -behaviour(gen_server). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). %% API -export([start_ping/2, stop_ping/2]). %% gen_mod callbacks -export([start/2, stop/1, reload/3]). %% gen_server callbacks -export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]). -export([iq_ping/1, user_online/3, user_offline/3, mod_doc/0, user_send/1, mod_opt_type/1, mod_options/1, depends/2]). -record(state, {host :: binary(), send_pings :: boolean(), ping_interval :: pos_integer(), ping_ack_timeout :: undefined | non_neg_integer(), timeout_action :: none | kill, timers :: timers()}). -type timers() :: #{ljid() => reference()}. %%==================================================================== %% API %%==================================================================== -spec start_ping(binary(), jid()) -> ok. start_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {start_ping, JID}). -spec stop_ping(binary(), jid()) -> ok. stop_ping(Host, JID) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {stop_ping, JID}). %%==================================================================== %% gen_mod callbacks %%==================================================================== start(Host, Opts) -> gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> gen_mod:stop_child(?MODULE, Host). reload(Host, NewOpts, OldOpts) -> Proc = gen_mod:get_module_proc(Host, ?MODULE), gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). %%==================================================================== %% gen_server callbacks %%==================================================================== init([Host|_]) -> process_flag(trap_exit, true), Opts = gen_mod:get_module_opts(Host, ?MODULE), State = init_state(Host, Opts), register_iq_handlers(Host), case State#state.send_pings of true -> register_hooks(Host); false -> ok end, {ok, State}. terminate(_Reason, #state{host = Host}) -> unregister_hooks(Host), unregister_iq_handlers(Host). handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast({reload, Host, NewOpts, _OldOpts}, #state{timers = Timers} = OldState) -> NewState = init_state(Host, NewOpts), case {NewState#state.send_pings, OldState#state.send_pings} of {true, false} -> register_hooks(Host); {false, true} -> unregister_hooks(Host); _ -> ok end, {noreply, NewState#state{timers = Timers}}; handle_cast({start_ping, JID}, State) -> Timers = add_timer(JID, State#state.ping_interval, State#state.timers), {noreply, State#state{timers = Timers}}; handle_cast({stop_ping, JID}, State) -> Timers = del_timer(JID, State#state.timers), {noreply, State#state{timers = Timers}}; handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info({iq_reply, #iq{}, _JID}, State) -> {noreply, State}; handle_info({iq_reply, timeout, JID}, State) -> ejabberd_hooks:run(user_ping_timeout, State#state.host, [JID]), Timers = case State#state.timeout_action of kill -> #jid{user = User, server = Server, resource = Resource} = JID, case ejabberd_sm:get_session_pid(User, Server, Resource) of Pid when is_pid(Pid) -> ejabberd_c2s:close(Pid, ping_timeout); _ -> ok end, del_timer(JID, State#state.timers); _ -> State#state.timers end, {noreply, State#state{timers = Timers}}; handle_info({timeout, _TRef, {ping, JID}}, State) -> Host = State#state.host, From = jid:make(Host), IQ = #iq{from = From, to = JID, type = get, sub_els = [#ping{}]}, ejabberd_router:route_iq(IQ, JID, gen_mod:get_module_proc(Host, ?MODULE), State#state.ping_ack_timeout), Timers = add_timer(JID, State#state.ping_interval, State#state.timers), {noreply, State#state{timers = Timers}}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %% Hook callbacks %%==================================================================== -spec iq_ping(iq()) -> iq(). iq_ping(#iq{type = get, sub_els = [#ping{}]} = IQ) -> xmpp:make_iq_result(IQ); iq_ping(#iq{lang = Lang} = IQ) -> Txt = ?T("Ping query is incorrect"), xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). -spec user_online(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_online(_SID, JID, _Info) -> start_ping(JID#jid.lserver, JID). -spec user_offline(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. user_offline(_SID, JID, _Info) -> case ejabberd_sm:get_session_pid(JID#jid.luser, JID#jid.lserver, JID#jid.lresource) of none -> stop_ping(JID#jid.lserver, JID); _ -> ok end. -spec user_send({stanza(), ejabberd_c2s:state()}) -> {stanza(), ejabberd_c2s:state()}. user_send({Packet, #{jid := JID} = C2SState}) -> start_ping(JID#jid.lserver, JID), {Packet, C2SState}. %%==================================================================== %% Internal functions %%==================================================================== init_state(Host, Opts) -> SendPings = mod_ping_opt:send_pings(Opts), PingInterval = mod_ping_opt:ping_interval(Opts), PingAckTimeout = mod_ping_opt:ping_ack_timeout(Opts), TimeoutAction = mod_ping_opt:timeout_action(Opts), #state{host = Host, send_pings = SendPings, ping_interval = PingInterval, timeout_action = TimeoutAction, ping_ack_timeout = PingAckTimeout, timers = #{}}. register_hooks(Host) -> ejabberd_hooks:add(sm_register_connection_hook, Host, ?MODULE, user_online, 100), ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, user_offline, 100), ejabberd_hooks:add(user_send_packet, Host, ?MODULE, user_send, 100). unregister_hooks(Host) -> ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, user_offline, 100), ejabberd_hooks:delete(sm_register_connection_hook, Host, ?MODULE, user_online, 100), ejabberd_hooks:delete(user_send_packet, Host, ?MODULE, user_send, 100). register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PING, ?MODULE, iq_ping), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_PING, ?MODULE, iq_ping). unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_PING), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PING). -spec add_timer(jid(), pos_integer(), timers()) -> timers(). add_timer(JID, Interval, Timers) -> LJID = jid:tolower(JID), NewTimers = case maps:find(LJID, Timers) of {ok, OldTRef} -> misc:cancel_timer(OldTRef), maps:remove(LJID, Timers); _ -> Timers end, TRef = erlang:start_timer(Interval, self(), {ping, JID}), maps:put(LJID, TRef, NewTimers). -spec del_timer(jid(), timers()) -> timers(). del_timer(JID, Timers) -> LJID = jid:tolower(JID), case maps:find(LJID, Timers) of {ok, TRef} -> misc:cancel_timer(TRef), maps:remove(LJID, Timers); _ -> Timers end. depends(_Host, _Opts) -> []. mod_opt_type(ping_interval) -> econf:timeout(second); mod_opt_type(ping_ack_timeout) -> econf:timeout(second); mod_opt_type(send_pings) -> econf:bool(); mod_opt_type(timeout_action) -> econf:enum([none, kill]). mod_options(_Host) -> [{ping_interval, timer:minutes(1)}, {ping_ack_timeout, undefined}, {send_pings, false}, {timeout_action, none}]. mod_doc() -> #{desc => ?T("This module implements support for " "https://xmpp.org/extensions/xep-0199.html" "[XEP-0199: XMPP Ping] and periodic keepalives. " "When this module is enabled ejabberd responds " "correctly to ping requests, as defined by the protocol."), opts => [{ping_interval, #{value => "timeout()", desc => ?T("How often to send pings to connected clients, " "if option 'send_pings' is set to 'true'. If a client " "connection does not send or receive any stanza " "within this interval, a ping request is sent to " "the client. The default value is '1' minute.")}}, {ping_ack_timeout, #{value => "timeout()", desc => ?T("How long to wait before deeming that a client " "has not answered a given server ping request. " "The default value is 'undefined'.")}}, {send_pings, #{value => "true | false", desc => ?T("If this option is set to 'true', the server " "sends pings to connected clients that are not " "active in a given interval defined in 'ping_interval' " "option. This is useful to keep client connections " "alive or checking availability. " "The default value is 'false'.")}}, {timeout_action, #{value => "none | kill", desc => ?T("What to do when a client does not answer to a " "server ping request in less than period defined " "in 'ping_ack_timeout' option: " "'kill' means destroying the underlying connection, " "'none' means to do nothing. NOTE: when _`mod_stream_mgmt`_ " "is loaded and stream management is enabled by " "a client, killing the client connection doesn't mean " "killing the client session - the session will be kept " "alive in order to give the client a chance to resume it. " "The default value is 'none'.")}}], example => ["modules:", " ...", " mod_ping:", " send_pings: true", " ping_interval: 4 min", " timeout_action: kill", " ..."]}. ejabberd-21.12/src/mod_metrics_opt.erl0000644000232200023220000000103114154362354020307 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_metrics_opt). -export([ip/1]). -export([port/1]). -spec ip(gen_mod:opts() | global | binary()) -> {127,0,0,1} | inet:ip4_address(). ip(Opts) when is_map(Opts) -> gen_mod:get_opt(ip, Opts); ip(Host) -> gen_mod:get_module_opt(Host, mod_metrics, ip). -spec port(gen_mod:opts() | global | binary()) -> 1..1114111. port(Opts) when is_map(Opts) -> gen_mod:get_opt(port, Opts); port(Host) -> gen_mod:get_module_opt(Host, mod_metrics, port). ejabberd-21.12/src/ejabberd_oauth_mnesia.erl0000644000232200023220000000523114154362354021420 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_oauth_mnesia.erl %%% Author : Alexey Shchepin %%% Purpose : OAUTH2 mnesia backend %%% Created : 20 Jul 2016 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%------------------------------------------------------------------- -module(ejabberd_oauth_mnesia). -behaviour(ejabberd_oauth). -export([init/0, store/1, lookup/1, clean/1, lookup_client/1, store_client/1, remove_client/1, use_cache/0]). -include("ejabberd_oauth.hrl"). init() -> ejabberd_mnesia:create(?MODULE, oauth_token, [{disc_only_copies, [node()]}, {attributes, record_info(fields, oauth_token)}]), ejabberd_mnesia:create(?MODULE, oauth_client, [{disc_copies, [node()]}, {attributes, record_info(fields, oauth_client)}]), ok. use_cache() -> case mnesia:table_info(oauth_token, storage_type) of disc_only_copies -> ejabberd_option:oauth_use_cache(); _ -> false end. store(R) -> mnesia:dirty_write(R). lookup(Token) -> case catch mnesia:dirty_read(oauth_token, Token) of [R] -> {ok, R}; _ -> error end. clean(TS) -> F = fun() -> Ts = mnesia:select( oauth_token, [{#oauth_token{expire = '$1', _ = '_'}, [{'<', '$1', TS}], ['$_']}]), lists:foreach(fun mnesia:delete_object/1, Ts) end, mnesia:async_dirty(F). lookup_client(ClientID) -> case catch mnesia:dirty_read(oauth_client, ClientID) of [R] -> {ok, R}; _ -> error end. remove_client(ClientID) -> mnesia:dirty_delete(oauth_client, ClientID). store_client(R) -> mnesia:dirty_write(R). ejabberd-21.12/src/ejabberd_auth_anonymous.erl0000644000232200023220000001353614154362354022024 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : ejabberd_auth_anonymous.erl %%% Author : Mickael Remond %%% Purpose : Anonymous feature support in ejabberd %%% Created : 17 Feb 2006 by Mickael Remond %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_auth_anonymous). -behaviour(ejabberd_auth). -author('mickael.remond@process-one.net'). -export([start/1, stop/1, use_cache/1, allow_anonymous/1, is_sasl_anonymous_enabled/1, is_login_anonymous_enabled/1, anonymous_user_exist/2, allow_multiple_connections/1, register_connection/3, unregister_connection/3 ]). -export([login/2, check_password/4, user_exists/2, get_users/2, count_users/2, store_type/1, plain_password_required/1]). -include("logger.hrl"). -include_lib("xmpp/include/jid.hrl"). start(Host) -> ejabberd_hooks:add(sm_register_connection_hook, Host, ?MODULE, register_connection, 100), ejabberd_hooks:add(sm_remove_connection_hook, Host, ?MODULE, unregister_connection, 100), ok. stop(Host) -> ejabberd_hooks:delete(sm_register_connection_hook, Host, ?MODULE, register_connection, 100), ejabberd_hooks:delete(sm_remove_connection_hook, Host, ?MODULE, unregister_connection, 100). use_cache(_) -> false. %% Return true if anonymous is allowed for host or false otherwise allow_anonymous(Host) -> lists:member(?MODULE, ejabberd_auth:auth_modules(Host)). %% Return true if anonymous mode is enabled and if anonymous protocol is SASL %% anonymous protocol can be: sasl_anon|login_anon|both is_sasl_anonymous_enabled(Host) -> case allow_anonymous(Host) of false -> false; true -> case anonymous_protocol(Host) of sasl_anon -> true; both -> true; _Other -> false end end. %% Return true if anonymous login is enabled on the server %% anonymous login can be use using standard authentication method (i.e. with %% clients that do not support anonymous login) is_login_anonymous_enabled(Host) -> case allow_anonymous(Host) of false -> false; true -> case anonymous_protocol(Host) of login_anon -> true; both -> true; _Other -> false end end. %% Return the anonymous protocol to use: sasl_anon|login_anon|both %% defaults to login_anon anonymous_protocol(Host) -> ejabberd_option:anonymous_protocol(Host). %% Return true if multiple connections have been allowed in the config file %% defaults to false allow_multiple_connections(Host) -> ejabberd_option:allow_multiple_connections(Host). anonymous_user_exist(User, Server) -> lists:any( fun({_LResource, Info}) -> proplists:get_value(auth_module, Info) == ?MODULE end, ejabberd_sm:get_user_info(User, Server)). %% Register connection -spec register_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> ok. register_connection(_SID, #jid{luser = LUser, lserver = LServer, lresource = LResource}, Info) -> case proplists:get_value(auth_module, Info) of ?MODULE -> % Register user only if we are first resource case ejabberd_sm:get_user_resources(LUser, LServer) of [LResource] -> ejabberd_hooks:run(register_user, LServer, [LUser, LServer]); _ -> ok end; _ -> ok end. %% Remove an anonymous user from the anonymous users table -spec unregister_connection(ejabberd_sm:sid(), jid(), ejabberd_sm:info()) -> any(). unregister_connection(_SID, #jid{luser = LUser, lserver = LServer}, Info) -> case proplists:get_value(auth_module, Info) of ?MODULE -> % Remove user data only if there is no more resources around case ejabberd_sm:get_user_resources(LUser, LServer) of [] -> ejabberd_hooks:run(remove_user, LServer, [LUser, LServer]); _ -> ok end; _ -> ok end. %% --------------------------------- %% Specific anonymous auth functions %% --------------------------------- check_password(User, _AuthzId, Server, _Password) -> {nocache, case ejabberd_auth:user_exists_in_other_modules(?MODULE, User, Server) of %% If user exists in other module, reject anonnymous authentication true -> false; %% If we are not sure whether the user exists in other module, reject anon auth maybe -> false; false -> login(User, Server) end}. login(User, Server) -> case is_login_anonymous_enabled(Server) of false -> false; true -> case anonymous_user_exist(User, Server) of %% Reject the login if an anonymous user with the same login %% is already logged and if multiple login has not been enable %% in the config file. true -> allow_multiple_connections(Server); %% Accept login and add user to the anonymous table false -> true end end. get_users(Server, _) -> [{U, S} || {U, S, _R} <- ejabberd_sm:get_vh_session_list(Server)]. count_users(Server, Opts) -> length(get_users(Server, Opts)). user_exists(User, Server) -> {nocache, anonymous_user_exist(User, Server)}. plain_password_required(_) -> false. store_type(_) -> external. ejabberd-21.12/src/ejabberd_oauth.erl0000644000232200023220000007535714154362354020104 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% File : ejabberd_oauth.erl %%% Author : Alexey Shchepin %%% Purpose : OAUTH2 support %%% Created : 20 Mar 2015 by Alexey Shchepin %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA %%% 02111-1307 USA %%% %%%------------------------------------------------------------------- -module(ejabberd_oauth). -behaviour(gen_server). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([start_link/0, get_client_identity/2, verify_redirection_uri/3, authenticate_user/2, authenticate_client/2, associate_access_code/3, associate_access_token/3, associate_refresh_token/3, check_token/1, check_token/4, check_token/2, scope_in_scope_list/2, process/2, config_reloaded/0, verify_resowner_scope/3]). -export([get_commands_spec/0, oauth_issue_token/3, oauth_list_tokens/0, oauth_revoke_token/1, oauth_add_client_password/3, oauth_add_client_implicit/3, oauth_remove_client/1]). -include_lib("xmpp/include/xmpp.hrl"). -include("logger.hrl"). -include("ejabberd_http.hrl"). -include("ejabberd_web_admin.hrl"). -include("ejabberd_oauth.hrl"). -include("ejabberd_commands.hrl"). -include("translate.hrl"). -callback init() -> any(). -callback store(#oauth_token{}) -> ok | {error, any()}. -callback lookup(binary()) -> {ok, #oauth_token{}} | error. -callback clean(non_neg_integer()) -> any(). -record(oauth_ctx, { password :: binary() | admin_generated, client :: #oauth_client{} | undefined }). %% There are two ways to obtain an oauth token: %% * Using the web form/api results in the token being generated in behalf of the user providing the user/pass %% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin %% (as it has access to ejabberd command line). get_commands_spec() -> [ #ejabberd_commands{name = oauth_issue_token, tags = [oauth], desc = "Issue an oauth token for the given jid", module = ?MODULE, function = oauth_issue_token, args = [{jid, string},{ttl, integer}, {scopes, string}], policy = restricted, args_example = ["user@server.com", 3600, "connected_users_number;muc_online_rooms"], args_desc = ["Jid for which issue token", "Time to live of generated token in seconds", "List of scopes to allow, separated by ';'"], result = {result, {tuple, [{token, string}, {scopes, string}, {expires_in, string}]}} }, #ejabberd_commands{name = oauth_list_tokens, tags = [oauth], desc = "List oauth tokens, user, scope, and seconds to expire (only Mnesia)", longdesc = "List oauth tokens, their user and scope, and how many seconds remain until expirity", module = ?MODULE, function = oauth_list_tokens, args = [], policy = restricted, result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}} }, #ejabberd_commands{name = oauth_revoke_token, tags = [oauth], desc = "Revoke authorization for a token (only Mnesia)", module = ?MODULE, function = oauth_revoke_token, args = [{token, string}], policy = restricted, result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}, result_desc = "List of remaining tokens" }, #ejabberd_commands{name = oauth_add_client_password, tags = [oauth], desc = "Add OAUTH client_id with password grant type", module = ?MODULE, function = oauth_add_client_password, args = [{client_id, binary}, {client_name, binary}, {secret, binary}], policy = restricted, result = {res, restuple} }, #ejabberd_commands{name = oauth_add_client_implicit, tags = [oauth], desc = "Add OAUTH client_id with implicit grant type", module = ?MODULE, function = oauth_add_client_implicit, args = [{client_id, binary}, {client_name, binary}, {redirect_uri, binary}], policy = restricted, result = {res, restuple} }, #ejabberd_commands{name = oauth_remove_client, tags = [oauth], desc = "Remove OAUTH client_id", module = ?MODULE, function = oauth_remove_client, args = [{client_id, binary}], policy = restricted, result = {res, restuple} } ]. oauth_issue_token(Jid, TTLSeconds, ScopesString) -> Scopes = [list_to_binary(Scope) || Scope <- string:tokens(ScopesString, ";")], try jid:decode(list_to_binary(Jid)) of #jid{luser =Username, lserver = Server} -> Ctx1 = #oauth_ctx{password = admin_generated}, case oauth2:authorize_password({Username, Server}, Scopes, Ctx1) of {ok, {_Ctx,Authorization}} -> {ok, {_AppCtx2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, TTLSeconds}]), {ok, AccessToken} = oauth2_response:access_token(Response), {ok, VerifiedScope} = oauth2_response:scope(Response), {AccessToken, VerifiedScope, integer_to_list(TTLSeconds) ++ " seconds"}; {error, Error} -> {error, Error} end catch _:{bad_jid, _} -> {error, "Invalid JID: " ++ Jid} end. oauth_list_tokens() -> Tokens = mnesia:dirty_match_object(#oauth_token{_ = '_'}), {MegaSecs, Secs, _MiniSecs} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, [{Token, jid:encode(jid:make(U,S)), Scope, integer_to_list(Expires - TS) ++ " seconds"} || #oauth_token{token=Token, scope=Scope, us= {U,S},expire=Expires} <- Tokens]. oauth_revoke_token(Token) -> ok = mnesia:dirty_delete(oauth_token, list_to_binary(Token)), oauth_list_tokens(). oauth_add_client_password(ClientID, ClientName, Secret) -> DBMod = get_db_backend(), DBMod:store_client(#oauth_client{client_id = ClientID, client_name = ClientName, grant_type = password, options = [{secret, Secret}]}), {ok, []}. oauth_add_client_implicit(ClientID, ClientName, RedirectURI) -> DBMod = get_db_backend(), DBMod:store_client(#oauth_client{client_id = ClientID, client_name = ClientName, grant_type = implicit, options = [{redirect_uri, RedirectURI}]}), {ok, []}. oauth_remove_client(Client) -> DBMod = get_db_backend(), DBMod:remove_client(Client), {ok, []}. config_reloaded() -> DBMod = get_db_backend(), case init_cache(DBMod) of true -> ets_cache:setopts(oauth_cache, cache_opts()); false -> ok end. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> DBMod = get_db_backend(), DBMod:init(), init_cache(DBMod), Expire = expire(), application:set_env(oauth2, backend, ejabberd_oauth), application:set_env(oauth2, expiry_time, Expire div 1000), application:start(oauth2), ejabberd_commands:register_commands(get_commands_spec()), ejabberd_hooks:add(config_reloaded, ?MODULE, config_reloaded, 50), erlang:send_after(expire(), self(), clean), {ok, ok}. handle_call(Request, From, State) -> ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. handle_cast(Msg, State) -> ?WARNING_MSG("Unexpected cast: ~p", [Msg]), {noreply, State}. handle_info(clean, State) -> {MegaSecs, Secs, MiniSecs} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, DBMod = get_db_backend(), DBMod:clean(TS), erlang:send_after(trunc(expire() * (1 + MiniSecs / 1000000)), self(), clean), {noreply, State}; handle_info(Info, State) -> ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. terminate(_Reason, _State) -> ejabberd_hooks:delete(config_reloaded, ?MODULE, config_reloaded, 50). code_change(_OldVsn, State, _Extra) -> {ok, State}. get_client_identity({client, ClientID}, Ctx) -> {ok, {Ctx, {client, ClientID}}}. verify_redirection_uri(_ClientID, RedirectURI, Ctx) -> case Ctx of #oauth_ctx{client = #oauth_client{grant_type = implicit} = Client} -> case get_redirect_uri(Client) of RedirectURI -> {ok, Ctx}; _ -> {error, invalid_uri} end; #oauth_ctx{client = #oauth_client{}} -> {error, invalid_client}; _ -> {ok, Ctx} end. authenticate_user({User, Server}, Ctx) -> case jid:make(User, Server) of #jid{} = JID -> Access = ejabberd_option:oauth_access(JID#jid.lserver), case acl:match_rule(JID#jid.lserver, Access, JID) of allow -> case Ctx of #oauth_ctx{password = admin_generated} -> {ok, {Ctx, {user, User, Server}}}; #oauth_ctx{password = Password} when is_binary(Password) -> case ejabberd_auth:check_password(User, <<"">>, Server, Password) of true -> {ok, {Ctx, {user, User, Server}}}; false -> {error, badpass} end end; deny -> {error, badpass} end; error -> {error, badpass} end. authenticate_client(ClientID, Ctx) -> case ejabberd_option:oauth_client_id_check() of allow -> {ok, {Ctx, {client, ClientID}}}; deny -> {error, not_allowed}; db -> DBMod = get_db_backend(), case DBMod:lookup_client(ClientID) of {ok, #oauth_client{} = Client} -> {ok, {Ctx#oauth_ctx{client = Client}, {client, ClientID}}}; _ -> {error, not_allowed} end end. -spec verify_resowner_scope({user, binary(), binary()}, [binary()], any()) -> {ok, any(), [binary()]} | {error, any()}. verify_resowner_scope({user, _User, _Server}, Scope, Ctx) -> Cmds = [atom_to_binary(Name, utf8) || {Name, _, _} <- ejabberd_commands:list_commands()], AllowedScopes = [<<"ejabberd:user">>, <<"ejabberd:admin">>, <<"sasl_auth">>] ++ Cmds, case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), oauth2_priv_set:new(AllowedScopes)) of true -> {ok, {Ctx, Scope}}; false -> {error, badscope} end; verify_resowner_scope(_, _, _) -> {error, badscope}. %% This is callback for oauth tokens generated through the command line. Only open and admin commands are %% made available. %verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) -> % RegisteredScope = dict:fetch_keys(get_cmd_scopes()), % case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope), % oauth2_priv_set:new(RegisteredScope)) of % true -> % {ok, {Ctx, Scope}}; % false -> % {error, badscope} % end. -spec seconds_since_epoch(integer()) -> non_neg_integer(). seconds_since_epoch(Diff) -> {Mega, Secs, _} = os:timestamp(), Mega * 1000000 + Secs + Diff. associate_access_code(_AccessCode, _Context, AppContext) -> %put(?ACCESS_CODE_TABLE, AccessCode, Context), {ok, AppContext}. associate_access_token(AccessToken, Context, AppContext) -> {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), Expire = case proplists:get_value(expiry_time, AppContext, undefined) of undefined -> proplists:get_value(<<"expiry_time">>, Context, 0); ExpiresIn -> %% There is no clean way in oauth2 lib to actually override the TTL of the generated token. %% It always pass the global configured value. Here we use the app context to pass the per-case %% ttl if we want to override it. seconds_since_epoch(ExpiresIn) end, {user, User, Server} = proplists:get_value(<<"resource_owner">>, Context, <<"">>), Scope = proplists:get_value(<<"scope">>, Context, []), R = #oauth_token{ token = AccessToken, us = {jid:nodeprep(User), jid:nodeprep(Server)}, scope = Scope, expire = Expire }, store(R), {ok, AppContext}. associate_refresh_token(_RefreshToken, _Context, AppContext) -> %put(?REFRESH_TOKEN_TABLE, RefreshToken, Context), {ok, AppContext}. scope_in_scope_list(Scope, ScopeList) -> TokenScopeSet = oauth2_priv_set:new(Scope), lists:any(fun(Scope2) -> oauth2_priv_set:is_member(Scope2, TokenScopeSet) end, ScopeList). -spec check_token(binary()) -> {ok, {binary(), binary()}, [binary()]} | {false, expired | not_found}. check_token(Token) -> case lookup(Token) of {ok, #oauth_token{us = US, scope = TokenScope, expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, if Expire > TS -> {ok, US, TokenScope}; true -> {false, expired} end; _ -> {false, not_found} end. check_token(User, Server, ScopeList, Token) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), case lookup(Token) of {ok, #oauth_token{us = {LUser, LServer}, scope = TokenScope, expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, if Expire > TS -> TokenScopeSet = oauth2_priv_set:new(TokenScope), lists:any(fun(Scope) -> oauth2_priv_set:is_member(Scope, TokenScopeSet) end, ScopeList); true -> {false, expired} end; _ -> {false, not_found} end. check_token(ScopeList, Token) -> case lookup(Token) of {ok, #oauth_token{us = US, scope = TokenScope, expire = Expire}} -> {MegaSecs, Secs, _} = os:timestamp(), TS = 1000000 * MegaSecs + Secs, if Expire > TS -> TokenScopeSet = oauth2_priv_set:new(TokenScope), case lists:any(fun(Scope) -> oauth2_priv_set:is_member(Scope, TokenScopeSet) end, ScopeList) of true -> {ok, user, US}; false -> {false, no_matching_scope} end; true -> {false, expired} end; _ -> {false, not_found} end. store(R) -> DBMod = get_db_backend(), case DBMod:store(R) of ok -> ets_cache:delete(oauth_cache, R#oauth_token.token, ejabberd_cluster:get_nodes()); {error, _} = Err -> Err end. lookup(Token) -> ets_cache:lookup(oauth_cache, Token, fun() -> DBMod = get_db_backend(), DBMod:lookup(Token) end). -spec init_cache(module()) -> boolean(). init_cache(DBMod) -> UseCache = use_cache(DBMod), case UseCache of true -> ets_cache:new(oauth_cache, cache_opts()); false -> ets_cache:delete(oauth_cache) end, UseCache. use_cache(DBMod) -> case erlang:function_exported(DBMod, use_cache, 0) of true -> DBMod:use_cache(); false -> ejabberd_option:oauth_use_cache() end. cache_opts() -> MaxSize = ejabberd_option:oauth_cache_size(), CacheMissed = ejabberd_option:oauth_cache_missed(), LifeTime = ejabberd_option:oauth_cache_life_time(), [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}]. expire() -> ejabberd_option:oauth_expire(). -define(DIV(Class, Els), ?XAE(<<"div">>, [{<<"class">>, Class}], Els)). -define(INPUTID(Type, Name, Value), ?XA(<<"input">>, [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}, {<<"id">>, Name}])). -define(LABEL(ID, Els), ?XAE(<<"label">>, [{<<"for">>, ID}], Els)). process(_Handlers, #request{method = 'GET', q = Q, lang = Lang, path = [_, <<"authorization_token">>]}) -> ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), Scope = proplists:get_value(<<"scope">>, Q, <<"">>), State = proplists:get_value(<<"state">>, Q, <<"">>), Form = ?XAE(<<"form">>, [{<<"action">>, <<"authorization_token">>}, {<<"method">>, <<"post">>}], [?LABEL(<<"username">>, [?CT(?T("User (jid)")), ?C(<<": ">>)]), ?INPUTID(<<"email">>, <<"username">>, <<"">>), ?BR, ?LABEL(<<"password">>, [?CT(?T("Password")), ?C(<<": ">>)]), ?INPUTID(<<"password">>, <<"password">>, <<"">>), ?INPUT(<<"hidden">>, <<"response_type">>, ResponseType), ?INPUT(<<"hidden">>, <<"client_id">>, ClientId), ?INPUT(<<"hidden">>, <<"redirect_uri">>, RedirectURI), ?INPUT(<<"hidden">>, <<"scope">>, Scope), ?INPUT(<<"hidden">>, <<"state">>, State), ?BR, ?LABEL(<<"ttl">>, [?CT(?T("Token TTL")), ?C(<<": ">>)]), ?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}], [ ?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>), ?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>), ?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>), ?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>), ?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]), ?BR, ?INPUTT(<<"submit">>, <<"">>, ?T("Accept")) ]), Top = ?DIV(<<"section">>, [?DIV(<<"block">>, [?A(<<"https://www.ejabberd.im">>, [?XA(<<"img">>, [{<<"height">>, <<"32">>}, {<<"src">>, logo()}])] )])]), Middle = ?DIV(<<"white section">>, [?DIV(<<"block">>, [?XC(<<"h1">>, <<"Authorization request">>), ?XE(<<"p">>, [?C(<<"Application ">>), ?XC(<<"em">>, ClientId), ?C(<<" wants to access scope ">>), ?XC(<<"em">>, Scope)]), Form ])]), Bottom = ?DIV(<<"section">>, [?DIV(<<"block">>, [?XAC(<<"a">>, [{<<"href">>, <<"https://www.ejabberd.im">>}, {<<"title">>, <<"ejabberd XMPP server">>}], <<"ejabberd">>), ?C(<<" is maintained by ">>), ?XAC(<<"a">>, [{<<"href">>, <<"https://www.process-one.net">>}, {<<"title">>, <<"ProcessOne - Leader in Instant Messaging and Push Solutions">>}], <<"ProcessOne">>) ])]), Body = ?DIV(<<"container">>, [Top, Middle, Bottom]), ejabberd_web:make_xhtml(web_head(), [Body]); process(_Handlers, #request{method = 'POST', q = Q, lang = _Lang, path = [_, <<"authorization_token">>]}) -> _ResponseType = proplists:get_value(<<"response_type">>, Q, <<"">>), ClientId = proplists:get_value(<<"client_id">>, Q, <<"">>), RedirectURI = proplists:get_value(<<"redirect_uri">>, Q, <<"">>), SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), #jid{user = Username, server = Server} = jid:decode(StringJID), Password = proplists:get_value(<<"password">>, Q, <<"">>), State = proplists:get_value(<<"state">>, Q, <<"">>), Scope = str:tokens(SScope, <<" ">>), TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), ExpiresIn = case TTL of <<>> -> undefined; _ -> binary_to_integer(TTL) end, case oauth2:authorize_password({Username, Server}, ClientId, RedirectURI, Scope, #oauth_ctx{password = Password}) of {ok, {_AppContext, Authorization}} -> {ok, {_AppContext2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), {ok, AccessToken} = oauth2_response:access_token(Response), {ok, Type} = oauth2_response:token_type(Response), %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have %%per-case expirity time. Expires = case ExpiresIn of undefined -> {ok, Ex} = oauth2_response:expires_in(Response), Ex; _ -> ExpiresIn end, {ok, VerifiedScope} = oauth2_response:scope(Response), %oauth2_wrq:redirected_access_token_response(ReqData, % RedirectURI, % AccessToken, % Type, % Expires, % VerifiedScope, % State, % Context); {302, [{<<"Location">>, <>))/binary, "&state=", State/binary>> }], ejabberd_web:make_xhtml([?XC(<<"h1">>, <<"302 Found">>)])}; {error, Error} when is_atom(Error) -> %oauth2_wrq:redirected_error_response( % ReqData, RedirectURI, Error, State, Context) {302, [{<<"Location">>, <>, <<"302 Found">>)])} end; process(_Handlers, #request{method = 'POST', q = Q, lang = _Lang, auth = HTTPAuth, path = [_, <<"token">>]}) -> Access = case ejabberd_option:oauth_client_id_check() of allow -> case proplists:get_value(<<"grant_type">>, Q, <<"">>) of <<"password">> -> password; _ -> unsupported_grant_type end; deny -> deny; db -> {ClientID, Secret} = case HTTPAuth of {ClientID1, Secret1} -> {ClientID1, Secret1}; _ -> ClientID1 = proplists:get_value( <<"client_id">>, Q, <<"">>), Secret1 = proplists:get_value( <<"client_secret">>, Q, <<"">>), {ClientID1, Secret1} end, DBMod = get_db_backend(), case DBMod:lookup_client(ClientID) of {ok, #oauth_client{grant_type = password} = Client} -> case get_client_secret(Client) of Secret -> case proplists:get_value(<<"grant_type">>, Q, <<"">>) of <<"password">> when Client#oauth_client.grant_type == password -> password; _ -> unsupported_grant_type end; _ -> deny end; _ -> deny end end, case Access of password -> SScope = proplists:get_value(<<"scope">>, Q, <<"">>), StringJID = proplists:get_value(<<"username">>, Q, <<"">>), #jid{user = Username, server = Server} = jid:decode(StringJID), Password = proplists:get_value(<<"password">>, Q, <<"">>), Scope = str:tokens(SScope, <<" ">>), TTL = proplists:get_value(<<"ttl">>, Q, <<"">>), ExpiresIn = case TTL of <<>> -> undefined; _ -> binary_to_integer(TTL) end, case oauth2:authorize_password({Username, Server}, Scope, #oauth_ctx{password = Password}) of {ok, {_AppContext, Authorization}} -> {ok, {_AppContext2, Response}} = oauth2:issue_token(Authorization, [{expiry_time, ExpiresIn} || ExpiresIn /= undefined ]), {ok, AccessToken} = oauth2_response:access_token(Response), {ok, Type} = oauth2_response:token_type(Response), %%Ugly: workardound to return the correct expirity time, given than oauth2 lib doesn't really have %%per-case expirity time. Expires = case ExpiresIn of undefined -> {ok, Ex} = oauth2_response:expires_in(Response), Ex; _ -> ExpiresIn end, {ok, VerifiedScope} = oauth2_response:scope(Response), json_response(200, {[ {<<"access_token">>, AccessToken}, {<<"token_type">>, Type}, {<<"scope">>, str:join(VerifiedScope, <<" ">>)}, {<<"expires_in">>, Expires}]}); {error, Error} when is_atom(Error) -> json_error(400, <<"invalid_grant">>, Error) end; unsupported_grant_type -> json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type); deny -> ejabberd_web:error(not_allowed) end; process(_Handlers, _Request) -> ejabberd_web:error(not_found). -spec get_db_backend() -> module(). get_db_backend() -> DBType = ejabberd_option:oauth_db_type(), list_to_existing_atom("ejabberd_oauth_" ++ atom_to_list(DBType)). get_client_secret(#oauth_client{grant_type = password, options = Options}) -> proplists:get_value(secret, Options, false). get_redirect_uri(#oauth_client{grant_type = implicit, options = Options}) -> proplists:get_value(redirect_uri, Options, false). %% Headers as per RFC 6749 json_response(Code, Body) -> {Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>}, {<<"Cache-Control">>, <<"no-store">>}, {<<"Pragma">>, <<"no-cache">>}], jiffy:encode(Body)}. %% OAauth error are defined in: %% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2 json_error(Code, Error, Reason) -> Desc = json_error_desc(Reason), Body = {[{<<"error">>, Error}, {<<"error_description">>, Desc}]}, json_response(Code, Body). json_error_desc(access_denied) -> <<"Access denied">>; json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>; json_error_desc(invalid_scope) -> <<"Invalid scope">>. web_head() -> [?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>}, {<<"content">>, <<"IE=edge">>}]), ?XA(<<"meta">>, [{<<"name">>, <<"viewport">>}, {<<"content">>, <<"width=device-width, initial-scale=1">>}]), ?XC(<<"title">>, <<"Authorization request">>), ?XC(<<"style">>, css()) ]. css() -> case misc:read_css("oauth.css") of {ok, Data} -> Data; {error, _} -> <<>> end. logo() -> case misc:read_img("oauth-logo.png") of {ok, Img} -> B64Img = base64:encode(Img), <<"data:image/png;base64,", B64Img/binary>>; {error, _} -> <<>> end. ejabberd-21.12/src/mod_push.erl0000644000232200023220000007022014154362354016744 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% File : mod_push.erl %%% Author : Holger Weiss %%% Purpose : Push Notifications (XEP-0357) %%% Created : 15 Jul 2017 by Holger Weiss %%% %%% %%% ejabberd, Copyright (C) 2017-2021 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(mod_push). -author('holger@zedat.fu-berlin.de'). -protocol({xep, 357, '0.2'}). -behaviour(gen_mod). %% gen_mod callbacks. -export([start/2, stop/1, reload/3, mod_opt_type/1, mod_options/1, depends/2]). -export([mod_doc/0]). %% ejabberd_hooks callbacks. -export([disco_sm_features/5, c2s_session_pending/1, c2s_copy_session/2, c2s_handle_cast/2, c2s_stanza/3, mam_message/7, offline_message/1, remove_user/2]). %% gen_iq_handler callback. -export([process_iq/1]). %% ejabberd command. -export([get_commands_spec/0, delete_old_sessions/1]). %% API (used by mod_push_keepalive). -export([notify/3, notify/5, notify/7, is_incoming_chat_msg/1]). %% For IQ callbacks -export([delete_session/3]). -include("ejabberd_commands.hrl"). -include("logger.hrl"). -include_lib("xmpp/include/xmpp.hrl"). -include("translate.hrl"). -define(PUSH_CACHE, push_cache). -type c2s_state() :: ejabberd_c2s:state(). -type timestamp() :: erlang:timestamp(). -type push_session() :: {timestamp(), ljid(), binary(), xdata()}. -type err_reason() :: notfound | db_failure. -type direction() :: send | recv | undefined. -callback init(binary(), gen_mod:opts()) -> any(). -callback store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) -> {ok, push_session()} | {error, err_reason()}. -callback lookup_session(binary(), binary(), jid(), binary()) -> {ok, push_session()} | {error, err_reason()}. -callback lookup_session(binary(), binary(), timestamp()) -> {ok, push_session()} | {error, err_reason()}. -callback lookup_sessions(binary(), binary(), jid()) -> {ok, [push_session()]} | {error, err_reason()}. -callback lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. -callback lookup_sessions(binary()) -> {ok, [push_session()]} | {error, err_reason()}. -callback delete_session(binary(), binary(), timestamp()) -> ok | {error, err_reason()}. -callback delete_old_sessions(binary() | global, erlang:timestamp()) -> ok | {error, err_reason()}. -callback use_cache(binary()) -> boolean(). -callback cache_nodes(binary()) -> [node()]. -optional_callbacks([use_cache/1, cache_nodes/1]). %%-------------------------------------------------------------------- %% gen_mod callbacks. %%-------------------------------------------------------------------- -spec start(binary(), gen_mod:opts()) -> ok. start(Host, Opts) -> Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), init_cache(Mod, Host, Opts), register_iq_handlers(Host), register_hooks(Host), ejabberd_commands:register_commands(?MODULE, get_commands_spec()). -spec stop(binary()) -> ok. stop(Host) -> unregister_hooks(Host), unregister_iq_handlers(Host), case gen_mod:is_loaded_elsewhere(Host, ?MODULE) of false -> ejabberd_commands:unregister_commands(get_commands_spec()); true -> ok end. -spec reload(binary(), gen_mod:opts(), gen_mod:opts()) -> ok. reload(Host, NewOpts, OldOpts) -> NewMod = gen_mod:db_mod(NewOpts, ?MODULE), OldMod = gen_mod:db_mod(OldOpts, ?MODULE), if NewMod /= OldMod -> NewMod:init(Host, NewOpts); true -> ok end, init_cache(NewMod, Host, NewOpts), ok. -spec depends(binary(), gen_mod:opts()) -> [{module(), hard | soft}]. depends(_Host, _Opts) -> []. -spec mod_opt_type(atom()) -> econf:validator(). mod_opt_type(include_sender) -> econf:bool(); mod_opt_type(include_body) -> econf:either( econf:bool(), econf:binary()); mod_opt_type(db_type) -> econf:db_type(?MODULE); mod_opt_type(use_cache) -> econf:bool(); mod_opt_type(cache_size) -> econf:pos_int(infinity); mod_opt_type(cache_missed) -> econf:bool(); mod_opt_type(cache_life_time) -> econf:timeout(second, infinity). -spec mod_options(binary()) -> [{atom(), any()}]. mod_options(Host) -> [{include_sender, false}, {include_body, <<"New message">>}, {db_type, ejabberd_config:default_db(Host, ?MODULE)}, {use_cache, ejabberd_option:use_cache(Host)}, {cache_size, ejabberd_option:cache_size(Host)}, {cache_missed, ejabberd_option:cache_missed(Host)}, {cache_life_time, ejabberd_option:cache_life_time(Host)}]. mod_doc() -> #{desc => ?T("This module implements the XMPP server's part of " "the push notification solution specified in " "https://xmpp.org/extensions/xep-0357.html" "[XEP-0357: Push Notifications]. It does not generate, " "for example, APNS or FCM notifications directly. " "Instead, it's designed to work with so-called " "\"app servers\" operated by third-party vendors of " "mobile apps. Those app servers will usually trigger " "notification delivery to the user's mobile device using " "platform-dependant backend services such as FCM or APNS."), opts => [{include_sender, #{value => "true | false", desc => ?T("If this option is set to 'true', the sender's JID " "is included with push notifications generated for " "incoming messages with a body. " "The default value is 'false'.")}}, {include_body, #{value => "true | false | Text", desc => ?T("If this option is set to 'true', the message text " "is included with push notifications generated for " "incoming messages with a body. The option can instead " "be set to a static 'Text', in which case the specified " "text will be included in place of the actual message " "body. This can be useful to signal the app server " "whether the notification was triggered by a message " "with body (as opposed to other types of traffic) " "without leaking actual message contents. " "The default value is \"New message\".")}}, {db_type, #{value => "mnesia | sql", desc => ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}}, {use_cache, #{value => "true | false", desc => ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}}, {cache_size, #{value => "pos_integer() | infinity", desc => ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}}, {cache_missed, #{value => "true | false", desc => ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}}, {cache_life_time, #{value => "timeout()", desc => ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}. %%-------------------------------------------------------------------- %% ejabberd command callback. %%-------------------------------------------------------------------- -spec get_commands_spec() -> [ejabberd_commands()]. get_commands_spec() -> [#ejabberd_commands{name = delete_old_push_sessions, tags = [purge], desc = "Remove push sessions older than DAYS", module = ?MODULE, function = delete_old_sessions, args = [{days, integer}], result = {res, rescode}}]. -spec delete_old_sessions(non_neg_integer()) -> ok | any(). delete_old_sessions(Days) -> CurrentTime = erlang:system_time(microsecond), Diff = Days * 24 * 60 * 60 * 1000000, TimeStamp = misc:usec_to_now(CurrentTime - Diff), DBTypes = lists:usort( lists:map( fun(Host) -> case mod_push_opt:db_type(Host) of sql -> {sql, Host}; Other -> {Other, global} end end, ejabberd_option:hosts())), Results = lists:map( fun({DBType, Host}) -> Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:delete_old_sessions(Host, TimeStamp) end, DBTypes), ets_cache:clear(?PUSH_CACHE, ejabberd_cluster:get_nodes()), case lists:filter(fun(Res) -> Res /= ok end, Results) of [] -> ?INFO_MSG("Deleted push sessions older than ~B days", [Days]), ok; [{error, Reason} | _] -> ?ERROR_MSG("Error while deleting old push sessions: ~p", [Reason]), Reason end. %%-------------------------------------------------------------------- %% Register/unregister hooks. %%-------------------------------------------------------------------- -spec register_hooks(binary()) -> ok. register_hooks(Host) -> ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:add(c2s_session_pending, Host, ?MODULE, c2s_session_pending, 50), ejabberd_hooks:add(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:add(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 50), ejabberd_hooks:add(c2s_handle_send, Host, ?MODULE, c2s_stanza, 50), ejabberd_hooks:add(store_mam_message, Host, ?MODULE, mam_message, 50), ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, offline_message, 55), ejabberd_hooks:add(remove_user, Host, ?MODULE, remove_user, 50). -spec unregister_hooks(binary()) -> ok. unregister_hooks(Host) -> ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_sm_features, 50), ejabberd_hooks:delete(c2s_session_pending, Host, ?MODULE, c2s_session_pending, 50), ejabberd_hooks:delete(c2s_copy_session, Host, ?MODULE, c2s_copy_session, 50), ejabberd_hooks:delete(c2s_handle_cast, Host, ?MODULE, c2s_handle_cast, 50), ejabberd_hooks:delete(c2s_handle_send, Host, ?MODULE, c2s_stanza, 50), ejabberd_hooks:delete(store_mam_message, Host, ?MODULE, mam_message, 50), ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE, offline_message, 55), ejabberd_hooks:delete(remove_user, Host, ?MODULE, remove_user, 50). %%-------------------------------------------------------------------- %% Service discovery. %%-------------------------------------------------------------------- -spec disco_sm_features(empty | {result, [binary()]} | {error, stanza_error()}, jid(), jid(), binary(), binary()) -> {result, [binary()]} | {error, stanza_error()}. disco_sm_features(empty, From, To, Node, Lang) -> disco_sm_features({result, []}, From, To, Node, Lang); disco_sm_features({result, OtherFeatures}, #jid{luser = U, lserver = S}, #jid{luser = U, lserver = S}, <<"">>, _Lang) -> {result, [?NS_PUSH_0 | OtherFeatures]}; disco_sm_features(Acc, _From, _To, _Node, _Lang) -> Acc. %%-------------------------------------------------------------------- %% IQ handlers. %%-------------------------------------------------------------------- -spec register_iq_handlers(binary()) -> ok. register_iq_handlers(Host) -> gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0, ?MODULE, process_iq). -spec unregister_iq_handlers(binary()) -> ok. unregister_iq_handlers(Host) -> gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUSH_0). -spec process_iq(iq()) -> iq(). process_iq(#iq{type = get, lang = Lang} = IQ) -> Txt = ?T("Value 'get' of 'type' attribute is not allowed"), xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); process_iq(#iq{lang = Lang, sub_els = [#push_enable{node = <<>>}]} = IQ) -> Txt = ?T("Enabling push without 'node' attribute is not supported"), xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang)); process_iq(#iq{from = #jid{lserver = LServer} = JID, to = #jid{lserver = LServer}, lang = Lang, sub_els = [#push_enable{jid = PushJID, node = Node, xdata = XData}]} = IQ) -> case enable(JID, PushJID, Node, XData) of ok -> xmpp:make_iq_result(IQ); {error, db_failure} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); {error, notfound} -> Txt = ?T("User session not found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; process_iq(#iq{from = #jid{lserver = LServer} = JID, to = #jid{lserver = LServer}, lang = Lang, sub_els = [#push_disable{jid = PushJID, node = Node}]} = IQ) -> case disable(JID, PushJID, Node) of ok -> xmpp:make_iq_result(IQ); {error, db_failure} -> Txt = ?T("Database failure"), xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); {error, notfound} -> Txt = ?T("Push record not found"), xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) end; process_iq(IQ) -> xmpp:make_error(IQ, xmpp:err_not_allowed()). -spec enable(jid(), jid(), binary(), xdata()) -> ok | {error, err_reason()}. enable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node, XData) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of {TS, PID} -> case store_session(LUser, LServer, TS, PushJID, Node, XData) of {ok, _} -> ?INFO_MSG("Enabling push notifications for ~ts", [jid:encode(JID)]), ejabberd_c2s:cast(PID, push_enable); {error, _} = Err -> ?ERROR_MSG("Cannot enable push for ~ts: database error", [jid:encode(JID)]), Err end; none -> ?WARNING_MSG("Cannot enable push for ~ts: session not found", [jid:encode(JID)]), {error, notfound} end. -spec disable(jid(), jid(), binary() | undefined) -> ok | {error, err_reason()}. disable(#jid{luser = LUser, lserver = LServer, lresource = LResource} = JID, PushJID, Node) -> case ejabberd_sm:get_session_sid(LUser, LServer, LResource) of {_TS, PID} -> ?INFO_MSG("Disabling push notifications for ~ts", [jid:encode(JID)]), ejabberd_c2s:cast(PID, push_disable); none -> ?WARNING_MSG("Session not found while disabling push for ~ts", [jid:encode(JID)]) end, if Node /= <<>> -> delete_session(LUser, LServer, PushJID, Node); true -> delete_sessions(LUser, LServer, PushJID) end. %%-------------------------------------------------------------------- %% Hook callbacks. %%-------------------------------------------------------------------- -spec c2s_stanza(c2s_state(), xmpp_element() | xmlel(), term()) -> c2s_state(). c2s_stanza(State, #stream_error{}, _SendResult) -> State; c2s_stanza(#{push_enabled := true, mgmt_state := pending} = State, Pkt, _SendResult) -> ?DEBUG("Notifying client of stanza", []), notify(State, Pkt, get_direction(Pkt)), State; c2s_stanza(State, _Pkt, _SendResult) -> State. -spec mam_message(message() | drop, binary(), binary(), jid(), binary(), chat | groupchat, recv | send) -> message(). mam_message(#message{} = Pkt, LUser, LServer, _Peer, _Nick, chat, Dir) -> case lookup_sessions(LUser, LServer) of {ok, [_|_] = Clients} -> case drop_online_sessions(LUser, LServer, Clients) of [_|_] = Clients1 -> ?DEBUG("Notifying ~ts@~ts of MAM message", [LUser, LServer]), notify(LUser, LServer, Clients1, Pkt, Dir); [] -> ok end; _ -> ok end, Pkt; mam_message(Pkt, _LUser, _LServer, _Peer, _Nick, _Type, _Dir) -> Pkt. -spec offline_message({any(), message()}) -> {any(), message()}. offline_message({offlined, #message{meta = #{mam_archived := true}}} = Acc) -> Acc; % Push notification was triggered via MAM. offline_message({offlined, #message{to = #jid{luser = LUser, lserver = LServer}} = Pkt} = Acc) -> case lookup_sessions(LUser, LServer) of {ok, [_|_] = Clients} -> ?DEBUG("Notifying ~ts@~ts of offline message", [LUser, LServer]), notify(LUser, LServer, Clients, Pkt, recv); _ -> ok end, Acc; offline_message(Acc) -> Acc. -spec c2s_session_pending(c2s_state()) -> c2s_state(). c2s_session_pending(#{push_enabled := true, mgmt_queue := Queue} = State) -> case p1_queue:len(Queue) of Len when Len > 0 -> ?DEBUG("Notifying client of unacknowledged stanza(s)", []), {Pkt, Dir} = case mod_stream_mgmt:queue_find( fun is_incoming_chat_msg/1, Queue) of none -> {none, undefined}; Pkt0 -> {Pkt0, get_direction(Pkt0)} end, notify(State, Pkt, Dir), State; 0 -> State end; c2s_session_pending(State) -> State. -spec c2s_copy_session(c2s_state(), c2s_state()) -> c2s_state(). c2s_copy_session(State, #{push_enabled := true}) -> State#{push_enabled => true}; c2s_copy_session(State, _) -> State. -spec c2s_handle_cast(c2s_state(), any()) -> c2s_state() | {stop, c2s_state()}. c2s_handle_cast(State, push_enable) -> {stop, State#{push_enabled => true}}; c2s_handle_cast(State, push_disable) -> {stop, maps:remove(push_enabled, State)}; c2s_handle_cast(State, _Msg) -> State. -spec remove_user(binary(), binary()) -> ok | {error, err_reason()}. remove_user(LUser, LServer) -> ?INFO_MSG("Removing any push sessions of ~ts@~ts", [LUser, LServer]), Mod = gen_mod:db_mod(LServer, ?MODULE), LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer) end, delete_sessions(LUser, LServer, LookupFun, Mod). %%-------------------------------------------------------------------- %% Generate push notifications. %%-------------------------------------------------------------------- -spec notify(c2s_state(), xmpp_element() | xmlel() | none, direction()) -> ok. notify(#{jid := #jid{luser = LUser, lserver = LServer}, sid := {TS, _}}, Pkt, Dir) -> case lookup_session(LUser, LServer, TS) of {ok, Client} -> notify(LUser, LServer, [Client], Pkt, Dir); _Err -> ok end. -spec notify(binary(), binary(), [push_session()], xmpp_element() | xmlel() | none, direction()) -> ok. notify(LUser, LServer, Clients, Pkt, Dir) -> lists:foreach( fun({TS, PushLJID, Node, XData}) -> HandleResponse = fun(#iq{type = result}) -> ?DEBUG("~ts accepted notification for ~ts@~ts (~ts)", [jid:encode(PushLJID), LUser, LServer, Node]); (#iq{type = error} = IQ) -> case inspect_error(IQ) of {wait, Reason} -> ?INFO_MSG("~ts rejected notification for " "~ts@~ts (~ts) temporarily: ~ts", [jid:encode(PushLJID), LUser, LServer, Node, Reason]); {Type, Reason} -> spawn(?MODULE, delete_session, [LUser, LServer, TS]), ?WARNING_MSG("~ts rejected notification for " "~ts@~ts (~ts), disabling push: ~ts " "(~ts)", [jid:encode(PushLJID), LUser, LServer, Node, Reason, Type]) end; (timeout) -> ?DEBUG("Timeout sending notification for ~ts@~ts (~ts) " "to ~ts", [LUser, LServer, Node, jid:encode(PushLJID)]), ok % Hmm. end, notify(LServer, PushLJID, Node, XData, Pkt, Dir, HandleResponse) end, Clients). -spec notify(binary(), ljid(), binary(), xdata(), xmpp_element() | xmlel() | none, direction(), fun((iq() | timeout) -> any())) -> ok. notify(LServer, PushLJID, Node, XData, Pkt0, Dir, HandleResponse) -> Pkt = unwrap_message(Pkt0), From = jid:make(LServer), Summary = make_summary(LServer, Pkt, Dir), Item = #ps_item{sub_els = [#push_notification{xdata = Summary}]}, PubSub = #pubsub{publish = #ps_publish{node = Node, items = [Item]}, publish_options = XData}, IQ = #iq{type = set, from = From, to = jid:make(PushLJID), id = p1_rand:get_string(), sub_els = [PubSub]}, ejabberd_router:route_iq(IQ, HandleResponse). %%-------------------------------------------------------------------- %% Miscellaneous. %%-------------------------------------------------------------------- -spec is_incoming_chat_msg(stanza()) -> boolean(). is_incoming_chat_msg(#message{} = Msg) -> case get_direction(Msg) of recv -> get_body_text(unwrap_message(Msg)) /= none; send -> false end; is_incoming_chat_msg(_Stanza) -> false. %%-------------------------------------------------------------------- %% Internal functions. %%-------------------------------------------------------------------- -spec store_session(binary(), binary(), timestamp(), jid(), binary(), xdata()) -> {ok, push_session()} | {error, err_reason()}. store_session(LUser, LServer, TS, PushJID, Node, XData) -> Mod = gen_mod:db_mod(LServer, ?MODULE), delete_session(LUser, LServer, PushJID, Node), case use_cache(Mod, LServer) of true -> ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)), ets_cache:update( ?PUSH_CACHE, {LUser, LServer, TS}, {ok, {TS, PushJID, Node, XData}}, fun() -> Mod:store_session(LUser, LServer, TS, PushJID, Node, XData) end, cache_nodes(Mod, LServer)); false -> Mod:store_session(LUser, LServer, TS, PushJID, Node, XData) end. -spec lookup_session(binary(), binary(), timestamp()) -> {ok, push_session()} | error | {error, err_reason()}. lookup_session(LUser, LServer, TS) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PUSH_CACHE, {LUser, LServer, TS}, fun() -> Mod:lookup_session(LUser, LServer, TS) end); false -> Mod:lookup_session(LUser, LServer, TS) end. -spec lookup_sessions(binary(), binary()) -> {ok, [push_session()]} | {error, err_reason()}. lookup_sessions(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case use_cache(Mod, LServer) of true -> ets_cache:lookup( ?PUSH_CACHE, {LUser, LServer}, fun() -> Mod:lookup_sessions(LUser, LServer) end); false -> Mod:lookup_sessions(LUser, LServer) end. -spec delete_session(binary(), binary(), timestamp()) -> ok | {error, db_failure}. delete_session(LUser, LServer, TS) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:delete_session(LUser, LServer, TS) of ok -> case use_cache(Mod, LServer) of true -> ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)), ets_cache:delete(?PUSH_CACHE, {LUser, LServer, TS}, cache_nodes(Mod, LServer)); false -> ok end; {error, _} = Err -> Err end. -spec delete_session(binary(), binary(), jid(), binary()) -> ok | {error, err_reason()}. delete_session(LUser, LServer, PushJID, Node) -> Mod = gen_mod:db_mod(LServer, ?MODULE), case Mod:lookup_session(LUser, LServer, PushJID, Node) of {ok, {TS, _, _, _}} -> delete_session(LUser, LServer, TS); error -> {error, notfound}; {error, _} = Err -> Err end. -spec delete_sessions(binary(), binary(), jid()) -> ok | {error, err_reason()}. delete_sessions(LUser, LServer, PushJID) -> Mod = gen_mod:db_mod(LServer, ?MODULE), LookupFun = fun() -> Mod:lookup_sessions(LUser, LServer, PushJID) end, delete_sessions(LUser, LServer, LookupFun, Mod). -spec delete_sessions(binary(), binary(), fun(() -> any()), module()) -> ok | {error, err_reason()}. delete_sessions(LUser, LServer, LookupFun, Mod) -> case LookupFun() of {ok, []} -> {error, notfound}; {ok, Clients} -> case use_cache(Mod, LServer) of true -> ets_cache:delete(?PUSH_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)); false -> ok end, lists:foreach( fun({TS, _, _, _}) -> ok = Mod:delete_session(LUser, LServer, TS), case use_cache(Mod, LServer) of true -> ets_cache:delete(?PUSH_CACHE, {LUser, LServer, TS}, cache_nodes(Mod, LServer)); false -> ok end end, Clients); {error, _} = Err -> Err end. -spec drop_online_sessions(binary(), binary(), [push_session()]) -> [push_session()]. drop_online_sessions(LUser, LServer, Clients) -> SessIDs = ejabberd_sm:get_session_sids(LUser, LServer), [Client || {TS, _, _, _} = Client <- Clients, lists:keyfind(TS, 1, SessIDs) == false]. -spec make_summary(binary(), xmpp_element() | xmlel() | none, direction()) -> xdata() | undefined. make_summary(Host, #message{from = From} = Pkt, recv) -> case {mod_push_opt:include_sender(Host), mod_push_opt:include_body(Host)} of {false, false} -> undefined; {IncludeSender, IncludeBody} -> case get_body_text(Pkt) of none -> undefined; Text -> Fields1 = case IncludeBody of StaticText when is_binary(StaticText) -> [{'last-message-body', StaticText}]; true -> [{'last-message-body', Text}]; false -> [] end, Fields2 = case IncludeSender of true -> [{'last-message-sender', From} | Fields1]; false -> Fields1 end, #xdata{type = submit, fields = push_summary:encode(Fields2)} end end; make_summary(_Host, _Pkt, _Dir) -> undefined. -spec unwrap_message(Stanza) -> Stanza when Stanza :: stanza() | none. unwrap_message(#message{meta = #{carbon_copy := true}} = Msg) -> misc:unwrap_carbon(Msg); unwrap_message(#message{type = normal} = Msg) -> case misc:unwrap_mucsub_message(Msg) of #message{} = InnerMsg -> InnerMsg; false -> Msg end; unwrap_message(Stanza) -> Stanza. -spec get_direction(stanza()) -> direction(). get_direction(#message{meta = #{carbon_copy := true}, from = #jid{luser = U, lserver = S}, to = #jid{luser = U, lserver = S}}) -> send; get_direction(#message{}) -> recv; get_direction(_Stanza) -> undefined. -spec get_body_text(message()) -> binary() | none. get_body_text(#message{body = Body} = Msg) -> case xmpp:get_text(Body) of Text when byte_size(Text) > 0 -> Text; <<>> -> case body_is_encrypted(Msg) of true -> <<"(encrypted)">>; false -> none end end. -spec body_is_encrypted(message()) -> boolean(). body_is_encrypted(#message{sub_els = MsgEls}) -> case lists:keyfind(<<"encrypted">>, #xmlel.name, MsgEls) of #xmlel{children = EncEls} -> lists:keyfind(<<"payload">>, #xmlel.name, EncEls) /= false; false -> false end. -spec inspect_error(iq()) -> {atom(), binary()}. inspect_error(IQ) -> case xmpp:get_error(IQ) of #stanza_error{type = Type} = Err -> {Type, xmpp:format_stanza_error(Err)}; undefined -> {undefined, <<"unrecognized error">>} end. %%-------------------------------------------------------------------- %% Caching. %%-------------------------------------------------------------------- -spec init_cache(module(), binary(), gen_mod:opts()) -> ok. init_cache(Mod, Host, Opts) -> case use_cache(Mod, Host) of true -> CacheOpts = cache_opts(Opts), ets_cache:new(?PUSH_CACHE, CacheOpts); false -> ets_cache:delete(?PUSH_CACHE) end. -spec cache_opts(gen_mod:opts()) -> [proplists:property()]. cache_opts(Opts) -> MaxSize = mod_push_opt:cache_size(Opts), CacheMissed = mod_push_opt:cache_missed(Opts), LifeTime = mod_push_opt:cache_life_time(Opts), [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. -spec use_cache(module(), binary()) -> boolean(). use_cache(Mod, Host) -> case erlang:function_exported(Mod, use_cache, 1) of true -> Mod:use_cache(Host); false -> mod_push_opt:use_cache(Host) end. -spec cache_nodes(module(), binary()) -> [node()]. cache_nodes(Mod, Host) -> case erlang:function_exported(Mod, cache_nodes, 1) of true -> Mod:cache_nodes(Host); false -> ejabberd_cluster:get_nodes() end. ejabberd-21.12/src/mod_mam_opt.erl0000644000232200023220000000667214154362354017433 0ustar debalancedebalance%% Generated automatically %% DO NOT EDIT: run `make options` instead -module(mod_mam_opt). -export([access_preferences/1]). -export([assume_mam_usage/1]). -export([cache_life_time/1]). -export([cache_missed/1]). -export([cache_size/1]). -export([clear_archive_on_room_destroy/1]). -export([compress_xml/1]). -export([db_type/1]). -export([default/1]). -export([request_activates_archiving/1]). -export([use_cache/1]). -export([user_mucsub_from_muc_archive/1]). -spec access_preferences(gen_mod:opts() | global | binary()) -> 'all' | acl:acl(). access_preferences(Opts) when is_map(Opts) -> gen_mod:get_opt(access_preferences, Opts); access_preferences(Host) -> gen_mod:get_module_opt(Host, mod_mam, access_preferences). -spec assume_mam_usage(gen_mod:opts() | global | binary()) -> boolean(). assume_mam_usage(Opts) when is_map(Opts) -> gen_mod:get_opt(assume_mam_usage, Opts); assume_mam_usage(Host) -> gen_mod:get_module_opt(Host, mod_mam, assume_mam_usage). -spec cache_life_time(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_life_time(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_life_time, Opts); cache_life_time(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_life_time). -spec cache_missed(gen_mod:opts() | global | binary()) -> boolean(). cache_missed(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_missed, Opts); cache_missed(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_missed). -spec cache_size(gen_mod:opts() | global | binary()) -> 'infinity' | pos_integer(). cache_size(Opts) when is_map(Opts) -> gen_mod:get_opt(cache_size, Opts); cache_size(Host) -> gen_mod:get_module_opt(Host, mod_mam, cache_size). -spec clear_archive_on_room_destroy(gen_mod:opts() | global | binary()) -> boolean(). clear_archive_on_room_destroy(Opts) when is_map(Opts) -> gen_mod:get_opt(clear_archive_on_room_destroy, Opts); clear_archive_on_room_destroy(Host) -> gen_mod:get_module_opt(Host, mod_mam, clear_archive_on_room_destroy). -spec compress_xml(gen_mod:opts() | global | binary()) -> boolean(). compress_xml(Opts) when is_map(Opts) -> gen_mod:get_opt(compress_xml, Opts); compress_xml(Host) -> gen_mod:get_module_opt(Host, mod_mam, compress_xml). -spec db_type(gen_mod:opts() | global | binary()) -> atom(). db_type(Opts) when is_map(Opts) -> gen_mod:get_opt(db_type, Opts); db_type(Host) -> gen_mod:get_module_opt(Host, mod_mam, db_type). -spec default(gen_mod:opts() | global | binary()) -> 'always' | 'never' | 'roster'. default(Opts) when is_map(Opts) -> gen_mod:get_opt(default, Opts); default(Host) -> gen_mod:get_module_opt(Host, mod_mam, default). -spec request_activates_archiving(gen_mod:opts() | global | binary()) -> boolean(). request_activates_archiving(Opts) when is_map(Opts) -> gen_mod:get_opt(request_activates_archiving, Opts); request_activates_archiving(Host) -> gen_mod:get_module_opt(Host, mod_mam, request_activates_archiving). -spec use_cache(gen_mod:opts() | global | binary()) -> boolean(). use_cache(Opts) when is_map(Opts) -> gen_mod:get_opt(use_cache, Opts); use_cache(Host) -> gen_mod:get_module_opt(Host, mod_mam, use_cache). -spec user_mucsub_from_muc_archive(gen_mod:opts() | global | binary()) -> boolean(). user_mucsub_from_muc_archive(Opts) when is_map(Opts) -> gen_mod:get_opt(user_mucsub_from_muc_archive, Opts); user_mucsub_from_muc_archive(Host) -> gen_mod:get_module_opt(Host, mod_mam, user_mucsub_from_muc_archive). ejabberd-21.12/ejabberd.yml.example0000644000232200023220000001215414154362354017550 0ustar debalancedebalance### ### ejabberd configuration file ### ### The parameters used in this configuration file are explained at ### ### https://docs.ejabberd.im/admin/configuration ### ### The configuration file is written in YAML. ### ******************************************************* ### ******* !!! WARNING !!! ******* ### ******* YAML IS INDENTATION SENSITIVE ******* ### ******* MAKE SURE YOU INDENT SECTIONS CORRECTLY ******* ### ******************************************************* ### Refer to http://en.wikipedia.org/wiki/YAML for the brief description. ### hosts: - localhost loglevel: info ## If you already have certificates, list them here # certfiles: # - /etc/letsencrypt/live/domain.tld/fullchain.pem # - /etc/letsencrypt/live/domain.tld/privkey.pem listen: - port: 5222 ip: "::" module: ejabberd_c2s max_stanza_size: 262144 shaper: c2s_shaper access: c2s starttls_required: true - port: 5223 ip: "::" tls: true module: ejabberd_c2s max_stanza_size: 262144 shaper: c2s_shaper access: c2s starttls_required: true - port: 5269 ip: "::" module: ejabberd_s2s_in max_stanza_size: 524288 - port: 5443 ip: "::" module: ejabberd_http tls: true request_handlers: /admin: ejabberd_web_admin /api: mod_http_api /bosh: mod_bosh /captcha: ejabberd_captcha /upload: mod_http_upload /ws: ejabberd_http_ws - port: 5280 ip: "::" module: ejabberd_http request_handlers: /admin: ejabberd_web_admin /.well-known/acme-challenge: ejabberd_acme - port: 3478 ip: "::" transport: udp module: ejabberd_stun use_turn: true ## The server's public IPv4 address: # turn_ipv4_address: "203.0.113.3" ## The server's public IPv6 address: # turn_ipv6_address: "2001:db8::3" - port: 1883 ip: "::" module: mod_mqtt backlog: 1000 s2s_use_starttls: optional acl: local: user_regexp: "" loopback: ip: - 127.0.0.0/8 - ::1/128 access_rules: local: allow: local c2s: deny: blocked allow: all announce: allow: admin configure: allow: admin muc_create: allow: local pubsub_createnode: allow: local trusted_network: allow: loopback api_permissions: "console commands": from: - ejabberd_ctl who: all what: "*" "admin access": who: access: allow: - acl: loopback - acl: admin oauth: scope: "ejabberd:admin" access: allow: - acl: loopback - acl: admin what: - "*" - "!stop" - "!start" "public commands": who: ip: 127.0.0.1/8 what: - status - connected_users_number shaper: normal: rate: 3000 burst_size: 20000 fast: 100000 shaper_rules: max_user_sessions: 10 max_user_offline_messages: 5000: admin 100: all c2s_shaper: none: admin normal: all s2s_shaper: fast modules: mod_adhoc: {} mod_admin_extra: {} mod_announce: access: announce mod_avatar: {} mod_blocking: {} mod_bosh: {} mod_caps: {} mod_carboncopy: {} mod_client_state: {} mod_configure: {} mod_disco: {} mod_fail2ban: {} mod_http_api: {} mod_http_upload: put_url: https://@HOST@:5443/upload custom_headers: "Access-Control-Allow-Origin": "https://@HOST@" "Access-Control-Allow-Methods": "GET,HEAD,PUT,OPTIONS" "Access-Control-Allow-Headers": "Content-Type" mod_last: {} mod_mam: ## Mnesia is limited to 2GB, better to use an SQL backend ## For small servers SQLite is a good fit and is very easy ## to configure. Uncomment this when you have SQL configured: ## db_type: sql assume_mam_usage: true default: always mod_mqtt: {} mod_muc: access: - allow access_admin: - allow: admin access_create: muc_create access_persistent: muc_create access_mam: - allow default_room_options: mam: true mod_muc_admin: {} mod_offline: access_max_user_messages: max_user_offline_messages mod_ping: {} mod_privacy: {} mod_private: {} mod_proxy65: access: local max_connections: 5 mod_pubsub: access_createnode: pubsub_createnode plugins: - flat - pep force_node_config: ## Avoid buggy clients to make their bookmarks public storage:bookmarks: access_model: whitelist mod_push: {} mod_push_keepalive: {} mod_register: ## Only accept registration requests from the "trusted" ## network (see access_rules section above). ## Think twice before enabling registration from any ## address. See the Jabber SPAM Manifesto for details: ## https://github.com/ge0rg/jabber-spam-fighting-manifesto ip_access: trusted_network mod_roster: versioning: true mod_s2s_dialback: {} mod_shared_roster: {} mod_stream_mgmt: resend_on_timeout: if_offline mod_stun_disco: {} mod_vcard: {} mod_vcard_xupdate: {} mod_version: show_os: false ### Local Variables: ### mode: yaml ### End: ### vim: set filetype=yaml tabstop=8 ejabberd-21.12/examples/0000755000232200023220000000000014154362354015450 5ustar debalancedebalanceejabberd-21.12/examples/extauth/0000755000232200023220000000000014154362354017132 5ustar debalancedebalanceejabberd-21.12/examples/extauth/check_pass_null.pl0000755000232200023220000000266314154362354022636 0ustar debalancedebalance#!/usr/bin/perl use Unix::Syslog qw(:macros :subs); my $domain = $ARGV[0] || "example.com"; while(1) { # my $rin = '',$rout; # vec($rin,fileno(STDIN),1) = 1; # $ein = $rin; # my $nfound = select($rout=$rin,undef,undef,undef); my $buf = ""; syslog LOG_INFO,"waiting for packet"; my $nread = sysread STDIN,$buf,2; do { syslog LOG_INFO,"port closed"; exit; } unless $nread == 2; my $len = unpack "n",$buf; my $nread = sysread STDIN,$buf,$len; my ($op,$user,$host,$password) = split /:/,$buf; #$user =~ s/\./\//og; my $jid = "$user\@$domain"; my $result; syslog(LOG_INFO,"request (%s)", $op); SWITCH: { $op eq 'auth' and do { $result = 1; },last SWITCH; $op eq 'setpass' and do { $result = 1; },last SWITCH; $op eq 'isuser' and do { # password is null. Return 1 if the user $user\@$domain exitst. $result = 1; },last SWITCH; $op eq 'tryregister' and do { $result = 1; },last SWITCH; $op eq 'removeuser' and do { # password is null. Return 1 if the user $user\@$domain exitst. $result = 1; },last SWITCH; $op eq 'removeuser3' and do { $result = 1; },last SWITCH; }; my $out = pack "nn",2,$result ? 1 : 0; syswrite STDOUT,$out; } closelog; ejabberd-21.12/examples/mtr/0000755000232200023220000000000014154362354016252 5ustar debalancedebalanceejabberd-21.12/examples/mtr/ejabberd-netbsd.sh0000644000232200023220000000411114154362354021616 0ustar debalancedebalance#!/bin/sh echo '1. fetch, compile, and install erlang' if [ ! pkg_info erlang 1>/dev/null 2>&1 ]; then cd /usr/pkgsrc/lang/erlang make fetch-list|sh make make install fi if pkg_info erlang | grep -q erlang-9.1nb1; then else echo "erlang-9.1nb1 not installed" 1>&2 exit 1 fi echo '2. install crypt_drv.so' if [ ! -d /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib ] ; then mkdir -p /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib fi if [ ! -f /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib/crypto_drv.so ]; then cp work/otp*/lib/crypto/priv/*/*/crypto_drv.so \ /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib fi echo '3. compile and install elibcrypto.so' if [ ! -f /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib/elibcrypto.so ]; then cd /usr/pkgsrc/lang/erlang/work/otp_src_R9B-1/lib/crypto/c_src ld -r -u CRYPTO_set_mem_functions -u MD5 -u MD5_Init -u MD5_Update \ -u MD5_Final -u SHA1 -u SHA1_Init -u SHA1_Update -u SHA1_Final \ -u des_set_key -u des_ncbc_encrypt -u des_ede3_cbc_encrypt \ -L/usr/lib -lcrypto -o ../priv/obj/i386--netbsdelf/elibcrypto.o cc -shared \ -L/usr/pkgsrc/lang/erlang/work/otp_src_R9B-1/lib/erl_interface/obj/i386--netbsdelf \ -o ../priv/obj/i386--netbsdelf/elibcrypto.so \ ../priv/obj/i386--netbsdelf/elibcrypto.o -L/usr/lib -lcrypto cp ../priv/obj/i386--netbsdelf/elibcrypto.so \ /usr/pkg/lib/erlang/lib/crypto-1.1.2.1/priv/lib fi echo '4. compile and install ssl_esock' if [ ! -f /usr/pkg/lib/erlang/lib/ssl-2.3.5/priv/bin/ssl_esock ]; then cd /usr/pkg/lib/erlang/lib/ssl-2.3.5/priv/obj/ make fi echo '5. initial ejabberd configuration' cd /usr/pkg/jabber/ejabberd/src ./configure echo '6. edit ejabberd Makefiles' for M in Makefile mod_*/Makefile; do if [ ! -f $M.orig ]; then mv $M $M.orig sed -e s%/usr/local%/usr/pkg%g < $M.orig > $M fi done echo '7. compile ejabberd' gmake for A in mod_muc mod_pubsub; do (cd $A; gmake) done echo '' echo 'now edit ejabberd.cfg' echo '' echo 'to start ejabberd: erl -sname ejabberd -s ejabberd' ejabberd-21.12/examples/mtr/ejabberd0000644000232200023220000000315414154362354017736 0ustar debalancedebalance#!/bin/sh # # PROVIDE: ejabberd # REQUIRE: DAEMON # KEYWORD: shutdown # HOME=/usr/pkg/jabber D=/usr/pkg/jabber/ejabberd export HOME name="ejabberd" rcvar=$name if [ -r /etc/rc.conf ] then . /etc/rc.conf else eval ${rcvar}=YES fi # $flags from environment overrides ${rcvar}_flags if [ -n "${flags}" ] then eval ${rcvar}_flags="${flags}" fi checkyesno() { eval _value=\$${1} case $_value in [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1) return 0 ;; [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0) return 1 ;; *) echo "\$${1} is not set properly." return 1 ;; esac } cmd=${1:-start} case ${cmd} in force*) cmd=${cmd#force} eval ${rcvar}=YES ;; esac if checkyesno ${rcvar} then else exit 0 fi case ${cmd} in start) if [ -x $D/src ]; then echo "Starting ${name}." cd $D/src ERL_MAX_PORTS=32000 export ERL_MAX_PORTS ulimit -n $ERL_MAX_PORTS su jabber -c "/usr/pkg/bin/erl -sname ejabberd -s ejabberd -heart -detached -sasl sasl_error_logger '{file, \"ejabberd-sasl.log\"}' &" \ 1>/dev/null 2>&1 fi ;; stop) echo "rpc:call('ejabberd@`hostname -s`', init, stop, [])." | \ su jabber -c "/usr/pkg/bin/erl -sname ejabberdstop" ;; restart) echo "rpc:call('ejabberd@`hostname -s`', init, restart, [])." | \ su jabber -c "/usr/pkg/bin/erl -sname ejabberdrestart" ;; *) echo "Usage: $0 {start|stop|restart}" exit 1 esac ejabberd-21.12/examples/mtr/ejabberd.cfg0000644000232200023220000000355014154362354020474 0ustar debalancedebalance% jabber.dbc.mtview.ca.us override_acls. {acl, admin, {user, "mrose", "jabber.dbc.mtview.ca.us"}}. {access, announce, [{allow, admin}, {deny, all}]}. {access, c2s, [{deny, blocked}, {allow, all}]}. {access, c2s_shaper, [{none, admin}, {normal, all}]}. {access, configure, [{allow, admin}, {deny, all}]}. {access, disco_admin, [{allow, admin}, {deny, all}]}. {access, muc_admin, [{allow, admin}, {deny, all}]}. {access, register, [{deny, all}]}. {access, s2s_shaper, [{fast, all}]}. {auth_method, internal}. {host, "jabber.dbc.mtview.ca.us"}. {outgoing_s2s_port, 5269}. {shaper, normal, {maxrate, 1000}}. {shaper, fast, {maxrate, 50000}}. {welcome_message, none}. {listen, [{5222, ejabberd_c2s, [{access, c2s}, {shaper, c2s_shaper}]}, {5223, ejabberd_c2s, [{access, c2s}, {shaper, c2s_shaper}, {ssl, [{certfile, "/etc/openssl/certs/ejabberd.pem"}]}]}, {5269, ejabberd_s2s_in, [{shaper, s2s_shaper}]}]}. {modules, [ {mod_register, []}, {mod_roster, []}, {mod_privacy, []}, {mod_configure, []}, {mod_disco, []}, {mod_stats, []}, {mod_vcard, []}, {mod_offline, []}, {mod_echo, [{host, "echo.jabber.dbc.mtview.ca.us"}]}, {mod_private, []}, {mod_muc, []}, {mod_pubsub, []}, {mod_time, []}, {mod_last, []}, {mod_version, []} ]}. % Local Variables: % mode: erlang % End: ejabberd-21.12/rebar.config0000644000232200023220000002465014154362354016123 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- {deps, [{base64url, ".*", {git, "https://github.com/dvv/base64url", {tag, "1.0.1"}}}, {cache_tab, ".*", {git, "https://github.com/processone/cache_tab", {tag, "1.0.29"}}}, {eimp, ".*", {git, "https://github.com/processone/eimp", {tag, "1.0.21"}}}, {if_var_true, tools, {ejabberd_po, ".*", {git, "https://github.com/processone/ejabberd-po", {branch, "main"}}}}, {if_var_true, elixir, {elixir, ".*", {git, "https://github.com/elixir-lang/elixir", {tag, "v1.4.4"}}}}, {if_var_true, pam, {epam, ".*", {git, "https://github.com/processone/epam", {tag, "1.0.12"}}}}, {if_var_true, redis, {eredis, ".*", {git, "https://github.com/wooga/eredis", {tag, "v1.2.0"}}}}, {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.45"}}}}, {if_var_true, zlib, {ezlib, ".*", {git, "https://github.com/processone/ezlib", {tag, "1.0.10"}}}}, {fast_tls, ".*", {git, "https://github.com/processone/fast_tls", {tag, "1.1.13"}}}, {fast_xml, ".*", {git, "https://github.com/processone/fast_xml", {tag, "1.1.48"}}}, {fast_yaml, ".*", {git, "https://github.com/processone/fast_yaml", {tag, "1.0.32"}}}, {idna, ".*", {git, "https://github.com/benoitc/erlang-idna", {tag, "6.0.0"}}}, {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "1.0.5"}}}, {jose, ".*", {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.1"}}}, {lager, ".*", {git, "https://github.com/erlang-lager/lager", {tag, "3.9.1"}}}, {if_var_true, lua, {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.3"}}}}, {mqtree, ".*", {git, "https://github.com/processone/mqtree", {tag, "1.0.14"}}}, {p1_acme, ".*", {git, "https://github.com/processone/p1_acme", {tag, "1.0.16"}}}, {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", {tag, "1.0.19"}}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.10"}}}, {if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", {tag, "1.1.16"}}}}, {p1_utils, ".*", {git, "https://github.com/processone/p1_utils", {tag, "1.0.23"}}}, {pkix, ".*", {git, "https://github.com/processone/pkix", {tag, "1.0.8"}}}, {if_not_rebar3, %% Needed because modules are not fully migrated to new structure and mix {if_var_true, elixir, {rebar_elixir_plugin, ".*", {git, "https://github.com/processone/rebar_elixir_plugin", "0.1.0"}}}}, {if_var_true, sqlite, {sqlite3, ".*", {git, "https://github.com/processone/erlang-sqlite3", {tag, "1.1.13"}}}}, {stringprep, ".*", {git, "https://github.com/processone/stringprep", {tag, "1.0.27"}}}, {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.47"}}}}, {xmpp, ".*", {git, "https://github.com/processone/xmpp", {tag, "1.5.6"}}}, {yconf, ".*", {git, "https://github.com/processone/yconf", {tag, "1.0.12"}}} ]}. {gitonly_deps, [elixir, luerl]}. {if_var_true, latest_deps, {floating_deps, [cache_tab, eimp, epam, esip, ezlib, fast_tls, fast_xml, fast_yaml, mqtree, p1_acme, p1_mysql, p1_oauth2, p1_pgsql, p1_utils, pkix, sqlite3, stringprep, stun, xmpp, yconf]}}. {erl_first_files, ["src/ejabberd_sql_pt.erl", "src/ejabberd_config.erl", "src/gen_mod.erl", "src/mod_muc_room.erl", "src/mod_push.erl", "src/xmpp_socket.erl"]}. {erl_opts, [nowarn_deprecated_function, {i, "include"}, {if_version_above, "20", {d, 'DEPRECATED_GET_STACKTRACE'}}, {if_version_below, "21", {d, 'USE_OLD_HTTP_URI'}}, {if_version_below, "22", {d, 'LAGER'}}, {if_version_below, "21", {d, 'NO_CUSTOMIZE_HOSTNAME_CHECK'}}, {if_version_below, "23", {d, 'USE_OLD_CRYPTO_HMAC'}}, {if_version_below, "23", {d, 'USE_OLD_PG2'}}, {if_version_below, "24", {d, 'COMPILER_REPORTS_ONLY_LINES'}}, {if_version_below, "24", {d, 'SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL'}}, {if_var_match, db_type, mssql, {d, 'mssql'}}, {if_var_false, debug, no_debug_info}, {if_var_true, debug, debug_info}, {if_var_true, elixir, {d, 'ELIXIR_ENABLED'}}, {if_var_true, new_sql_schema, {d, 'NEW_SQL_SCHEMA'}}, {if_var_true, roster_gateway_workaround, {d, 'ROSTER_GATWAY_WORKAROUND'}}, {if_var_true, sip, {d, 'SIP'}}, {if_var_true, stun, {d, 'STUN'}}, {if_have_fun, {erl_error, format_exception, 6}, {d, 'HAVE_ERL_ERROR'}}, {if_have_fun, {uri_string, normalize, 1}, {d, 'HAVE_URI_STRING'}}, {src_dirs, [src, {if_rebar3, sql}, {if_var_true, tools, tools}, {if_var_true, elixir, include}]}]}. {if_rebar3, {plugins, [rebar3_hex, {provider_asn1, "0.2.0"}]}}. {if_rebar3, {project_plugins, [configure_deps]}}. {if_not_rebar3, {plugins, [ deps_erl_opts, override_deps_versions2, override_opts, configure_deps, {if_var_true, elixir, rebar_elixir_compiler}, {if_var_true, elixir, rebar_exunit} ]}}. {if_rebar3, {if_var_true, elixir, {project_app_dirs, [".", "elixir/lib"]}}}. {if_not_rebar3, {if_var_true, elixir, {lib_dirs, ["deps/elixir/lib"]}}}. {if_var_true, elixir, {src_dirs, ["include"]}}. {sub_dirs, ["rel"]}. {keep_build_info, true}. {xref_warnings, false}. {xref_checks, [deprecated_function_calls]}. {xref_exclusions, [ "(\"gen_transport\":_/_)", "(\"eprof\":_/_)", {if_var_false, elixir, "(\"Elixir.*\":_/_)"}, {if_var_false, http, "(\"lhttpc\":_/_)"}, {if_var_false, mysql, "(\".*mysql.*\":_/_)"}, {if_var_false, odbc, "(\"odbc\":_/_)"}, {if_var_false, pam, "(\"epam\":_/_)"}, {if_var_false, pgsql, "(\".*pgsql.*\":_/_)"}, {if_var_false, redis, "(\"eredis\":_/_)"}, {if_var_false, sqlite, "(\"sqlite3\":_/_)"}, {if_var_false, zlib, "(\"ezlib\":_/_)"}]}. {eunit_compile_opts, [{i, "tools"}, {i, "include"}]}. {cover_enabled, true}. {cover_export_enabled, true}. {coveralls_coverdata, "_build/test/cover/ct.coverdata"}. {coveralls_service_name, "github"}. {recursive_cmds, ['configure-deps']}. {overrides, [ {del, [{erl_opts, [warnings_as_errors]}]}]}. {post_hook_configure, [{"eimp", []}, {if_var_true, pam, {"epam", []}}, {if_var_true, sip, {"esip", []}}, {if_var_true, zlib, {"ezlib", []}}, {"fast_tls", []}, {"fast_xml", [{if_var_true, full_xml, "--enable-full-xml"}]}, {"fast_yaml", []}, {"stringprep", []}]}. {relx, [{release, {ejabberd, {cmd, "grep {vsn, vars.config | sed 's|{vsn, \"||;s|\"}.||' | tr -d '\012'"}}, [ejabberd]}, {sys_config, "./rel/sys.config"}, {vm_args, "./rel/vm.args"}, {overlay_vars, "vars.config"}, {extended_start_script, true}, {overlay, [{mkdir, "var/log/ejabberd"}, {mkdir, "var/lib/ejabberd"}, {mkdir, "etc/ejabberd"}, {copy, "rel/files/erl", "\{\{erts_vsn\}\}/bin/erl"}, % in rebar2 this prepends erts- {template, "ejabberdctl.template", "bin/ejabberdctl"}, {copy, "inetrc", "etc/ejabberd/inetrc"}, {copy, "tools/captcha*.sh", "lib/ejabberd-\{\{release_version\}\}/priv/bin/"}, {copy, "rel/files/install_upgrade.escript", "bin/install_upgrade.escript"}]} ]}. {profiles, [{prod, [{relx, [{debug_info, strip}, {dev_mode, false}, {include_erts, true}, {include_src, true}, {overlay, [{copy, "sql/*", "lib/ejabberd-\{\{release_version\}\}/priv/sql/"}, {copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg"}, {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml"}]}]}]}, {dev, [{post_hooks, [{release, "rel/setup-dev.sh"}]}, {relx, [{debug_info, keep}, {dev_mode, true}, {include_erts, true}, {include_src, false}, {overlay, [{copy, "ejabberdctl.cfg.example", "etc/ejabberd/ejabberdctl.cfg.example"}, {copy, "ejabberd.yml.example", "etc/ejabberd/ejabberd.yml.example"}, {copy, "test/ejabberd_SUITE_data/ca.pem", "etc/ejabberd/"}, {copy, "test/ejabberd_SUITE_data/cert.pem", "etc/ejabberd/"}]}]}]}, {test, [{erl_opts, [nowarn_export_all]}]}]}. %% Local Variables: %% mode: erlang %% End: %% vim: set filetype=erlang tabstop=8: ejabberd-21.12/CODE_OF_CONDUCT.md0000644000232200023220000000622414154362354016435 0ustar debalancedebalance# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@process-one.net. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ejabberd-21.12/ejabberd.service.template0000644000232200023220000000077214154362354020572 0ustar debalancedebalance[Unit] Description=XMPP Server After=network.target [Service] Type=notify User=@installuser@ Group=@installuser@ LimitNOFILE=65536 Restart=on-failure RestartSec=5 WatchdogSec=30 ExecStart=@ctlscriptpath@/ejabberdctl foreground ExecStop=/bin/sh -c '@ctlscriptpath@/ejabberdctl stop && @ctlscriptpath@/ejabberdctl stopped' ExecReload=@ctlscriptpath@/ejabberdctl reload_config NotifyAccess=all PrivateDevices=true AmbientCapabilities=CAP_NET_BIND_SERVICE TimeoutSec=300 [Install] WantedBy=multi-user.target ejabberd-21.12/include/0000755000232200023220000000000014154362354015255 5ustar debalancedebalanceejabberd-21.12/include/adhoc.hrl0000644000232200023220000000316014154362354017042 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(adhoc_request, { lang = <<"">> :: binary(), node = <<"">> :: binary(), sessionid = <<"">> :: binary(), action = <<"">> :: binary(), xdata = false :: false | xmlel(), others = [] :: [xmlel()] }). -record(adhoc_response, { lang = <<"">> :: binary(), node = <<"">> :: binary(), sessionid = <<"">> :: binary(), status :: atom(), defaultaction = <<"">> :: binary(), actions = [] :: [binary()], notes = [] :: [{binary(), binary()}], elements = [] :: [xmlel()] }). -type adhoc_request() :: #adhoc_request{}. -type adhoc_response() :: #adhoc_response{}. ejabberd-21.12/include/mod_privacy.hrl0000644000232200023220000000350714154362354020305 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(privacy, {us = {<<"">>, <<"">>} :: {binary(), binary()}, default = none :: none | binary(), lists = [] :: [{binary(), [listitem()]}]}). -type privacy() :: #privacy{}. -record(listitem, {type = none :: listitem_type(), value = none :: listitem_value(), action = allow :: listitem_action(), order = 0 :: integer(), match_all = false :: boolean(), match_iq = false :: boolean(), match_message = false :: boolean(), match_presence_in = false :: boolean(), match_presence_out = false :: boolean()}). -type listitem() :: #listitem{}. -type listitem_type() :: none | jid | group | subscription. -type listitem_value() :: none | both | from | to | jid:ljid() | binary(). -type listitem_action() :: allow | deny. ejabberd-21.12/include/ELDAPv3.hrl0000644000232200023220000000416614154362354017071 0ustar debalancedebalance%% Generated by the Erlang ASN.1 compiler version:2.0.1 %% Purpose: Erlang record definitions for each named and unnamed %% SEQUENCE and SET, and macro definitions for each value %% definition,in module ELDAPv3 -record('LDAPMessage',{ messageID, protocolOp, controls = asn1_NOVALUE}). -record('AttributeValueAssertion',{ attributeDesc, assertionValue}). -record('Attribute',{ type, vals}). -record('LDAPResult',{ resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE}). -record('Control',{ controlType, criticality = asn1_DEFAULT, controlValue = asn1_NOVALUE}). -record('BindRequest',{ version, name, authentication}). -record('SaslCredentials',{ mechanism, credentials = asn1_NOVALUE}). -record('BindResponse',{ resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, serverSaslCreds = asn1_NOVALUE}). -record('SearchRequest',{ baseObject, scope, derefAliases, sizeLimit, timeLimit, typesOnly, filter, attributes}). -record('SubstringFilter',{ type, substrings}). -record('MatchingRuleAssertion',{ matchingRule = asn1_NOVALUE, type = asn1_NOVALUE, matchValue, dnAttributes = asn1_DEFAULT}). -record('SearchResultEntry',{ objectName, attributes}). -record('PartialAttributeList_SEQOF',{ type, vals}). -record('ModifyRequest',{ object, modification}). -record('ModifyRequest_modification_SEQOF',{ operation, modification}). -record('AttributeTypeAndValues',{ type, vals}). -record('AddRequest',{ entry, attributes}). -record('AttributeList_SEQOF',{ type, vals}). -record('ModifyDNRequest',{ entry, newrdn, deleteoldrdn, newSuperior = asn1_NOVALUE}). -record('CompareRequest',{ entry, ava}). -record('ExtendedRequest',{ requestName, requestValue = asn1_NOVALUE}). -record('ExtendedResponse',{ resultCode, matchedDN, errorMessage, referral = asn1_NOVALUE, responseName = asn1_NOVALUE, response = asn1_NOVALUE}). -record('PasswdModifyRequestValue',{ userIdentity = asn1_NOVALUE, oldPasswd = asn1_NOVALUE, newPasswd = asn1_NOVALUE}). -record('PasswdModifyResponseValue',{ genPasswd = asn1_NOVALUE}). -define('maxInt', 2147483647). -define('passwdModifyOID', [49,46,51,46,54,46,49,46,52,46,49,46,52,50,48,51,46,49,46,49,49,46,49]). ejabberd-21.12/include/mqtt.hrl0000644000232200023220000002136014154362354016753 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% @author Evgeny Khramtsov %%% @copyright (C) 2002-2021 ProcessOne, SARL. All Rights Reserved. %%% %%% Licensed under the Apache License, Version 2.0 (the "License"); %%% you may not use this file except in compliance with the License. %%% You may obtain a copy of the License at %%% %%% http://www.apache.org/licenses/LICENSE-2.0 %%% %%% Unless required by applicable law or agreed to in writing, software %%% distributed under the License is distributed on an "AS IS" BASIS, %%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %%% See the License for the specific language governing permissions and %%% limitations under the License. %%% %%%------------------------------------------------------------------- -define(MQTT_VERSION_4, 4). -define(MQTT_VERSION_5, 5). -record(connect, {proto_level = 4 :: non_neg_integer(), will :: undefined | publish(), clean_start = true :: boolean(), keep_alive = 0 :: non_neg_integer(), client_id = <<>> :: binary(), username = <<>> :: binary(), password = <<>> :: binary(), will_properties = #{} :: properties(), properties = #{} :: properties()}). -record(connack, {session_present = false :: boolean(), code = success :: reason_code(), properties = #{} :: properties()}). -record(publish, {id :: undefined | non_neg_integer(), dup = false :: boolean(), qos = 0 :: qos(), retain = false :: boolean(), topic :: binary(), payload :: binary(), properties = #{} :: properties(), meta = #{} :: map()}). -record(puback, {id :: non_neg_integer(), code = success :: reason_code(), properties = #{} :: properties()}). -record(pubrec, {id :: non_neg_integer(), code = success :: reason_code(), properties = #{} :: properties()}). -record(pubrel, {id :: non_neg_integer(), code = success :: reason_code(), properties = #{} :: properties(), meta = #{} :: map()}). -record(pubcomp, {id :: non_neg_integer(), code = success :: reason_code(), properties = #{} :: properties()}). -record(subscribe, {id :: non_neg_integer(), filters :: [{binary(), sub_opts()}], properties = #{} :: properties(), meta = #{} :: map()}). -record(suback, {id :: non_neg_integer(), codes = [] :: [char() | reason_code()], properties = #{} :: properties()}). -record(unsubscribe, {id :: non_neg_integer(), filters :: [binary()], properties = #{} :: properties(), meta = #{} :: map()}). -record(unsuback, {id :: non_neg_integer(), codes = [] :: [reason_code()], properties = #{} :: properties()}). -record(pingreq, {meta = #{} :: map()}). -record(pingresp, {}). -record(disconnect, {code = 'normal-disconnection' :: reason_code(), properties = #{} :: properties()}). -record(auth, {code = success :: reason_code(), properties = #{} :: properties()}). -record(sub_opts, {qos = 0 :: qos(), no_local = false :: boolean(), retain_as_published = false :: boolean(), retain_handling = 0 :: 0..2}). -type qos() :: 0|1|2. -type sub_opts() :: #sub_opts{}. -type utf8_pair() :: {binary(), binary()}. -type properties() :: #{assigned_client_identifier => binary(), authentication_data => binary(), authentication_method => binary(), content_type => binary(), correlation_data => binary(), maximum_packet_size => pos_integer(), maximum_qos => 0|1, message_expiry_interval => non_neg_integer(), payload_format_indicator => binary | utf8, reason_string => binary(), receive_maximum => pos_integer(), request_problem_information => boolean(), request_response_information => boolean(), response_information => binary(), response_topic => binary(), retain_available => boolean(), server_keep_alive => non_neg_integer(), server_reference => binary(), session_expiry_interval => non_neg_integer(), shared_subscription_available => boolean(), subscription_identifier => [non_neg_integer()] | non_neg_integer(), subscription_identifiers_available => boolean(), topic_alias => pos_integer(), topic_alias_maximum => non_neg_integer(), user_property => [utf8_pair()], wildcard_subscription_available => boolean(), will_delay_interval => non_neg_integer()}. -type property() :: assigned_client_identifier | authentication_data | authentication_method | content_type | correlation_data | maximum_packet_size | maximum_qos | message_expiry_interval | payload_format_indicator | reason_string | receive_maximum | request_problem_information | request_response_information | response_information | response_topic | retain_available | server_keep_alive | server_reference | session_expiry_interval | shared_subscription_available | subscription_identifier | subscription_identifiers_available | topic_alias | topic_alias_maximum | user_property | wildcard_subscription_available | will_delay_interval. -type reason_code() :: 'success' | 'normal-disconnection' | 'granted-qos-0' | 'granted-qos-1' | 'granted-qos-2' | 'disconnect-with-will-message' | 'no-matching-subscribers' | 'no-subscription-existed' | 'continue-authentication' | 're-authenticate' | 'unspecified-error' | 'malformed-packet' | 'protocol-error' | 'implementation-specific-error' | 'unsupported-protocol-version' | 'client-identifier-not-valid' | 'bad-user-name-or-password' | 'not-authorized' | 'server-unavailable' | 'server-busy' | 'banned' | 'server-shutting-down' | 'bad-authentication-method' | 'keep-alive-timeout' | 'session-taken-over' | 'topic-filter-invalid' | 'topic-name-invalid' | 'packet-identifier-in-use' | 'packet-identifier-not-found' | 'receive-maximum-exceeded' | 'topic-alias-invalid' | 'packet-too-large' | 'message-rate-too-high' | 'quota-exceeded' | 'administrative-action' | 'payload-format-invalid' | 'retain-not-supported' | 'qos-not-supported' | 'use-another-server' | 'server-moved' | 'shared-subscriptions-not-supported' | 'connection-rate-exceeded' | 'maximum-connect-time' | 'subscription-identifiers-not-supported' | 'wildcard-subscriptions-not-supported'. -type connect() :: #connect{}. -type connack() :: #connack{}. -type publish() :: #publish{}. -type puback() :: #puback{}. -type pubrel() :: #pubrel{}. -type pubrec() :: #pubrec{}. -type pubcomp() :: #pubcomp{}. -type subscribe() :: #subscribe{}. -type suback() :: #suback{}. -type unsubscribe() :: #unsubscribe{}. -type unsuback() :: #unsuback{}. -type pingreq() :: #pingreq{}. -type pingresp() :: #pingresp{}. -type disconnect() :: #disconnect{}. -type auth() :: #auth{}. -type mqtt_packet() :: connect() | connack() | publish() | puback() | pubrel() | pubrec() | pubcomp() | subscribe() | suback() | unsubscribe() | unsuback() | pingreq() | pingresp() | disconnect() | auth(). -type mqtt_version() :: ?MQTT_VERSION_4 | ?MQTT_VERSION_5. ejabberd-21.12/include/http_bind.hrl0000644000232200023220000000321514154362354017740 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). -define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, POST, OPTIONS">>}). -define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type">>}). -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(NO_CACHE, {<<"Cache-Control">>, <<"max-age=0, no-cache, no-store">>}). -define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER, [?CT_XML, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS, ?NO_CACHE]). ejabberd-21.12/include/logger.hrl0000644000232200023220000000433614154362354017251 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(PRINT(Format, Args), io:format(Format, Args)). -ifdef(LAGER). -compile([{parse_transform, lager_transform}]). -define(DEBUG(Format, Args), begin lager:debug(Format, Args), ok end). -define(INFO_MSG(Format, Args), begin lager:info(Format, Args), ok end). -define(WARNING_MSG(Format, Args), begin lager:warning(Format, Args), ok end). -define(ERROR_MSG(Format, Args), begin lager:error(Format, Args), ok end). -define(CRITICAL_MSG(Format, Args), begin lager:critical(Format, Args), ok end). -else. -include_lib("kernel/include/logger.hrl"). -define(DEBUG(Format, Args), begin ?LOG_DEBUG(Format, Args), ok end). -define(INFO_MSG(Format, Args), begin ?LOG_INFO(Format, Args), ok end). -define(WARNING_MSG(Format, Args), begin ?LOG_WARNING(Format, Args), ok end). -define(ERROR_MSG(Format, Args), begin ?LOG_ERROR(Format, Args), ok end). -define(CRITICAL_MSG(Format, Args), begin ?LOG_CRITICAL(Format, Args), ok end). -endif. %% Use only when trying to troubleshoot test problem with ExUnit -define(EXUNIT_LOG(Format, Args), case lists:keyfind(logger, 1, application:loaded_applications()) of false -> ok; _ -> 'Elixir.Logger':bare_log(error, io_lib:format(Format, Args), [?MODULE]) end). %% Uncomment if you want to debug p1_fsm/gen_fsm %%-define(DBGFSM, true). ejabberd-21.12/include/ejabberd_auth.hrl0000644000232200023220000000206714154362354020550 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(passwd, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', password = <<"">> :: binary() | scram() | '_'}). ejabberd-21.12/include/mod_private.hrl0000644000232200023220000000220014154362354020267 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(private_storage, {usns = {<<"">>, <<"">>, <<"">>} :: {binary(), binary(), binary() | '$1' | '_'}, xml = #xmlel{} :: xmlel() | '_' | '$1'}). ejabberd-21.12/include/ejabberd_sql_pt.hrl0000644000232200023220000000177314154362354021114 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -compile([{parse_transform, ejabberd_sql_pt}]). -include("ejabberd_sql.hrl"). ejabberd-21.12/include/ejabberd_sql.hrl0000644000232200023220000000375314154362354020411 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(SQL_MARK, sql__mark_). -define(SQL(SQL), ?SQL_MARK(SQL)). -define(SQL_UPSERT_MARK, sql_upsert__mark_). -define(SQL_UPSERT(Host, Table, Fields), ejabberd_sql:sql_query(Host, ?SQL_UPSERT_MARK(Table, Fields))). -define(SQL_UPSERT_T(Table, Fields), ejabberd_sql:sql_query_t(?SQL_UPSERT_MARK(Table, Fields))). -define(SQL_INSERT_MARK, sql_insert__mark_). -define(SQL_INSERT(Table, Fields), ?SQL_INSERT_MARK(Table, Fields)). -ifdef(COMPILER_REPORTS_ONLY_LINES). -record(sql_query, {hash :: binary(), format_query :: fun(), format_res :: fun(), args :: fun(), loc :: {module(), pos_integer()}}). -else. -record(sql_query, {hash :: binary(), format_query :: fun(), format_res :: fun(), args :: fun(), loc :: {module(), {pos_integer(), pos_integer()}}}). -endif. -record(sql_escape, {string :: fun((binary()) -> binary()), integer :: fun((integer()) -> binary()), boolean :: fun((boolean()) -> binary()), in_array_string :: fun((binary()) -> binary()), like_escape :: fun(() -> binary())}). ejabberd-21.12/include/translate.hrl0000644000232200023220000000002614154362354017757 0ustar debalancedebalance-define(T(S), <>). ejabberd-21.12/include/ejabberd_config.hrl0000644000232200023220000000242214154362354021047 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(local_config, {key :: any(), value :: any()}). -type local_config() :: #local_config{}. -record(state, {opts = [] :: [acl:acl() | local_config()], hosts = [] :: [binary()], override_local = false :: boolean(), override_global = false :: boolean(), override_acls = false :: boolean()}). ejabberd-21.12/include/ejabberd_commands.hrl0000644000232200023220000001217214154362354021406 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -type aterm() :: {atom(), atype()}. -type atype() :: integer | string | binary | {tuple, [aterm()]} | {list, aterm()}. -type rterm() :: {atom(), rtype()}. -type rtype() :: integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. -type oauth_scope() :: atom(). %% ejabberd_commands OAuth ReST ACL definition: %% Two fields exist that are used to control access on a command from ReST API: %% 1. Policy %% If policy is: %% - restricted: command is not exposed as OAuth Rest API. %% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access %% - user: Command might be called by any server user. %% - open: Command can be called by anyone. %% %% Policy is just used to control who can call the command. A specific additional access rules can be performed, as %% defined by access option. %% Access option can be a list of: %% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command. %% - AccessRule name: direct name of the access rule to check in config file. %% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter %% to command, so that the command can perform additional check. -record(ejabberd_commands, {name :: atom(), tags = [] :: [atom()] | '_' | '$2', desc = "" :: string() | '_' | '$3', longdesc = "" :: string() | '_', version = 0 :: integer(), note = "" :: string(), weight = 1 :: integer(), module :: atom() | '_', function :: atom() | '_', args = [] :: [aterm()] | '_' | '$1' | '$2', policy = restricted :: open | restricted | admin | user, %% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}] access = [] :: [{atom(),atom(),atom()}|atom()], definer = unknown :: atom(), result = {res, rescode} :: rterm() | '_' | '$2', args_rename = [] :: [{atom(),atom()}], args_desc = none :: none | [string()] | '_', result_desc = none :: none | string() | '_', args_example = none :: none | [any()] | '_', result_example = none :: any()}). %% TODO Fix me: Type is not up to date -type ejabberd_commands() :: #ejabberd_commands{name :: atom(), tags :: [atom()], desc :: string(), longdesc :: string(), version :: integer(), module :: atom(), function :: atom(), args :: [aterm()], policy :: open | restricted | admin | user, access :: [{atom(),atom(),atom()}|atom()], result :: rterm()}. %% @type ejabberd_commands() = #ejabberd_commands{ %% name = atom(), %% tags = [atom()], %% desc = string(), %% longdesc = string(), %% module = atom(), %% function = atom(), %% args = [aterm()], %% result = rterm() %% }. %% desc: Description of the command %% args: Describe the accepted arguments. %% This way the function that calls the command can format the %% arguments before calling. %% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}. %% Allowed types for arguments are integer, string, tuple and list. %% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple. %% A rtype is either an atom or a tuple with two elements. %% @type aterm() = {Name::atom(), Type::atype()}. %% An argument term is a tuple with the term name and the term type. %% @type rterm() = {Name::atom(), Type::rtype()}. %% A result term is a tuple with the term name and the term type. ejabberd-21.12/include/bosh.hrl0000644000232200023220000000323214154362354016717 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(CT_XML, {<<"Content-Type">>, <<"text/xml; charset=utf-8">>}). -define(CT_PLAIN, {<<"Content-Type">>, <<"text/plain">>}). -define(CT_JSON, {<<"Content-Type">>, <<"application/json">>}). -define(AC_ALLOW_ORIGIN, {<<"Access-Control-Allow-Origin">>, <<"*">>}). -define(AC_ALLOW_METHODS, {<<"Access-Control-Allow-Methods">>, <<"GET, POST, OPTIONS">>}). -define(AC_ALLOW_HEADERS, {<<"Access-Control-Allow-Headers">>, <<"Content-Type">>}). -define(AC_MAX_AGE, {<<"Access-Control-Max-Age">>, <<"86400">>}). -define(OPTIONS_HEADER, [?CT_PLAIN, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_METHODS, ?AC_ALLOW_HEADERS, ?AC_MAX_AGE]). -define(HEADER(CType), [CType, ?AC_ALLOW_ORIGIN, ?AC_ALLOW_HEADERS]). -define(BOSH_CACHE, bosh_cache). ejabberd-21.12/include/ejabberd_router.hrl0000644000232200023220000000035114154362354021121 0ustar debalancedebalance-define(ROUTES_CACHE, routes_cache). -type local_hint() :: integer() | {apply, atom(), atom()}. -record(route, {domain :: binary(), server_host :: binary(), pid :: undefined | pid(), local_hint :: local_hint() | undefined}). ejabberd-21.12/include/ejabberd_http.hrl0000644000232200023220000000464714154362354020574 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(request, {method :: method(), path = [] :: [binary()], raw_path :: binary(), q = [] :: [{binary() | nokey, binary()}], us = {<<>>, <<>>} :: {binary(), binary()}, auth :: {binary(), binary()} | {oauth, binary(), []} | undefined | invalid, lang = <<"">> :: binary(), data = <<"">> :: binary(), ip :: {inet:ip_address(), inet:port_number()}, host = <<"">> :: binary(), port = 5280 :: inet:port_number(), opts = [] :: list(), tp = http :: protocol(), headers = [] :: [{atom() | binary(), binary()}], length = 0 :: non_neg_integer(), sockmod :: gen_tcp | fast_tls, socket :: inet:socket() | fast_tls:tls_socket()}). -record(ws, {socket :: inet:socket() | fast_tls:tls_socket(), sockmod = gen_tcp :: gen_tcp | fast_tls, ip :: {inet:ip_address(), inet:port_number()}, host = <<"">> :: binary(), port = 5280 :: inet:port_number(), path = [] :: [binary()], headers = [] :: [{atom() | binary(), binary()}], local_path = [] :: [binary()], q = [] :: [{binary() | nokey, binary()}], buf :: binary(), http_opts = [] :: list()}). -type method() :: 'GET' | 'HEAD' | 'DELETE' | 'OPTIONS' | 'PUT' | 'POST' | 'TRACE' | 'PATCH'. -type protocol() :: http | https. -type http_request() :: #request{}. ejabberd-21.12/include/mod_vcard_xupdate.hrl0000644000232200023220000000205214154362354021453 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(vcard_xupdate, {us = {<<>>, <<>>} :: {binary(), binary()}, hash = <<>> :: binary()}). ejabberd-21.12/include/ejabberd_oauth.hrl0000644000232200023220000000270614154362354020727 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(oauth_token, { token = <<"">> :: binary() | '_', us = {<<"">>, <<"">>} :: {binary(), binary()} | '_', scope = [] :: [binary()] | '_', expire :: integer() | '$1' | '_' }). -record(oauth_client, { client_id = <<"">> :: binary() | '_', client_name = <<"">> :: binary() | '_', grant_type :: password | implicit | '_', options :: [any()] | '_' }). ejabberd-21.12/include/mod_muc_room.hrl0000644000232200023220000001377514154362354020460 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(MAX_USERS_DEFAULT, 200). -define(SETS, gb_sets). -record(lqueue, { queue = p1_queue:new() :: p1_queue:queue(lqueue_elem()), max = 0 :: integer() }). -type lqueue() :: #lqueue{}. -type lqueue_elem() :: {binary(), message(), boolean(), erlang:timestamp(), non_neg_integer()}. -record(config, { title = <<"">> :: binary(), description = <<"">> :: binary(), allow_change_subj = true :: boolean(), allow_query_users = true :: boolean(), allow_private_messages = true :: boolean(), allow_private_messages_from_visitors = anyone :: anyone | moderators | nobody , allow_visitor_status = true :: boolean(), allow_visitor_nickchange = true :: boolean(), public = true :: boolean(), public_list = true :: boolean(), persistent = false :: boolean(), moderated = true :: boolean(), captcha_protected = false :: boolean(), members_by_default = true :: boolean(), members_only = false :: boolean(), allow_user_invites = false :: boolean(), allow_subscription = false :: boolean(), password_protected = false :: boolean(), password = <<"">> :: binary(), anonymous = true :: boolean(), presence_broadcast = [moderator, participant, visitor] :: [moderator | participant | visitor], allow_voice_requests = true :: boolean(), voice_request_min_interval = 1800 :: non_neg_integer(), max_users = ?MAX_USERS_DEFAULT :: non_neg_integer() | none, logging = false :: boolean(), vcard = <<"">> :: binary(), vcard_xupdate = undefined :: undefined | external | binary(), captcha_whitelist = (?SETS):empty() :: gb_sets:set(), mam = false :: boolean(), pubsub = <<"">> :: binary(), enable_hats = false :: boolean(), lang = ejabberd_option:language() :: binary() }). -type config() :: #config{}. -type role() :: moderator | participant | visitor | none. -type affiliation() :: admin | member | outcast | owner | none. -record(user, { jid :: jid(), nick :: binary(), role :: role(), %%is_subscriber = false :: boolean(), %%subscriptions = [] :: [binary()], last_presence :: presence() | undefined }). -record(subscriber, {jid :: jid(), nick = <<>> :: binary(), nodes = [] :: [binary()]}). -record(muc_subscribers, {subscribers = #{} :: subscribers(), subscriber_nicks = #{} :: subscriber_nicks(), subscriber_nodes = #{} :: subscriber_nodes() }). -type subscribers() :: #{ljid() => #subscriber{}}. -type subscriber_nicks() :: #{binary() => [ljid()]}. -type subscriber_nodes() :: #{binary() => subscribers()}. -record(activity, { message_time = 0 :: integer(), presence_time = 0 :: integer(), message_shaper = none :: ejabberd_shaper:shaper(), presence_shaper = none :: ejabberd_shaper:shaper(), message :: message() | undefined, presence :: {binary(), presence()} | undefined }). -record(state, { room = <<"">> :: binary(), host = <<"">> :: binary(), server_host = <<"">> :: binary(), access = {none,none,none,none,none} :: {atom(), atom(), atom(), atom(), atom()}, jid = #jid{} :: jid(), config = #config{} :: config(), users = #{} :: users(), muc_subscribers = #muc_subscribers{} :: #muc_subscribers{}, last_voice_request_time = treap:empty() :: treap:treap(), robots = #{} :: robots(), nicks = #{} :: nicks(), affiliations = #{} :: affiliations(), history = #lqueue{} :: lqueue(), subject = [] :: [text()], subject_author = <<"">> :: binary(), hats_users = #{} :: map(), % FIXME on OTP 21+: #{ljid() => #{binary() => binary()}}, just_created = erlang:system_time(microsecond) :: true | integer(), activity = treap:empty() :: treap:treap(), room_shaper = none :: ejabberd_shaper:shaper(), room_queue :: p1_queue:queue({message | presence, jid()}) | undefined, hibernate_timer = none :: reference() | none | hibernating }). -type users() :: #{ljid() => #user{}}. -type robots() :: #{jid() => {binary(), stanza()}}. -type nicks() :: #{binary() => [ljid()]}. -type affiliations() :: #{ljid() => affiliation() | {affiliation(), binary()}}. ejabberd-21.12/include/mod_push.hrl0000644000232200023220000000241714154362354017606 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% ejabberd, Copyright (C) 2017-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(push_session, {us = {<<"">>, <<"">>} :: {binary(), binary()}, timestamp = erlang:timestamp() :: erlang:timestamp(), service = {<<"">>, <<"">>, <<"">>} :: ljid(), node = <<"">> :: binary(), xml :: undefined | xmlel()}). ejabberd-21.12/include/mod_shared_roster.hrl0000644000232200023220000000231214154362354021465 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(sr_group, {group_host = {<<"">>, <<"">>} :: {'$1' | binary(), '$2' | binary()}, opts = [] :: list() | '_' | '$2'}). -record(sr_user, {us = {<<"">>, <<"">>} :: {binary(), binary()}, group_host = {<<"">>, <<"">>} :: {binary(), binary()}}). ejabberd-21.12/include/mod_proxy65.hrl0000644000232200023220000000351114154362354020157 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% RFC 1928 constants. %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %% Version -define(VERSION_5, 5). %% Authentication methods -define(AUTH_ANONYMOUS, 0). -define(AUTH_GSSAPI, 1). -define(AUTH_PLAIN, 2). %% Address Type -define(AUTH_NO_METHODS, 255). -define(ATYP_IPV4, 1). -define(ATYP_DOMAINNAME, 3). -define(ATYP_IPV6, 4). %% Commands -define(CMD_CONNECT, 1). -define(CMD_BIND, 2). -define(CMD_UDP, 3). %% RFC 1928 replies -define(SUCCESS, 0). -define(ERR_GENERAL_FAILURE, 1). -define(ERR_NOT_ALLOWED, 2). -define(ERR_NETWORK_UNREACHABLE, 3). -define(ERR_HOST_UNREACHABLE, 4). -define(ERR_CONNECTION_REFUSED, 5). -define(ERR_TTL_EXPIRED, 6). -define(ERR_COMMAND_NOT_SUPPORTED, 7). -define(ERR_ADDRESS_TYPE_NOT_SUPPORTED, 8). %% RFC 1928 defined timeout. -define(SOCKS5_REPLY_TIMEOUT, 10000). -record(s5_request, {rsv = 0 :: integer(), cmd = connect :: connect | udp, sha1 = <<"">> :: binary()}). ejabberd-21.12/include/eldap.hrl0000644000232200023220000000652014154362354017054 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(LDAP_PORT, 389). -define(LDAPS_PORT, 636). -type scope() :: baseObject | singleLevel | wholeSubtree. -record(eldap_search, {scope = wholeSubtree :: scope(), base = <<"">> :: binary(), filter :: eldap:filter() | undefined, limit = 0 :: non_neg_integer(), attributes = [] :: [binary()], types_only = false :: boolean(), deref_aliases = neverDerefAliases :: neverDerefAliases | derefInSearching | derefFindingBaseObj | derefAlways, timeout = 0 :: non_neg_integer()}). -record(eldap_search_result, {entries = [] :: [eldap_entry()], referrals = [] :: list()}). -record(eldap_entry, {object_name = <<>> :: binary(), attributes = [] :: [{binary(), [binary()]}]}). -type tlsopts() :: [{encrypt, tls | starttls | none} | {tls_certfile, binary() | undefined} | {tls_cacertfile, binary() | undefined} | {tls_depth, non_neg_integer() | undefined} | {tls_verify, hard | soft | false}]. -record(eldap_config, {servers = [] :: [binary()], backups = [] :: [binary()], tls_options = [] :: tlsopts(), port = ?LDAP_PORT :: inet:port_number(), dn = <<"">> :: binary(), password = <<"">> :: binary(), base = <<"">> :: binary(), deref_aliases = never :: never | searching | finding | always}). -type eldap_config() :: #eldap_config{}. -type eldap_search() :: #eldap_search{}. -type eldap_entry() :: #eldap_entry{}. -define(eldap_config(M, H), #eldap_config{ servers = M:ldap_servers(H), backups = M:ldap_backups(H), tls_options = [{encrypt, M:ldap_encrypt(H)}, {tls_verify, M:ldap_tls_verify(H)}, {tls_certfile, M:ldap_tls_certfile(H)}, {tls_cacertfile, M:ldap_tls_cacertfile(H)}, {tls_depth, M:ldap_tls_depth(H)}], port = M:ldap_port(H), dn = M:ldap_rootdn(H), password = M:ldap_password(H), base = M:ldap_base(H), deref_aliases = M:ldap_deref_aliases(H)}). ejabberd-21.12/include/ejabberd_ctl.hrl0000644000232200023220000000203414154362354020363 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(STATUS_SUCCESS, 0). -define(STATUS_ERROR, 1). -define(STATUS_USAGE, 2). -define(STATUS_BADRPC, 3). ejabberd-21.12/include/mod_muc.hrl0000644000232200023220000000317414154362354017414 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(muc_room, {name_host = {<<"">>, <<"">>} :: {binary(), binary()} | {'_', binary()}, opts = [] :: list() | '_'}). -record(muc_registered, {us_host = {{<<"">>, <<"">>}, <<"">>} :: {{binary(), binary()}, binary()} | '$1', nick = <<"">> :: binary()}). -record(muc_online_room, {name_host :: {binary(), binary()} | '$1' | {'_', binary()} | '_', pid :: pid() | '$2' | '_' | '$1'}). -record(muc_online_users, {us :: {binary(), binary()}, resource :: binary() | '_', room :: binary() | '_' | '$1', host :: binary() | '_' | '$2'}). ejabberd-21.12/include/mod_roster.hrl0000644000232200023220000000341014154362354020137 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(roster, { usj = {<<>>, <<>>, {<<>>, <<>>, <<>>}} :: {binary(), binary(), jid:ljid()} | '_', us = {<<>>, <<>>} :: {binary(), binary()} | '_', jid = {<<>>, <<>>, <<>>} :: jid:ljid(), name = <<>> :: binary() | '_', subscription = none :: subscription() | '_', ask = none :: ask() | '_', groups = [] :: [binary()] | '_', askmessage = <<"">> :: binary() | '_', xs = [] :: [fxml:xmlel()] | '_' }). -record(roster_version, { us = {<<>>, <<>>} :: {binary(), binary()}, version = <<>> :: binary() }). -type ask() :: none | in | out | both | subscribe | unsubscribe. -type subscription() :: none | both | from | to | remove. ejabberd-21.12/include/mod_last.hrl0000644000232200023220000000215014154362354017564 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(last_activity, {us = {<<"">>, <<"">>} :: {binary(), binary()}, timestamp = 0 :: non_neg_integer(), status = <<"">> :: binary()}). ejabberd-21.12/include/ejabberd_stacktrace.hrl0000644000232200023220000000223614154362354021731 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -ifdef(DEPRECATED_GET_STACKTRACE). -define(EX_RULE(Class, Reason, Stack), Class:Reason:Stack). -define(EX_STACK(Stack), Stack). -else. -define(EX_RULE(Class, Reason, _), Class:Reason). -define(EX_STACK(_), erlang:get_stacktrace()). -endif. ejabberd-21.12/include/mod_announce.hrl0000644000232200023220000000220414154362354020427 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(motd, {server = <<"">> :: binary(), packet = #xmlel{} :: xmlel()}). -record(motd_users, {us = {<<"">>, <<"">>} :: {binary(), binary()} | '$1', dummy = [] :: [] | '_'}). ejabberd-21.12/include/mod_mam.hrl0000644000232200023220000000313714154362354017401 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(archive_msg, {us = {<<"">>, <<"">>} :: {binary(), binary()}, id = <<>> :: binary(), timestamp = erlang:timestamp() :: erlang:timestamp(), peer = {<<"">>, <<"">>, <<"">>} :: ljid() | undefined, bare_peer = {<<"">>, <<"">>, <<"">>} :: ljid(), packet = #xmlel{} :: xmlel() | message(), nick = <<"">> :: binary(), type = chat :: chat | groupchat}). -record(archive_prefs, {us = {<<"">>, <<"">>} :: {binary(), binary()}, default = never :: never | always | roster, always = [] :: [ljid()], never = [] :: [ljid()]}). ejabberd-21.12/include/ejabberd_sm.hrl0000644000232200023220000000272014154362354020222 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -ifndef(EJABBERD_SM_HRL). -define(EJABBERD_SM_HRL, true). -define(SM_CACHE, sm_cache). -record(session, {sid, usr, us, priority, info = []}). -record(session_counter, {vhost, count}). -type sid() :: {erlang:timestamp(), pid()}. -type ip() :: {inet:ip_address(), inet:port_number()} | undefined. -type info() :: [{conn, atom()} | {ip, ip()} | {node, atom()} | {oor, boolean()} | {auth_module, atom()} | {num_stanzas_in, non_neg_integer()} | {atom(), term()}]. -type prio() :: undefined | integer(). -endif. ejabberd-21.12/include/mod_caps.hrl0000644000232200023220000000207714154362354017557 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(caps_features, {node_pair = {<<"">>, <<"">>} :: {binary(), binary()}, features = [] :: [binary()] | pos_integer() }). ejabberd-21.12/include/pubsub.hrl0000644000232200023220000001260414154362354017267 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %% ------------------------------- %% Pubsub constants -define(ERR_EXTENDED(E, C), mod_pubsub:extended_error(E, C)). %% The actual limit can be configured with mod_pubsub's option max_items_node -define(MAXITEMS, 1000). %% this is currently a hard limit. %% Would be nice to have it configurable. -define(MAX_PAYLOAD_SIZE, 250000). %% ------------------------------- %% Pubsub types -type(hostPubsub() :: binary()). %%

hostPubsub is the name of the PubSub service. For example, it can be %% "pubsub.localhost".

-type(hostPEP() :: {binary(), binary(), <<>>}). %% @type hostPEP() = {User, Server, Resource} %% User = string() %% Server = string() %% Resource = []. %%

For example, it can be : %% ```{"bob", "example.org", []}'''.

-type(host() :: hostPubsub() | hostPEP()). %% @type host() = hostPubsub() | hostPEP(). -type(nodeId() :: binary()). %% @type nodeId() = binary(). %%

A node is defined by a list of its ancestors. The last element is the name %% of the current node. For example: %% ```<<"/home/localhost/user">>'''

-type(nodeIdx() :: pos_integer() | binary()). %% @type nodeIdx() = integer() | binary(). %% note: pos_integer() should always be used, but we allow anything else coded %% as binary, so one can have a custom implementation of nodetree with custom %% indexing (see nodetree_virtual). this also allows to use any kind of key for %% indexing nodes, as this can be useful with external backends such as sql. -type(itemId() :: binary()). %% @type itemId() = string(). -type(subId() :: binary()). %% @type subId() = string(). -type(nodeOption() :: {Option::atom(), Value::atom() | [binary()] | boolean() | non_neg_integer() }). -type(nodeOptions() :: [mod_pubsub:nodeOption(),...]). %% @type nodeOption() = {Option, Value} %% Option = atom() %% Value = term(). %% Example: %% ```{deliver_payloads, true}''' -type(subOption() :: {Option::atom(), Value::binary() | [binary()] | boolean() }). -type(subOptions() :: [mod_pubsub:subOption()]). -type(pubOption() :: {Option::binary(), Values::[binary()] }). -type(pubOptions() :: [mod_pubsub:pubOption()]). -type(affiliation() :: 'none' | 'owner' | 'publisher' | 'publish_only' | 'member' | 'outcast' ). %% @type affiliation() = 'none' | 'owner' | 'publisher' | 'publish-only' | 'member' | 'outcast'. -type(accessModel() :: 'open' | 'presence' | 'roster' | 'authorize' | 'whitelist' ). %% @type accessModel() = 'open' | 'presence' | 'roster' | 'authorize' | 'whitelist'. -type(publishModel() :: 'publishers' | 'subscribers' | 'open' ). %% @type publishModel() = 'publishers' | 'subscribers' | 'open' -record(pubsub_index, { index :: atom(), last :: mod_pubsub:nodeIdx(), free :: [mod_pubsub:nodeIdx()] }). -record(pubsub_node, { nodeid ,% :: {mod_pubsub:host(), mod_pubsub:nodeId()}, id ,% :: mod_pubsub:nodeIdx(), parents = [] ,% :: [mod_pubsub:nodeId(),...], type = <<"flat">>,% :: binary(), owners = [] ,% :: [jid:ljid(),...], options = [] % :: mod_pubsub:nodeOptions() }). -record(pubsub_state, { stateid ,% :: {jid:ljid(), mod_pubsub:nodeIdx()}, nodeidx ,% :: mod_pubsub:nodeIdx(), items = [] ,% :: [mod_pubsub:itemId(),...], affiliation = 'none',% :: mod_pubsub:affiliation(), subscriptions = [] % :: [{mod_pubsub:subscription(), mod_pubsub:subId()}] }). -record(pubsub_item, { itemid ,% :: {mod_pubsub:itemId(), mod_pubsub:nodeIdx()}, nodeidx ,% :: mod_pubsub:nodeIdx(), creation = {unknown, unknown},% :: {erlang:timestamp(), jid:ljid()}, modification = {unknown, unknown},% :: {erlang:timestamp(), jid:ljid()}, payload = [] % :: mod_pubsub:payload() }). -record(pubsub_subscription, { subid ,% :: mod_pubsub:subId(), options = [] % :: mod_pubsub:subOptions() }). -record(pubsub_last_item, { nodeid ,% :: {binary(), mod_pubsub:nodeIdx()}, itemid ,% :: mod_pubsub:itemId(), creation ,% :: {erlang:timestamp(), jid:ljid()}, payload % :: mod_pubsub:payload() }). -record(pubsub_orphan, { nodeid ,% :: mod_pubsub:nodeIdx(), items = [] % :: list() }). ejabberd-21.12/include/mod_offline.hrl0000644000232200023220000000252414154362354020250 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(offline_msg, {us = {<<"">>, <<"">>} :: {binary(), binary()}, timestamp :: erlang:timestamp() | '_' | undefined, expire :: erlang:timestamp() | never | undefined | '_', from = #jid{} :: jid() | '_', to = #jid{} :: jid() | '_', packet = #xmlel{} :: xmlel() | message() | '_'}). -record(state, {host = <<"">> :: binary(), access_max_offline_messages}). ejabberd-21.12/include/mod_vcard.hrl0000644000232200023220000000241414154362354017723 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -record(vcard_search, {us, user, luser, fn, lfn, family, lfamily, given, lgiven, middle, lmiddle, nickname, lnickname, bday, lbday, ctry, lctry, locality, llocality, email, lemail, orgname, lorgname, orgunit, lorgunit}). -record(vcard, {us = {<<"">>, <<"">>} :: {binary(), binary()} | binary(), vcard = #xmlel{} :: xmlel()}). ejabberd-21.12/include/ejabberd_web_admin.hrl0000644000232200023220000000660114154362354021532 0ustar debalancedebalance%%%---------------------------------------------------------------------- %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- -define(X(Name), #xmlel{name = Name, attrs = [], children = []}). -define(XA(Name, Attrs), #xmlel{name = Name, attrs = Attrs, children = []}). -define(XE(Name, Els), #xmlel{name = Name, attrs = [], children = Els}). -define(XAE(Name, Attrs, Els), #xmlel{name = Name, attrs = Attrs, children = Els}). -define(C(Text), {xmlcdata, Text}). -define(XC(Name, Text), ?XE(Name, [?C(Text)])). -define(XAC(Name, Attrs, Text), ?XAE(Name, Attrs, [?C(Text)])). -define(CT(Text), ?C((translate:translate(Lang, Text)))). -define(XCT(Name, Text), ?XC(Name, (translate:translate(Lang, Text)))). -define(XACT(Name, Attrs, Text), ?XAC(Name, Attrs, (translate:translate(Lang, Text)))). -define(LI(Els), ?XE(<<"li">>, Els)). -define(A(URL, Els), ?XAE(<<"a">>, [{<<"href">>, URL}], Els)). -define(AC(URL, Text), ?A(URL, [?C(Text)])). -define(ACT(URL, Text), ?AC(URL, (translate:translate(Lang, Text)))). -define(P, ?X(<<"p">>)). -define(BR, ?X(<<"br">>)). -define(INPUT(Type, Name, Value), ?XA(<<"input">>, [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}])). -define(INPUTT(Type, Name, Value), ?INPUT(Type, Name, (translate:translate(Lang, Value)))). -define(INPUTD(Type, Name, Value), ?XA(<<"input">>, [{<<"type">>, Type}, {<<"name">>, Name}, {<<"class">>, <<"btn-danger">>}, {<<"value">>, Value}])). -define(INPUTTD(Type, Name, Value), ?INPUTD(Type, Name, (translate:translate(Lang, Value)))). -define(INPUTS(Type, Name, Value, Size), ?XA(<<"input">>, [{<<"type">>, Type}, {<<"name">>, Name}, {<<"value">>, Value}, {<<"size">>, Size}])). -define(INPUTST(Type, Name, Value, Size), ?INPUT(Type, Name, (translate:translate(Lang, Value)), Size)). -define(ACLINPUT(Text), ?XE(<<"td">>, [?INPUT(<<"text">>, <<"value", ID/binary>>, Text)])). -define(TEXTAREA(Name, Rows, Cols, Value), ?XAC(<<"textarea">>, [{<<"name">>, Name}, {<<"rows">>, Rows}, {<<"cols">>, Cols}], Value)). %% Build an xmlelement for result -define(XRES(Text), ?XAC(<<"p">>, [{<<"class">>, <<"result">>}], Text)). %% Guide Link -define(XREST(Text), ?XRES((translate:translate(Lang, Text)))). -define(GL(Ref, Title), ?XAE(<<"div">>, [{<<"class">>, <<"guidelink">>}], [?XAE(<<"a">>, [{<<"href">>, <<"https://docs.ejabberd.im/admin/configuration/", Ref/binary>>}, {<<"target">>, <<"_blank">>}], [?C(<<"docs: ", Title/binary>>)])])). %% h1 with a Guide Link -define(H1GL(Name, Ref, Title), [?XC(<<"h1">>, Name), ?GL(Ref, Title)]). ejabberd-21.12/mix.exs0000644000232200023220000002422314154362354015153 0ustar debalancedebalancedefmodule Ejabberd.MixProject do use Mix.Project def project do [app: :ejabberd, version: version(), description: description(), elixir: "~> 1.4", elixirc_paths: ["lib"], compile_path: ".", compilers: [:asn1] ++ Mix.compilers, erlc_options: erlc_options(), erlc_paths: ["asn1", "src"], # Elixir tests are starting the part of ejabberd they need aliases: [test: "test --no-start"], start_permanent: Mix.env() == :prod, language: :erlang, releases: releases(), package: package(), deps: deps()] end def version do case config(:vsn) do :false -> "0.0.0" # ./configure wasn't run: vars.config not created '0.0' -> "0.0.0" # the full git repository wasn't downloaded 'latest.0' -> "0.0.0" # running 'docker-ejabberd/ecs/build.sh latest' [_, _, ?., _, _] = x -> head = String.replace(:erlang.list_to_binary(x), ~r/0+([0-9])/, "\\1") <> vsn -> String.replace(:erlang.list_to_binary(vsn), ~r/0+([0-9])/, "\\1") end end def description do """ Robust, Ubiquitous and Massively Scalable Messaging Platform (XMPP, MQTT, SIP Server) """ end def application do [mod: {:ejabberd_app, []}, extra_applications: [:mix], applications: [:idna, :inets, :kernel, :sasl, :ssl, :stdlib, :base64url, :fast_tls, :fast_xml, :fast_yaml, :jiffy, :jose, :p1_utils, :stringprep, :stun, :yconf], included_applications: [:lager, :mnesia, :os_mon, :cache_tab, :eimp, :esip, :mqtree, :p1_acme, :p1_oauth2, :pkix, :xmpp] ++ cond_apps()] end defp if_function_exported(mod, fun, arity, okResult) do :code.ensure_loaded(mod) if :erlang.function_exported(mod, fun, arity) do okResult else [] end end defp if_version_above(ver, okResult) do if :erlang.system_info(:otp_release) > ver do okResult else [] end end defp if_version_below(ver, okResult) do if :erlang.system_info(:otp_release) < ver do okResult else [] end end defp erlc_options do # Use our own includes + includes from all dependencies includes = ["include"] ++ deps_include(["fast_xml", "xmpp", "p1_utils"]) result = [:debug_info, {:d, :ELIXIR_ENABLED}] ++ cond_options() ++ Enum.map(includes, fn (path) -> {:i, path} end) ++ if_version_above('20', [{:d, :DEPRECATED_GET_STACKTRACE}]) ++ if_version_below('21', [{:d, :USE_OLD_HTTP_URI}]) ++ if_version_below('22', [{:d, :LAGER}]) ++ if_version_below('23', [{:d, :USE_OLD_CRYPTO_HMAC}]) ++ if_version_below('23', [{:d, :USE_OLD_PG2}]) ++ if_version_below('24', [{:d, :COMPILER_REPORTS_ONLY_LINES}]) ++ if_version_below('24', [{:d, :SYSTOOLS_APP_DEF_WITHOUT_OPTIONAL}]) ++ if_function_exported(:uri_string, :normalize, 1, [{:d, :HAVE_URI_STRING}]) if_function_exported(:erl_error, :format_exception, 6, [{:d, :HAVE_ERL_ERROR}]) defines = for {:d, value} <- result, do: {:d, value} result ++ [{:d, :ALL_DEFS, defines}] end defp cond_options do for {:true, option} <- [{config(:sip), {:d, :SIP}}, {config(:stun), {:d, :STUN}}, {config(:roster_gateway_workaround), {:d, :ROSTER_GATWAY_WORKAROUND}}, {config(:new_sql_schema), {:d, :NEW_SQL_SCHEMA}} ], do: option end defp deps do [{:base64url, "~> 1.0"}, {:cache_tab, "~> 1.0"}, {:distillery, "~> 2.0"}, {:eimp, "~> 1.0"}, {:esip, "~> 1.0"}, {:ex_doc, ">= 0.0.0", only: :dev}, {:fast_tls, "~> 1.1"}, {:fast_xml, "~> 1.1"}, {:fast_yaml, "~> 1.0"}, {:idna, "~> 6.0"}, {:jiffy, "~> 1.0.5"}, {:jose, "~> 1.11.1"}, {:lager, "~> 3.9.1"}, {:mqtree, "~> 1.0"}, {:p1_acme, "~> 1.0"}, {:p1_mysql, "~> 1.0"}, {:p1_oauth2, "~> 0.6"}, {:p1_pgsql, "~> 1.1"}, {:p1_utils, "~> 1.0"}, {:pkix, "~> 1.0"}, {:stringprep, ">= 1.0.26"}, {:stun, "~> 1.0"}, {:xmpp, "~> 1.5"}, {:yconf, "~> 1.0"}] ++ cond_deps() end defp deps_include(deps) do base = if Mix.Project.umbrella?() do "../../deps" else case Mix.Project.deps_paths()[:ejabberd] do nil -> "deps" _ -> ".." end end Enum.map(deps, fn dep -> base<>"/#{dep}/include" end) end defp cond_deps do for {:true, dep} <- [{config(:pam), {:epam, "~> 1.0"}}, {config(:redis), {:eredis, "~> 1.2.0"}}, {config(:zlib), {:ezlib, "~> 1.0"}}, {config(:lua), {:luerl, "~> 0.3.1"}}, {config(:sqlite), {:sqlite3, "~> 1.1"}}], do: dep end defp cond_apps do for {:true, app} <- [{config(:redis), :eredis}, {config(:mysql), :p1_mysql}, {config(:odbc), :odbc}, {config(:pgsql), :p1_pgsql}, {config(:sqlite), :sqlite3}], do: app end defp package do [# These are the default files included in the package files: ["include", "lib", "priv", "sql", "src", "COPYING", "README.md", "mix.exs", "rebar.config", "rebar.config.script", "vars.config"], maintainers: ["ProcessOne"], licenses: ["GPLv2"], links: %{"Site" => "https://www.ejabberd.im", "Documentation" => "http://docs.ejabberd.im", "Source" => "https://github.com/processone/ejabberd", "ProcessOne" => "http://www.process-one.net/"}] end defp vars do case :file.consult("vars.config") do {:ok,config} -> config _ -> [zlib: true] end end defp config(key) do case vars()[key] do nil -> false value -> value end end defp releases do maybe_tar = case Mix.env() do :prod -> [:tar] _ -> [] end [ ejabberd: [ include_executables_for: [:unix], # applications: [runtime_tools: :permanent] steps: [©_extra_files/1, :assemble | maybe_tar] ] ] end defp copy_extra_files(release) do assigns = [ version: version(), rootdir: config(:rootdir), installuser: config(:installuser), libdir: config(:libdir), sysconfdir: config(:sysconfdir), localstatedir: config(:localstatedir), docdir: config(:docdir), erl: config(:erl), epmd: config(:epmd), bindir: Path.join([config(:release_dir), "releases", version()]), release_dir: config(:release_dir), erts_vsn: "erts-#{release.erts_version}" ] ro = "rel/overlays" File.rm_rf(ro) # Elixir lower than 1.12.0 don't have System.shell execute = fn(command) -> case function_exported?(System, :shell, 1) do true -> System.shell(command) false -> :os.cmd(to_charlist(command)) end end # Mix/Elixir lower than 1.11.0 use config/releases.exs instead of runtime.exs case Version.match?(System.version, ">= 1.11.0") do true -> :ok false -> execute.("cp config/runtime.exs config/releases.exs") end execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.template > ejabberdctl.example1") Mix.Generator.copy_template("ejabberdctl.example1", "ejabberdctl.example2", assigns) execute.("sed -e 's|{{\\(\[_a-z\]*\\)}}|<%= @\\1 %>|g' ejabberdctl.example2 > ejabberdctl.example3") execute.("sed -e 's|ERLANG_NODE=ejabberd@localhost|ERLANG_NODE=ejabberd|g' ejabberdctl.example3 > ejabberdctl.example4") execute.("sed -e 's|INSTALLUSER=|ERL_OPTIONS=\"-setcookie \\$\\(cat \"\\${SCRIPT_DIR%/*}/releases/COOKIE\")\"\\nINSTALLUSER=|g' ejabberdctl.example4 > ejabberdctl.example5") Mix.Generator.copy_template("ejabberdctl.example5", "#{ro}/bin/ejabberdctl", assigns) File.chmod("#{ro}/bin/ejabberdctl", 0o755) File.rm("ejabberdctl.example1") File.rm("ejabberdctl.example2") File.rm("ejabberdctl.example3") File.rm("ejabberdctl.example4") File.rm("ejabberdctl.example5") suffix = case Mix.env() do :dev -> Mix.Generator.copy_file("test/ejabberd_SUITE_data/ca.pem", "#{ro}/etc/ejabberd/ca.pem") Mix.Generator.copy_file("test/ejabberd_SUITE_data/cert.pem", "#{ro}/etc/ejabberd/cert.pem") ".example" _ -> "" end Mix.Generator.copy_file("ejabberd.yml.example", "#{ro}/etc/ejabberd/ejabberd.yml#{suffix}") Mix.Generator.copy_file("ejabberdctl.cfg.example", "#{ro}/etc/ejabberd/ejabberdctl.cfg#{suffix}") Mix.Generator.copy_file("inetrc", "#{ro}/etc/ejabberd/inetrc") Mix.Generator.copy_template("rel/vm.args.mix", "#{ro}/etc/ejabberd/vm.args", assigns) Enum.each(File.ls!("sql"), fn x -> Mix.Generator.copy_file("sql/#{x}", "#{ro}/lib/ejabberd-#{release.version}/priv/sql/#{x}") end) Mix.Generator.create_directory("#{ro}/var/lib/ejabberd") case Mix.env() do :dev -> execute.("REL_DIR_TEMP=$PWD/rel/overlays/ rel/setup-dev.sh") _ -> :ok end release end end defmodule Mix.Tasks.Compile.Asn1 do use Mix.Task alias Mix.Compilers.Erlang @recursive true @manifest ".compile.asn1" def run(args) do {opts, _, _} = OptionParser.parse(args, switches: [force: :boolean]) project = Mix.Project.config source_paths = project[:asn1_paths] || ["asn1"] dest_paths = project[:asn1_target] || ["src"] mappings = Enum.zip(source_paths, dest_paths) options = project[:asn1_options] || [] force = case opts[:force] do :true -> [force: true] _ -> [force: false] end Erlang.compile(manifest(), mappings, :asn1, :erl, force, fn input, output -> options = options ++ [:noobj, outdir: Erlang.to_erl_file(Path.dirname(output))] case :asn1ct.compile(Erlang.to_erl_file(input), options) do :ok -> {:ok, :done} error -> error end end) end def manifests, do: [manifest()] defp manifest, do: Path.join(Mix.Project.manifest_path, @manifest) def clean, do: Erlang.clean(manifest()) end ejabberd-21.12/config/0000755000232200023220000000000014154362354015077 5ustar debalancedebalanceejabberd-21.12/config/runtime.exs0000644000232200023220000000054514154362354017307 0ustar debalancedebalanceimport Config rootpath = System.get_env("RELEASE_ROOT", "") # This is standard path in the context of ejabberd release config :ejabberd, file: Path.join(rootpath, "etc/ejabberd/ejabberd.yml"), log_path: Path.join(rootpath, 'var/log/ejabberd/ejabberd.log') # Customize Mnesia directory: config :mnesia, dir: Path.join(rootpath, 'var/lib/ejabberd/') ejabberd-21.12/config/ejabberd.exs0000644000232200023220000000627714154362354017372 0ustar debalancedebalancedefmodule Ejabberd.ConfigFile do use Ejabberd.Config def start do [loglevel: 4, log_rotate_size: 10485760, log_rotate_count: 1, 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_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 [ queue_chat_states: true, queue_presence: false] end module :mod_configure do end module :mod_disco 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 ejabberd-21.12/install-sh0000644000232200023220000003253714154362354015645 0ustar debalancedebalance#!/bin/sh # install - install a program, script, or datafile scriptversion=2009-04-28.21; # UTC # This originates from X11R5 (mit/util/scripts/install.sh), which was # later released in X11R6 (xc/config/util/install.sh) with the # following copyright and license. # # Copyright (C) 1994 X Consortium # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- # TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Except as contained in this notice, the name of the X Consortium shall not # be used in advertising or otherwise to promote the sale, use or other deal- # ings in this Software without prior written authorization from the X Consor- # tium. # # # FSF changes to this file are in the public domain. # # Calling this script install-sh is preferred over install.sh, to prevent # `make' implicit rules from creating a file called install from it # when there is no Makefile. # # This script is compatible with the BSD install script, but was written # from scratch. nl=' ' IFS=" "" $nl" # set DOITPROG to echo to test this script # Don't use :- since 4.3BSD and earlier shells don't like it. doit=${DOITPROG-} if test -z "$doit"; then doit_exec=exec else doit_exec=$doit fi # Put in absolute file names if you don't have them in your path; # or use environment vars. chgrpprog=${CHGRPPROG-chgrp} chmodprog=${CHMODPROG-chmod} chownprog=${CHOWNPROG-chown} cmpprog=${CMPPROG-cmp} cpprog=${CPPROG-cp} mkdirprog=${MKDIRPROG-mkdir} mvprog=${MVPROG-mv} rmprog=${RMPROG-rm} stripprog=${STRIPPROG-strip} posix_glob='?' initialize_posix_glob=' test "$posix_glob" != "?" || { if (set -f) 2>/dev/null; then posix_glob= else posix_glob=: fi } ' posix_mkdir= # Desired mode of installed file. mode=0755 chgrpcmd= chmodcmd=$chmodprog chowncmd= mvcmd=$mvprog rmcmd="$rmprog -f" stripcmd= src= dst= dir_arg= dst_arg= copy_on_change=false no_target_directory= usage="\ Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE or: $0 [OPTION]... SRCFILES... DIRECTORY or: $0 [OPTION]... -t DIRECTORY SRCFILES... or: $0 [OPTION]... -d DIRECTORIES... In the 1st form, copy SRCFILE to DSTFILE. In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. In the 4th, create DIRECTORIES. Options: --help display this help and exit. --version display version info and exit. -c (ignored) -C install only if different (preserve the last data modification time) -d create directories instead of installing files. -g GROUP $chgrpprog installed files to GROUP. -m MODE $chmodprog installed files to MODE. -o USER $chownprog installed files to USER. -s $stripprog installed files. -t DIRECTORY install into DIRECTORY. -T report an error if DSTFILE is a directory. Environment variables override the default commands: CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG RMPROG STRIPPROG " while test $# -ne 0; do case $1 in -c) ;; -C) copy_on_change=true;; -d) dir_arg=true;; -g) chgrpcmd="$chgrpprog $2" shift;; --help) echo "$usage"; exit $?;; -m) mode=$2 case $mode in *' '* | *' '* | *' '* | *'*'* | *'?'* | *'['*) echo "$0: invalid mode: $mode" >&2 exit 1;; esac shift;; -o) chowncmd="$chownprog $2" shift;; -s) stripcmd=$stripprog;; -t) dst_arg=$2 shift;; -T) no_target_directory=true;; --version) echo "$0 $scriptversion"; exit $?;; --) shift break;; -*) echo "$0: invalid option: $1" >&2 exit 1;; *) break;; esac shift done if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then # When -d is used, all remaining arguments are directories to create. # When -t is used, the destination is already specified. # Otherwise, the last argument is the destination. Remove it from $@. for arg do if test -n "$dst_arg"; then # $@ is not empty: it contains at least $arg. set fnord "$@" "$dst_arg" shift # fnord fi shift # arg dst_arg=$arg done fi if test $# -eq 0; then if test -z "$dir_arg"; then echo "$0: no input file specified." >&2 exit 1 fi # It's OK to call `install-sh -d' without argument. # This can happen when creating conditional directories. exit 0 fi if test -z "$dir_arg"; then trap '(exit $?); exit' 1 2 13 15 # Set umask so as not to create temps with too-generous modes. # However, 'strip' requires both read and write access to temps. case $mode in # Optimize common cases. *644) cp_umask=133;; *755) cp_umask=22;; *[0-7]) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw='% 200' fi cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; *) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw=,u+rw fi cp_umask=$mode$u_plus_rw;; esac fi for src do # Protect names starting with `-'. case $src in -*) src=./$src;; esac if test -n "$dir_arg"; then dst=$src dstdir=$dst test -d "$dstdir" dstdir_status=$? else # Waiting for this to be detected by the "$cpprog $src $dsttmp" command # might cause directories to be created, which would be especially bad # if $src (and thus $dsttmp) contains '*'. if test ! -f "$src" && test ! -d "$src"; then echo "$0: $src does not exist." >&2 exit 1 fi if test -z "$dst_arg"; then echo "$0: no destination specified." >&2 exit 1 fi dst=$dst_arg # Protect names starting with `-'. case $dst in -*) dst=./$dst;; esac # If destination is a directory, append the input filename; won't work # if double slashes aren't ignored. if test -d "$dst"; then if test -n "$no_target_directory"; then echo "$0: $dst_arg: Is a directory" >&2 exit 1 fi dstdir=$dst dst=$dstdir/`basename "$src"` dstdir_status=0 else # Prefer dirname, but fall back on a substitute if dirname fails. dstdir=` (dirname "$dst") 2>/dev/null || expr X"$dst" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$dst" : 'X\(//\)[^/]' \| \ X"$dst" : 'X\(//\)$' \| \ X"$dst" : 'X\(/\)' \| . 2>/dev/null || echo X"$dst" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q' ` test -d "$dstdir" dstdir_status=$? fi fi obsolete_mkdir_used=false if test $dstdir_status != 0; then case $posix_mkdir in '') # Create intermediate dirs using mode 755 as modified by the umask. # This is like FreeBSD 'install' as of 1997-10-28. umask=`umask` case $stripcmd.$umask in # Optimize common cases. *[2367][2367]) mkdir_umask=$umask;; .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; *[0-7]) mkdir_umask=`expr $umask + 22 \ - $umask % 100 % 40 + $umask % 20 \ - $umask % 10 % 4 + $umask % 2 `;; *) mkdir_umask=$umask,go-w;; esac # With -d, create the new directory with the user-specified mode. # Otherwise, rely on $mkdir_umask. if test -n "$dir_arg"; then mkdir_mode=-m$mode else mkdir_mode= fi posix_mkdir=false case $umask in *[123567][0-7][0-7]) # POSIX mkdir -p sets u+wx bits regardless of umask, which # is incompatible with FreeBSD 'install' when (umask & 300) != 0. ;; *) tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ trap 'ret=$?; rmdir "$tmpdir/d" "$tmpdir" 2>/dev/null; exit $ret' 0 if (umask $mkdir_umask && exec $mkdirprog $mkdir_mode -p -- "$tmpdir/d") >/dev/null 2>&1 then if test -z "$dir_arg" || { # Check for POSIX incompatibilities with -m. # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or # other-writeable bit of parent directory when it shouldn't. # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. ls_ld_tmpdir=`ls -ld "$tmpdir"` case $ls_ld_tmpdir in d????-?r-*) different_mode=700;; d????-?--*) different_mode=755;; *) false;; esac && $mkdirprog -m$different_mode -p -- "$tmpdir" && { ls_ld_tmpdir_1=`ls -ld "$tmpdir"` test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" } } then posix_mkdir=: fi rmdir "$tmpdir/d" "$tmpdir" else # Remove any dirs left behind by ancient mkdir implementations. rmdir ./$mkdir_mode ./-p ./-- 2>/dev/null fi trap '' 0;; esac;; esac if $posix_mkdir && ( umask $mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" ) then : else # The umask is ridiculous, or mkdir does not conform to POSIX, # or it failed possibly due to a race condition. Create the # directory the slow way, step by step, checking for races as we go. case $dstdir in /*) prefix='/';; -*) prefix='./';; *) prefix='';; esac eval "$initialize_posix_glob" oIFS=$IFS IFS=/ $posix_glob set -f set fnord $dstdir shift $posix_glob set +f IFS=$oIFS prefixes= for d do test -z "$d" && continue prefix=$prefix$d if test -d "$prefix"; then prefixes= else if $posix_mkdir; then (umask=$mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break # Don't fail if two instances are running concurrently. test -d "$prefix" || exit 1 else case $prefix in *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; *) qprefix=$prefix;; esac prefixes="$prefixes '$qprefix'" fi fi prefix=$prefix/ done if test -n "$prefixes"; then # Don't fail if two instances are running concurrently. (umask $mkdir_umask && eval "\$doit_exec \$mkdirprog $prefixes") || test -d "$dstdir" || exit 1 obsolete_mkdir_used=true fi fi fi if test -n "$dir_arg"; then { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 else # Make a couple of temp file names in the proper directory. dsttmp=$dstdir/_inst.$$_ rmtmp=$dstdir/_rm.$$_ # Trap to clean up those temp files at exit. trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 # Copy the file name to the temp name. (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && # and set any options; do chmod last to preserve setuid bits. # # If any of these fail, we abort the whole thing. If we want to # ignore errors from any of these, just make sure not to ignore # errors from the above "$doit $cpprog $src $dsttmp" command. # { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && # If -C, don't bother to copy if it wouldn't change the file. if $copy_on_change && old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && eval "$initialize_posix_glob" && $posix_glob set -f && set X $old && old=:$2:$4:$5:$6 && set X $new && new=:$2:$4:$5:$6 && $posix_glob set +f && test "$old" = "$new" && $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 then rm -f "$dsttmp" else # Rename the file to the real destination. $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || # The rename failed, perhaps because mv can't rename something else # to itself, or perhaps because mv is so ancient that it does not # support -f. { # Now remove or move aside any old file at destination location. # We try this two ways since rm can't unlink itself on some # systems and the destination file might be busy for other # reasons. In this case, the final cleanup might fail but the new # file should still install successfully. { test ! -f "$dst" || $doit $rmcmd -f "$dst" 2>/dev/null || { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } } || { echo "$0: cannot unlink or rename $dst" >&2 (exit 1); exit 1 } } && # Now rename the file to the real destination. $doit $mvcmd "$dsttmp" "$dst" } fi || exit 1 trap '' 0 fi done # Local variables: # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "scriptversion=" # time-stamp-format: "%:y-%02m-%02d.%02H" # time-stamp-time-zone: "UTC" # time-stamp-end: "; # UTC" # End: ejabberd-21.12/test/0000755000232200023220000000000014154362354014611 5ustar debalancedebalanceejabberd-21.12/test/docker/0000755000232200023220000000000014154362354016060 5ustar debalancedebalanceejabberd-21.12/test/docker/README.md0000644000232200023220000000273314154362354017344 0ustar debalancedebalance# Docker database images to run ejabberd tests ## Starting databases You can start the Docker environment with Docker Compose, from ejabberd repository root. The following command will launch MySQL, MSSQL, PostgreSQL, Redis and keep the console attached to it. ``` mkdir test/docker/db/mysql/data mkdir test/docker/db/postgres/data (cd test/docker; docker-compose up) ``` You can stop all the databases with CTRL-C. ## Creating database for MSSQL The following commands will create the necessary login, user and database, will grant rights on the database in MSSQL and create the ejabberd schema: ``` docker exec ejabberd-mssql /opt/mssql-tools/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -i /initdb_mssql.sql docker exec ejabberd-mssql /opt/mssql-tools/bin/sqlcmd -U SA -P ejabberd_Test1 -S localhost -i /mssql.sql ``` ## Running tests Before running the test, you can ensure there is no running instance of Erlang common test tool. You can run the following command, especially if all test are skipped with an `eaddrinuse` error: ``` pkill -9 ct_run ``` You can run tests with (from ejabberd repository root): ``` make test ``` ## Cleaning up the test environment You can fully clean up the environment with: ``` (cd test/docker; docker-compose down) ``` If you want to clean the data, you can remove the data volumes after the `docker-compose down` command: ``` docker volume rm ejabberd-mysqldata docker volume rm ejabberd-mssqldata docker volume rm ejabberd-pgsqldata ``` ejabberd-21.12/test/docker/db/0000755000232200023220000000000014154362354016445 5ustar debalancedebalanceejabberd-21.12/test/docker/db/mssql/0000755000232200023220000000000014154362354017604 5ustar debalancedebalanceejabberd-21.12/test/docker/db/mssql/initdb/0000755000232200023220000000000014154362354021055 5ustar debalancedebalanceejabberd-21.12/test/docker/db/mssql/initdb/initdb_mssql.sql0000644000232200023220000000060314154362354024265 0ustar debalancedebalanceUSE [master] GO IF DB_ID('ejabberd_test') IS NOT NULL set noexec on -- prevent creation when already exists CREATE DATABASE ejabberd_test; GO USE ejabberd_test; GO CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1'; GO CREATE USER ejabberd_test FOR LOGIN ejabberd_test; GO GRANT ALL TO ejabberd_test; GO GRANT CONTROL ON SCHEMA ::dbo TO ejabberd_test; GO ejabberd-21.12/test/docker/docker-compose.yml0000644000232200023220000000263214154362354021520 0ustar debalancedebalanceversion: '3.7' services: mysql: image: mysql:latest container_name: ejabberd-mysql volumes: - mysqldata:/var/lib/mysql - ../../sql/mysql.sql:/docker-entrypoint-initdb.d/mysql.sql:ro command: --default-authentication-plugin=mysql_native_password restart: always ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: ejabberd_test MYSQL_USER: ejabberd_test MYSQL_PASSWORD: ejabberd_test mssql: image: mcr.microsoft.com/mssql/server container_name: ejabberd-mssql volumes: - mssqldata:/var/opt/mssql - ./db/mssql/initdb/initdb_mssql.sql:/initdb_mssql.sql:ro - ../../sql/mssql.sql:/mssql.sql:ro restart: always ports: - 1433:1433 environment: ACCEPT_EULA: Y SA_PASSWORD: ejabberd_Test1 postgres: image: postgres:latest container_name: ejabberd-postgres volumes: - pgsqldata:/var/lib/postgresql/data - ../../sql/pg.sql:/docker-entrypoint-initdb.d/pg.sql:ro ports: - 5432:5432 environment: POSTGRES_PASSWORD: ejabberd_test POSTGRES_USER: ejabberd_test POSTGRES_DB: ejabberd_test redis: image: redis:latest container_name: ejabberd-redis ports: - 6379:6379 volumes: mysqldata: name: ejabberd-mysqldata mssqldata: name: ejabberd-mssqldata pgsqldata: name: ejabberd-pgsqldata ejabberd-21.12/test/webadmin_tests.erl0000644000232200023220000001310614154362354020326 0ustar debalancedebalance%%%------------------------------------------------------------------- %%% Author : Pawel Chmielowski %%% Created : 23 Mar 2020 by Pawel Chmielowski %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(webadmin_tests). %% API -compile(export_all). -import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, my_jid/1, wait_for_slave/1, wait_for_master/1, send_recv/2, put_event/2, get_event/1]). -include("suite.hrl"). -include_lib("stdlib/include/assert.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {webadmin_single, [sequence], [single_test(login_page), single_test(welcome_page), single_test(user_page), single_test(adduser), single_test(changepassword), single_test(removeuser)]}. login_page(Config) -> Headers = ?match({ok, {{"HTTP/1.1", 401, _}, Headers, _}}, httpc:request(get, {page(Config, ""), []}, [], [{body_format, binary}]), Headers), ?match("basic realm=\"ejabberd\"", proplists:get_value("www-authenticate", Headers, none)). welcome_page(Config) -> Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, httpc:request(get, {page(Config, ""), [basic_auth_header(Config)]}, [], [{body_format, binary}]), Body), ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). user_page(Config) -> Server = ?config(server, Config), URL = "server/" ++ binary_to_list(Server) ++ "/user/admin/", Body = ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, httpc:request(get, {page(Config, URL), [basic_auth_header(Config)]}, [], [{body_format, binary}]), Body), ?match({_, _}, binary:match(Body, <<"ejabberd Web Admin">>)). adduser(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Password = ?config(password, Config), Body = make_query( Config, "server/" ++ binary_to_list(Server) ++ "/users/", <<"newusername=", (mue(User))/binary, "&newuserpassword=", (mue(Password))/binary, "&addnewuser=Add+User">>), Password = ejabberd_auth:get_password(User, Server), ?match({_, _}, binary:match(Body, <<"<a href='../user/">>)). changepassword(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Password = <<"newpassword-", (?config(password, Config))/binary>>, Body = make_query( Config, "server/" ++ binary_to_list(Server) ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", <<"password=", (mue(Password))/binary, "&chpassword=Change+Password">>), ?match(Password, ejabberd_auth:get_password(User, Server)), ?match({_, _}, binary:match(Body, <<"<p class='result'>Submitted</p>">>)). removeuser(Config) -> User = <<"userwebadmin-", (?config(user, Config))/binary>>, Server = ?config(server, Config), Body = make_query( Config, "server/" ++ binary_to_list(Server) ++ "/user/" ++ binary_to_list(mue(User)) ++ "/", <<"password=&removeuser=Remove+User">>), false = ejabberd_auth:user_exists(User, Server), ?match(nomatch, binary:match(Body, <<"<h3>Last Activity</h3>20">>)). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("webadmin_" ++ atom_to_list(T)). basic_auth_header(Config) -> User = <<"admin">>, Server = ?config(server, Config), Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), basic_auth_header(User, Server, Password). basic_auth_header(Username, Server, Password) -> JidBin = <<Username/binary, "@", Server/binary, ":", Password/binary>>, {"authorization", "Basic " ++ base64:encode_to_string(JidBin)}. page(Config, Tail) -> Server = ?config(server_host, Config), Port = ct:get_config(web_port, 5280), Url = "http://" ++ Server ++ ":" ++ integer_to_list(Port) ++ "/admin/" ++ Tail, %% This bypasses a bug introduced in Erlang OTP R21 and fixed in 23.2: case catch uri_string:normalize("/%2525") of "/%25" -> string:replace(Url, "%25", "%2525", all); _ -> Url end. mue(Binary) -> misc:url_encode(Binary). make_query(Config, URL, BodyQ) -> ?match({ok, {{"HTTP/1.1", 200, _}, _, Body}}, httpc:request(post, {page(Config, URL), [basic_auth_header(Config)], "application/x-www-form-urlencoded", BodyQ}, [], [{body_format, binary}]), Body). ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/announce_tests.erl��������������������������������������������������������������0000644�0002322�0002322�00000005734�14154362354�020356� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(announce_tests). %% API -compile(export_all). -import(suite, [server_jid/1, send_recv/2, recv_message/1, disconnect/1, send/2, wait_for_master/1, wait_for_slave/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {announce_single, [sequence], []}. %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {announce_master_slave, [sequence], [master_slave_test(set_motd)]}. set_motd_master(Config) -> ServerJID = server_jid(Config), MotdJID = jid:replace_resource(ServerJID, <<"announce/motd">>), Body = xmpp:mk_text(<<"motd">>), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send(Config, #message{to = MotdJID, body = Body}), #message{from = ServerJID, body = Body} = recv_message(Config), disconnect(Config). set_motd_slave(Config) -> ServerJID = server_jid(Config), Body = xmpp:mk_text(<<"motd">>), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), #message{from = ServerJID, body = Body} = recv_message(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("announce_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("announce_" ++ atom_to_list(T)), [parallel], [list_to_atom("announce_" ++ atom_to_list(T) ++ "_master"), list_to_atom("announce_" ++ atom_to_list(T) ++ "_slave")]}. ������������������������������������ejabberd-21.12/test/stundisco_tests.erl�������������������������������������������������������������0000644�0002322�0002322�00000014315�14154362354�020556� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Created : 22 Apr 2020 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% %%% ejabberd, Copyright (C) 2020-2021 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(stundisco_tests). %% API -compile(export_all). -import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, server_jid/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {stundisco_single, [sequence], [single_test(feature_enabled), single_test(stun_service), single_test(turn_service), single_test(turns_service), single_test(turn_credentials), single_test(turns_credentials)]}. feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_EXTDISCO_2), disconnect(Config). stun_service(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, Port = ct:get_config(stun_port, 3478), Type = stun, Transport = udp, Request = #services{type = Type}, #iq{type = result, sub_els = [#services{ type = undefined, list = [#service{host = Host, port = Port, type = Type, transport = Transport, restricted = false, username = <<>>, password = <<>>, expires = undefined, action = undefined, xdata = undefined}]}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), disconnect(Config). turn_service(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, Port = ct:get_config(stun_port, 3478), Type = turn, Transport = udp, Request = #services{type = Type}, #iq{type = result, sub_els = [#services{ type = undefined, list = [#service{host = Host, port = Port, type = Type, transport = Transport, restricted = true, username = Username, password = Password, expires = Expires, action = undefined, xdata = undefined}]}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). turns_service(Config) -> ServerJID = server_jid(Config), Host = <<"example.com">>, Port = 5349, Type = turns, Transport = tcp, Request = #services{type = Type}, #iq{type = result, sub_els = [#services{ type = undefined, list = [#service{host = Host, port = Port, type = Type, transport = Transport, restricted = true, username = Username, password = Password, expires = Expires, action = undefined, xdata = undefined}]}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). turn_credentials(Config) -> ServerJID = server_jid(Config), Host = {203, 0, 113, 3}, Port = ct:get_config(stun_port, 3478), Type = turn, Transport = udp, Request = #credentials{services = [#service{host = Host, port = Port, type = Type}]}, #iq{type = result, sub_els = [#services{ type = undefined, list = [#service{host = Host, port = Port, type = Type, transport = Transport, restricted = true, username = Username, password = Password, expires = Expires, action = undefined, xdata = undefined}]}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). turns_credentials(Config) -> ServerJID = server_jid(Config), Host = <<"example.com">>, Port = 5349, Type = turns, Transport = tcp, Request = #credentials{services = [#service{host = Host, port = Port, type = Type}]}, #iq{type = result, sub_els = [#services{ type = undefined, list = [#service{host = Host, port = Port, type = Type, transport = Transport, restricted = true, username = Username, password = Password, expires = Expires, action = undefined, xdata = undefined}]}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), true = check_password(Username, Password), true = check_expires(Expires), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("stundisco_" ++ atom_to_list(T)). check_password(Username, Password) -> Secret = <<"cryptic">>, Password == base64:encode(misc:crypto_hmac(sha, Secret, Username)). check_expires({_, _, _} = Expires) -> Now = {MegaSecs, Secs, MicroSecs} = erlang:timestamp(), Later = {MegaSecs + 1, Secs, MicroSecs}, (Expires > Now) and (Expires < Later). �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/upload_tests.erl����������������������������������������������������������������0000644�0002322�0002322�00000015121�14154362354�020023� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 17 May 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(upload_tests). %% API -compile(export_all). -import(suite, [disconnect/1, is_feature_advertised/3, upload_jid/1, my_jid/1, wait_for_slave/1, wait_for_master/1, send_recv/2, put_event/2, get_event/1]). -include("suite.hrl"). -define(CONTENT_TYPE, "image/png"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {upload_single, [sequence], [single_test(feature_enabled), single_test(service_vcard), single_test(get_max_size), single_test(slot_request), single_test(put_get_request), single_test(max_size_exceed)]}. feature_enabled(Config) -> lists:foreach( fun(NS) -> true = is_feature_advertised(Config, NS, upload_jid(Config)) end, namespaces()), disconnect(Config). service_vcard(Config) -> Upload = upload_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(Upload)]), VCard = mod_http_upload_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = send_recv(Config, #iq{type = get, to = Upload, sub_els = [#vcard_temp{}]}), disconnect(Config). get_max_size(Config) -> Xs = get_disco_info_xdata(Config), lists:foreach( fun(NS) -> get_max_size(Config, Xs, NS) end, namespaces()), disconnect(Config). get_max_size(_, _, ?NS_HTTP_UPLOAD_OLD) -> %% This old spec didn't specify 'max-file-size' attribute ok; get_max_size(Config, Xs, NS) -> Xs = get_disco_info_xdata(Config), get_size(NS, Config, Xs). slot_request(Config) -> lists:foreach( fun(NS) -> slot_request(Config, NS) end, namespaces()), disconnect(Config). put_get_request(Config) -> lists:foreach( fun(NS) -> {GetURL, PutURL, _Filename, Size} = slot_request(Config, NS), Data = p1_rand:bytes(Size), put_request(Config, PutURL, Data), get_request(Config, GetURL, Data) end, namespaces()), disconnect(Config). max_size_exceed(Config) -> lists:foreach( fun(NS) -> max_size_exceed(Config, NS) end, namespaces()), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("upload_" ++ atom_to_list(T)). get_disco_info_xdata(Config) -> To = upload_jid(Config), #iq{type = result, sub_els = [#disco_info{xdata = Xs}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}), Xs. get_size(NS, Config, [X|Xs]) -> case xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X) of [NS] -> [Size] = xmpp_util:get_xdata_values(<<"max-file-size">>, X), true = erlang:binary_to_integer(Size) > 0, Size; _ -> get_size(NS, Config, Xs) end; get_size(NS, _Config, []) -> ct:fail({disco_info_xdata_failed, NS}). slot_request(Config, NS) -> To = upload_jid(Config), Filename = filename(), Size = p1_rand:uniform(1, 1024), case NS of ?NS_HTTP_UPLOAD_0 -> #iq{type = result, sub_els = [#upload_slot_0{get = GetURL, put = PutURL, xmlns = NS}]} = send_recv(Config, #iq{type = get, to = To, sub_els = [#upload_request_0{ filename = Filename, size = Size, 'content-type' = <<?CONTENT_TYPE>>, xmlns = NS}]}), {GetURL, PutURL, Filename, Size}; _ -> #iq{type = result, sub_els = [#upload_slot{get = GetURL, put = PutURL, xmlns = NS}]} = send_recv(Config, #iq{type = get, to = To, sub_els = [#upload_request{ filename = Filename, size = Size, 'content-type' = <<?CONTENT_TYPE>>, xmlns = NS}]}), {GetURL, PutURL, Filename, Size} end. put_request(_Config, URL0, Data) -> ct:comment("Putting ~B bytes to ~s", [size(Data), URL0]), URL = binary_to_list(URL0), {ok, {{"HTTP/1.1", 201, _}, _, _}} = httpc:request(put, {URL, [], ?CONTENT_TYPE, Data}, [], []). get_request(_Config, URL0, Data) -> ct:comment("Getting ~B bytes from ~s", [size(Data), URL0]), URL = binary_to_list(URL0), {ok, {{"HTTP/1.1", 200, _}, _, Body}} = httpc:request(get, {URL, []}, [], [{body_format, binary}]), ct:comment("Checking returned body"), Body = Data. max_size_exceed(Config, NS) -> To = upload_jid(Config), Filename = filename(), Size = 1000000000, IQErr = case NS of ?NS_HTTP_UPLOAD_0 -> #iq{type = error} = send_recv(Config, #iq{type = get, to = To, sub_els = [#upload_request_0{ filename = Filename, size = Size, 'content-type' = <<?CONTENT_TYPE>>, xmlns = NS}]}); _ -> #iq{type = error} = send_recv(Config, #iq{type = get, to = To, sub_els = [#upload_request{ filename = Filename, size = Size, 'content-type' = <<?CONTENT_TYPE>>, xmlns = NS}]}) end, check_size_error(IQErr, Size, NS). check_size_error(IQErr, Size, NS) -> Err = xmpp:get_error(IQErr), FileTooLarge = xmpp:get_subtag(Err, #upload_file_too_large{xmlns = NS}), #stanza_error{reason = 'not-acceptable'} = Err, #upload_file_too_large{'max-file-size' = MaxSize} = FileTooLarge, true = Size > MaxSize. namespaces() -> [?NS_HTTP_UPLOAD_0, ?NS_HTTP_UPLOAD, ?NS_HTTP_UPLOAD_OLD]. filename() -> <<(p1_rand:get_string())/binary, ".png">>. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/sm_tests.erl��������������������������������������������������������������������0000644�0002322�0002322�00000016334�14154362354�017165� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(sm_tests). %% API -compile(export_all). -import(suite, [send/2, recv/1, close_socket/1, set_opt/3, my_jid/1, recv_message/1, disconnect/1, send_recv/2, put_event/2, get_event/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {sm_single, [sequence], [single_test(feature_enabled), single_test(enable), single_test(resume), single_test(resume_failed)]}. feature_enabled(Config) -> true = ?config(sm, Config), disconnect(Config). enable(Config) -> Server = ?config(server, Config), ServerJID = jid:make(<<"">>, Server, <<"">>), ct:comment("Send messages of type 'headline' so the server discards them silently"), Msg = #message{to = ServerJID, type = headline, body = [#text{data = <<"body">>}]}, ct:comment("Enable the session management with resumption enabled"), send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), #sm_enabled{id = ID, resume = true} = recv(Config), ct:comment("Initial request; 'h' should be 0"), send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), #sm_a{h = 0} = recv(Config), ct:comment("Sending two messages and requesting again; 'h' should be 3"), send(Config, Msg), send(Config, Msg), send(Config, Msg), send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), #sm_a{h = 3} = recv(Config), ct:comment("Closing socket"), close_socket(Config), {save_config, set_opt(sm_previd, ID, Config)}. resume(Config) -> {_, SMConfig} = ?config(saved_config, Config), ID = ?config(sm_previd, SMConfig), Server = ?config(server, Config), ServerJID = jid:make(<<"">>, Server, <<"">>), MyJID = my_jid(Config), Txt = #text{data = <<"body">>}, Msg = #message{from = ServerJID, to = MyJID, body = [Txt]}, ct:comment("Route message. The message should be queued by the C2S process"), ejabberd_router:route(Msg), ct:comment("Resuming the session"), send(Config, #sm_resume{previd = ID, h = 0, xmlns = ?NS_STREAM_MGMT_3}), #sm_resumed{previd = ID, h = 3} = recv(Config), ct:comment("Receiving unacknowledged stanza"), #message{from = ServerJID, to = MyJID, body = [Txt]} = recv_message(Config), #sm_r{} = recv(Config), send(Config, #sm_a{h = 1, xmlns = ?NS_STREAM_MGMT_3}), ct:comment("Checking if the server counts stanzas correctly"), send(Config, #sm_r{xmlns = ?NS_STREAM_MGMT_3}), #sm_a{h = 3} = recv(Config), ct:comment("Send another stanza to increment the server's 'h' for sm_resume_failed"), send(Config, #presence{to = ServerJID}), ct:comment("Closing socket"), close_socket(Config), {save_config, set_opt(sm_previd, ID, Config)}. resume_failed(Config) -> {_, SMConfig} = ?config(saved_config, Config), ID = ?config(sm_previd, SMConfig), ct:comment("Waiting for the session to time out"), ct:sleep(5000), ct:comment("Trying to resume timed out session"), send(Config, #sm_resume{previd = ID, h = 1, xmlns = ?NS_STREAM_MGMT_3}), #sm_failed{reason = 'item-not-found', h = 4} = recv(Config), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {sm_master_slave, [sequence], [master_slave_test(queue_limit), master_slave_test(queue_limit_detached)]}. queue_limit_master(Config) -> ct:comment("Waiting for 'send' command from the peer"), send = get_event(Config), send_recv_messages(Config), ct:comment("Waiting for peer to disconnect"), peer_down = get_event(Config), disconnect(Config). queue_limit_slave(Config) -> ct:comment("Enable the session management without resumption"), send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3}), #sm_enabled{resume = false} = recv(Config), put_event(Config, send), ct:comment("Receiving all messages"), lists:foreach( fun(I) -> ID = integer_to_binary(I), Body = xmpp:mk_text(ID), #message{id = ID, body = Body} = recv_message(Config) end, lists:seq(1, 11)), ct:comment("Receiving request ACK"), #sm_r{} = recv(Config), ct:comment("Receiving policy-violation stream error"), #stream_error{reason = 'policy-violation'} = recv(Config), {xmlstreamend, <<"stream:stream">>} = recv(Config), ct:comment("Closing socket"), close_socket(Config). queue_limit_detached_master(Config) -> ct:comment("Waiting for the peer to disconnect"), peer_down = get_event(Config), send_recv_messages(Config), disconnect(Config). queue_limit_detached_slave(Config) -> #presence{} = send_recv(Config, #presence{}), ct:comment("Enable the session management with resumption enabled"), send(Config, #sm_enable{resume = true, xmlns = ?NS_STREAM_MGMT_3}), #sm_enabled{resume = true} = recv(Config), ct:comment("Closing socket"), close_socket(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("sm_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("sm_" ++ atom_to_list(T)), [parallel], [list_to_atom("sm_" ++ atom_to_list(T) ++ "_master"), list_to_atom("sm_" ++ atom_to_list(T) ++ "_slave")]}. send_recv_messages(Config) -> PeerJID = ?config(peer, Config), Msg = #message{to = PeerJID}, ct:comment("Sending messages to peer"), lists:foreach( fun(I) -> ID = integer_to_binary(I), send(Config, Msg#message{id = ID, body = xmpp:mk_text(ID)}) end, lists:seq(1, 11)), ct:comment("Receiving bounced messages from the peer"), lists:foreach( fun(I) -> ID = integer_to_binary(I), Err = #message{id = ID, type = error} = recv_message(Config), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) end, lists:seq(1, 11)). ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/offline_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000045761�14154362354�020176� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 7 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(offline_tests). %% API -compile(export_all). -import(suite, [send/2, disconnect/1, my_jid/1, send_recv/2, recv_message/1, get_features/1, recv/1, get_event/1, server_jid/1, wait_for_master/1, wait_for_slave/1, connect/1, open_session/1, bind/1, auth/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== single_cases() -> {offline_single, [sequence], [single_test(feature_enabled), single_test(check_identity), single_test(send_non_existent), single_test(view_non_existent), single_test(remove_non_existent), single_test(view_non_integer), single_test(remove_non_integer), single_test(malformed_iq), single_test(wrong_user), single_test(unsupported_iq)]}. feature_enabled(Config) -> Features = get_features(Config), ct:comment("Checking if offline features are set"), true = lists:member(?NS_FEATURE_MSGOFFLINE, Features), true = lists:member(?NS_FLEX_OFFLINE, Features), disconnect(Config). check_identity(Config) -> #iq{type = result, sub_els = [#disco_info{ node = ?NS_FLEX_OFFLINE, identities = Ids}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{ node = ?NS_FLEX_OFFLINE}]}), true = lists:any( fun(#identity{category = <<"automation">>, type = <<"message-list">>}) -> true; (_) -> false end, Ids), disconnect(Config). send_non_existent(Config) -> Server = ?config(server, Config), To = jid:make(<<"non-existent">>, Server), #message{type = error} = Err = send_recv(Config, #message{to = To}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err), disconnect(Config). view_non_existent(Config) -> #stanza_error{reason = 'item-not-found'} = view(Config, [rand_string()], false), disconnect(Config). remove_non_existent(Config) -> ok = remove(Config, [rand_string()]), disconnect(Config). view_non_integer(Config) -> #stanza_error{reason = 'item-not-found'} = view(Config, [<<"foo">>], false), disconnect(Config). remove_non_integer(Config) -> #stanza_error{reason = 'item-not-found'} = remove(Config, [<<"foo">>]), disconnect(Config). malformed_iq(Config) -> Item = #offline_item{node = rand_string()}, Range = [{Type, SubEl} || Type <- [set, get], SubEl <- [#offline{items = [], _ = false}, #offline{items = [Item], _ = true}]] ++ [{set, #offline{items = [], fetch = true, purge = false}}, {set, #offline{items = [Item], fetch = true, purge = false}}, {get, #offline{items = [], fetch = false, purge = true}}, {get, #offline{items = [Item], fetch = false, purge = true}}], lists:foreach( fun({Type, SubEl}) -> #iq{type = error} = Err = send_recv(Config, #iq{type = Type, sub_els = [SubEl]}), #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err) end, Range), disconnect(Config). wrong_user(Config) -> Server = ?config(server, Config), To = jid:make(<<"foo">>, Server), Item = #offline_item{node = rand_string()}, Range = [{Type, Items, Purge, Fetch} || Type <- [set, get], Items <- [[], [Item]], Purge <- [false, true], Fetch <- [false, true]], lists:foreach( fun({Type, Items, Purge, Fetch}) -> #iq{type = error} = Err = send_recv(Config, #iq{type = Type, to = To, sub_els = [#offline{items = Items, purge = Purge, fetch = Fetch}]}), #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err) end, Range), disconnect(Config). unsupported_iq(Config) -> Item = #offline_item{node = rand_string()}, lists:foreach( fun(Type) -> #iq{type = error} = Err = send_recv(Config, #iq{type = Type, sub_els = [Item]}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) end, [set, get]), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases(DB) -> {offline_master_slave, [sequence], [master_slave_test(flex), master_slave_test(send_all), master_slave_test(from_mam), master_slave_test(mucsub_mam)]}. flex_master(Config) -> send_messages(Config, 5), disconnect(Config). flex_slave(Config) -> wait_for_master(Config), peer_down = get_event(Config), 5 = get_number(Config), Nodes = get_nodes(Config), %% Since headers are received we can send initial presence without a risk %% of getting offline messages flood #presence{} = send_recv(Config, #presence{}), ct:comment("Checking fetch"), Nodes = fetch(Config, lists:seq(1, 5)), ct:comment("Fetching 2nd and 4th message"), [2, 4] = view(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), ct:comment("Deleting 2nd and 4th message"), ok = remove(Config, [lists:nth(2, Nodes), lists:nth(4, Nodes)]), ct:comment("Checking if messages were deleted"), [1, 3, 5] = view(Config, [lists:nth(1, Nodes), lists:nth(3, Nodes), lists:nth(5, Nodes)]), ct:comment("Purging everything left"), ok = purge(Config), ct:comment("Checking if there are no offline messages"), 0 = get_number(Config), clean(disconnect(Config)). from_mam_master(Config) -> C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), C3 = send_all_master(C2), lists:keydelete(mam_enabled, 1, C3). from_mam_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), ok = mam_tests:set_default(Config, always), C2 = lists:keystore(mam_enabled, 1, Config, {mam_enabled, true}), C3 = send_all_slave(C2), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), C4 = lists:keydelete(mam_enabled, 1, C3), mam_tests:clean(C4). mucsub_mam_master(Config) -> Room = suite:muc_room_jid(Config), Peer = ?config(peer, Config), wait_for_slave(Config), ct:comment("Joining muc room"), ok = muc_tests:join_new(Config), ct:comment("Enabling mam in room"), CfgOpts = muc_tests:get_config(Config), %% Find the MAM field in the config ?match(true, proplists:is_defined(mam, CfgOpts)), ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), %% Enable MAM [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), ct:comment("Subscribing peer to room"), ?send_recv(#iq{to = Room, type = set, sub_els = [ #muc_subscribe{jid = Peer, nick = <<"peer">>, events = [?NS_MUCSUB_NODES_MESSAGES]} ]}, #iq{type = result}), ?match(#message{type = groupchat}, send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"1">>)})), ?match(#message{type = groupchat}, send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"2">>), sub_els = [#hint{type = 'no-store'}]})), ?match(#message{type = groupchat}, send_recv(Config, #message{type = groupchat, to = Room, body = xmpp:mk_text(<<"3">>)})), ct:comment("Cleaning up"), suite:put_event(Config, ready), ready = get_event(Config), muc_tests:leave(Config), mam_tests:clean(clean(disconnect(Config))). mucsub_mam_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => true}), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), Room = suite:muc_room_jid(Config), MyJID = my_jid(Config), MyJIDBare = jid:remove_resource(MyJID), ok = mam_tests:set_default(Config, always), #presence{} = send_recv(Config, #presence{}), send(Config, #presence{type = unavailable}), wait_for_master(Config), ready = get_event(Config), ct:sleep(100), ct:comment("Receiving offline messages"), ?match(#presence{}, suite:send_recv(Config, #presence{})), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ #ps_item{} = PS ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) end, [1, 3]), % Unsubscribe yourself ?send_recv(#iq{to = Room, type = set, sub_els = [ #muc_unsubscribe{} ]}, #iq{type = result}), suite:put_event(Config, ready), mam_tests:clean(clean(disconnect(Config))), gen_mod:update_module(Server, mod_offline, #{use_mam_for_storage => false}), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}). send_all_master(Config) -> wait_for_slave(Config), Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), {Deliver, Errors} = message_iterator(Config), N = lists:foldl( fun(#message{type = error} = Msg, Acc) -> send(Config, Msg#message{to = BarePeer}), Acc; (Msg, Acc) -> I = send(Config, Msg#message{to = BarePeer}), case {xmpp:get_subtag(Msg, #offline{}), xmpp:get_subtag(Msg, #xevent{})} of {#offline{}, _} -> ok; {_, #xevent{offline = true, id = undefined}} -> ct:comment("Receiving event-reply for:~n~s", [xmpp:pp(Msg)]), #message{} = Reply = recv_message(Config), #xevent{id = I} = xmpp:get_subtag(Reply, #xevent{}); _ -> ok end, Acc + 1 end, 0, Deliver), lists:foreach( fun(#message{type = headline} = Msg) -> send(Config, Msg#message{to = BarePeer}); (Msg) -> #message{type = error} = Err = send_recv(Config, Msg#message{to = BarePeer}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) end, Errors), ok = wait_for_complete(Config, N), disconnect(Config). send_all_slave(Config) -> ServerJID = server_jid(Config), Peer = ?config(peer, Config), #presence{} = send_recv(Config, #presence{}), send(Config, #presence{type = unavailable}), wait_for_master(Config), peer_down = get_event(Config), #presence{} = send_recv(Config, #presence{}), {Deliver, _Errors} = message_iterator(Config), lists:foreach( fun(#message{type = error}) -> ok; (#message{type = Type, body = Body, subject = Subject} = Msg) -> ct:comment("Receiving message:~n~s", [xmpp:pp(Msg)]), #message{from = Peer, type = Type, body = Body, subject = Subject} = RecvMsg = recv_message(Config), ct:comment("Checking if delay tag is correctly set"), #delay{from = ServerJID} = xmpp:get_subtag(RecvMsg, #delay{}) end, Deliver), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("offline_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("offline_" ++ atom_to_list(T)), [parallel], [list_to_atom("offline_" ++ atom_to_list(T) ++ "_master"), list_to_atom("offline_" ++ atom_to_list(T) ++ "_slave")]}. clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_offline:remove_user(U, S), Config. send_messages(Config, Num) -> send_messages(Config, Num, normal, []). send_messages(Config, Num, Type, SubEls) -> wait_for_slave(Config), Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), lists:foreach( fun(I) -> Body = integer_to_binary(I), send(Config, #message{to = BarePeer, type = Type, body = [#text{data = Body}], subject = [#text{data = <<"subject">>}], sub_els = SubEls}) end, lists:seq(1, Num)), ct:comment("Waiting for all messages to be delivered to offline spool"), ok = wait_for_complete(Config, Num). recv_messages(Config, Num) -> wait_for_master(Config), peer_down = get_event(Config), Peer = ?config(peer, Config), #presence{} = send_recv(Config, #presence{}), lists:foreach( fun(I) -> Text = integer_to_binary(I), #message{sub_els = SubEls, from = Peer, body = [#text{data = Text}], subject = [#text{data = <<"subject">>}]} = recv_message(Config), true = lists:keymember(delay, 1, SubEls) end, lists:seq(1, Num)), clean(disconnect(Config)). get_number(Config) -> ct:comment("Getting offline message number"), #iq{type = result, sub_els = [#disco_info{ node = ?NS_FLEX_OFFLINE, xdata = [X]}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{ node = ?NS_FLEX_OFFLINE}]}), Form = flex_offline:decode(X#xdata.fields), proplists:get_value(number_of_messages, Form). get_nodes(Config) -> MyJID = my_jid(Config), MyBareJID = jid:remove_resource(MyJID), Peer = ?config(peer, Config), Peer_s = jid:encode(Peer), ct:comment("Getting headers"), #iq{type = result, sub_els = [#disco_items{ node = ?NS_FLEX_OFFLINE, items = DiscoItems}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_items{ node = ?NS_FLEX_OFFLINE}]}), ct:comment("Checking if headers are correct"), lists:sort( lists:map( fun(#disco_item{jid = J, name = P, node = N}) when (J == MyBareJID) and (P == Peer_s) -> N end, DiscoItems)). fetch(Config, Range) -> ID = send(Config, #iq{type = get, sub_els = [#offline{fetch = true}]}), Nodes = lists:map( fun(I) -> Text = integer_to_binary(I), #message{body = Body, sub_els = SubEls} = recv(Config), [#text{data = Text}] = Body, #offline{items = [#offline_item{node = Node}]} = lists:keyfind(offline, 1, SubEls), #delay{} = lists:keyfind(delay, 1, SubEls), Node end, Range), #iq{id = ID, type = result, sub_els = []} = recv(Config), Nodes. view(Config, Nodes) -> view(Config, Nodes, true). view(Config, Nodes, NeedReceive) -> Items = lists:map( fun(Node) -> #offline_item{action = view, node = Node} end, Nodes), I = send(Config, #iq{type = get, sub_els = [#offline{items = Items}]}), Range = if NeedReceive -> lists:map( fun(Node) -> #message{body = [#text{data = Text}], sub_els = SubEls} = recv(Config), #offline{items = [#offline_item{node = Node}]} = lists:keyfind(offline, 1, SubEls), binary_to_integer(Text) end, Nodes); true -> [] end, case recv(Config) of #iq{id = I, type = result, sub_els = []} -> Range; #iq{id = I, type = error} = Err -> xmpp:get_error(Err) end. remove(Config, Nodes) -> Items = lists:map( fun(Node) -> #offline_item{action = remove, node = Node} end, Nodes), case send_recv(Config, #iq{type = set, sub_els = [#offline{items = Items}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. purge(Config) -> case send_recv(Config, #iq{type = set, sub_els = [#offline{purge = true}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. wait_for_complete(_Config, 0) -> ok; wait_for_complete(Config, N) -> {U, S, _} = jid:tolower(?config(peer, Config)), lists:foldl( fun(_Time, ok) -> ok; (Time, Acc) -> timer:sleep(Time), case mod_offline:count_offline_messages(U, S) of N -> ok; _ -> Acc end end, error, [0, 100, 200, 2000, 5000, 10000]). xevent_stored(#message{body = [], subject = []}, _) -> false; xevent_stored(#message{type = T}, _) when T /= chat, T /= normal -> false; xevent_stored(_, #xevent{id = undefined}) -> true; xevent_stored(_, #xevent{offline = true}) -> true; xevent_stored(_, #xevent{delivered = true}) -> true; xevent_stored(_, #xevent{displayed = true}) -> true; xevent_stored(_, _) -> false. message_iterator(Config) -> ServerJID = server_jid(Config), ChatStates = [[#chatstate{type = composing}]], Offline = [[#offline{}]], Hints = [[#hint{type = T}] || T <- [store, 'no-store']], XEvent = [[#xevent{id = ID, offline = OfflineFlag}] || ID <- [undefined, rand_string()], OfflineFlag <- [false, true]], Delay = [[#delay{stamp = p1_time_compat:timestamp(), from = ServerJID}]], AllEls = [Els1 ++ Els2 || Els1 <- [[]] ++ ChatStates ++ Delay ++ Hints ++ Offline, Els2 <- [[]] ++ XEvent], All = [#message{type = Type, body = Body, subject = Subject, sub_els = Els} || %%Type <- [chat], Type <- [error, chat, normal, groupchat, headline], Body <- [[], xmpp:mk_text(<<"body">>)], Subject <- [[], xmpp:mk_text(<<"subject">>)], Els <- AllEls], MamEnabled = ?config(mam_enabled, Config) == true, lists:partition( fun(#message{type = error}) -> true; (#message{type = groupchat}) -> false; (#message{sub_els = [#hint{type = store}|_]}) when MamEnabled -> true; (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; (#message{sub_els = [#offline{}|_]}) when not MamEnabled -> false; (#message{sub_els = [#hint{type = store}, #xevent{} = Event | _]} = Msg) when not MamEnabled -> xevent_stored(Msg#message{body = body, type = chat}, Event); (#message{sub_els = [#xevent{} = Event]} = Msg) when not MamEnabled -> xevent_stored(Msg, Event); (#message{sub_els = [_, #xevent{} = Event | _]} = Msg) when not MamEnabled -> xevent_stored(Msg, Event); (#message{sub_els = [#xevent{id = I}]}) when I /= undefined, not MamEnabled -> false; (#message{sub_els = [#hint{type = store}|_]}) -> true; (#message{sub_els = [#hint{type = 'no-store'}|_]}) -> false; (#message{body = [], subject = []}) -> false; (#message{type = Type}) -> (Type == chat) or (Type == normal); (_) -> false end, All). rand_string() -> integer_to_binary(p1_rand:uniform((1 bsl 31)-1)). ���������������ejabberd-21.12/test/ldap_srv.erl��������������������������������������������������������������������0000644�0002322�0002322�00000035473�14154362354�017143� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 21 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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. %%% %%%---------------------------------------------------------------------- %%% Simple LDAP server intended for LDAP modules testing -module(ldap_srv). -behaviour(gen_server). %% API -export([start/1, load_ldif/1, equalityMatch/3, greaterOrEqual/3, lessOrEqual/3, approxMatch/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ELDAPv3.hrl"). -define(INFO_MSG(Fmt, Args), error_logger:info_msg(Fmt, Args)). -define(ERROR_MSG(Fmt, Args), error_logger:error_msg(Fmt, Args)). -define(TCP_SEND_TIMEOUT, 32000). -define(SERVER, ?MODULE). -record(state, {listener = make_ref() :: reference()}). %%%=================================================================== %%% API %%%=================================================================== start(LDIFFile) -> gen_server:start({local, ?SERVER}, ?MODULE, [LDIFFile], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== init([LDIFFile]) -> case gen_tcp:listen(1389, [binary, {packet, asn1}, {active, false}, {reuseaddr, true}, {nodelay, true}, {send_timeout, ?TCP_SEND_TIMEOUT}, {send_timeout_close, true}, {keepalive, true}]) of {ok, ListenSocket} -> case load_ldif(LDIFFile) of {ok, Tree} -> ?INFO_MSG("LDIF tree loaded, " "ready to accept connections at ~B", [1389]), {_Pid, MRef} = spawn_monitor( fun() -> accept(ListenSocket, Tree) end ), {ok, #state{listener = MRef}}; {error, Reason} -> {stop, Reason} end; {error, Reason} = Err -> ?ERROR_MSG("failed to fetch sockname: ~p", [Err]), {stop, Reason} end. handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info({'DOWN', MRef, _Type, _Object, Info}, #state{listener = MRef} = State) -> ?ERROR_MSG("listener died with reason ~p, terminating", [Info]), {stop, normal, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== accept(ListenSocket, Tree) -> case gen_tcp:accept(ListenSocket) of {ok, Socket} -> spawn(fun() -> process(Socket, Tree) end), accept(ListenSocket, Tree); Err -> ?ERROR_MSG("failed to accept: ~p", [Err]), Err end. process(Socket, Tree) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> case 'ELDAPv3':decode('LDAPMessage', B) of {ok, Msg} -> Replies = process_msg(Msg, Tree), Id = Msg#'LDAPMessage'.messageID, lists:foreach( fun(ReplyOp) -> Reply = #'LDAPMessage'{messageID = Id, protocolOp = ReplyOp}, %%?DEBUG("sent:~n~p", [Reply]), {ok, Bytes} = 'ELDAPv3':encode( 'LDAPMessage', Reply), gen_tcp:send(Socket, Bytes) end, Replies), process(Socket, Tree); Err -> ?ERROR_MSG("failed to decode msg: ~p", [Err]), Err end; Err -> Err end. process_msg(#'LDAPMessage'{protocolOp = Op} = _Msg, TopTree) -> %%?DEBUG("got:~n~p", [Msg]), case Op of {bindRequest, #'BindRequest'{name = DN}} -> ResCode = case find_obj(DN, TopTree) of {ok, _} -> success; error -> invalidCredentials %%success end, [{bindResponse, #'BindResponse'{resultCode = ResCode, matchedDN = <<"">>, errorMessage = <<"">>}}]; {searchRequest, #'SearchRequest'{baseObject = DN, scope = Scope, filter = Filter, attributes = Attrs}} -> DNs = process_dn_filter(DN, Scope, Filter, TopTree), Es = lists:map( fun(D) -> make_entry(D, TopTree, Attrs) end, DNs), Es ++ [{searchResDone, #'LDAPResult'{resultCode = success, matchedDN = <<"">>, errorMessage = <<"">>}}]; {extendedReq, _} -> [{extendedResp, #'ExtendedResponse'{matchedDN = <<"">>, errorMessage = <<"Not Implemented">>, resultCode = operationsError}}]; _ -> RespOp = case Op of {modifyRequest, _} -> modifyResponse; {addRequest, _} -> addResponse; {delRequest, _} -> delResponse; {modDNRequest, _} -> modDNResponse; {compareRequest, _} -> compareResponse; _ -> undefined end, case RespOp of undefined -> []; _ -> [{RespOp, #'LDAPResult'{matchedDN = <<"">>, errorMessage = <<"Not implemented">>, resultCode = operationsError}}] end end. make_entry(DN, Tree, Attrs) -> KVs = case ets:lookup(Tree, {dn, DN}) of [{_, _KVs}|_] -> _KVs; _ -> [] end, NewKVs = if Attrs /= [], Attrs /= [<<"*">>] -> lists:filter( fun({A, _V}) -> member(A, Attrs) end, KVs); true -> KVs end, KVs1 = dict:to_list( lists:foldl( fun({A, V}, D) -> dict:append(A, V, D) end, dict:new(), NewKVs)), {searchResEntry, #'SearchResultEntry'{ objectName = str:join(DN, <<",">>), attributes = [#'PartialAttributeList_SEQOF'{type = T, vals = V} || {T, V} <- KVs1]}}. process_dn_filter(DN, Level, F, Tree) -> DN1 = str:tokens(DN, <<",">>), Fun = filter_to_fun(F), filter(Fun, DN1, Tree, Level). filter_to_fun({'and', Fs}) -> fun(KVs) -> lists:all( fun(F) -> (filter_to_fun(F))(KVs) end, Fs) end; filter_to_fun({'or', Fs}) -> fun(KVs) -> lists:any( fun(F) -> (filter_to_fun(F))(KVs) end, Fs) end; filter_to_fun({present, Attr}) -> fun(KVs) -> present(Attr, KVs) end; filter_to_fun({Tag, #'AttributeValueAssertion'{attributeDesc = Attr, assertionValue = Val}}) when Tag == equalityMatch; Tag == greaterOrEqual; Tag == lessOrEqual; Tag == approxMatch -> fun(KVs) -> apply(?MODULE, Tag, [Attr, Val, KVs]) end; filter_to_fun({substrings, #'SubstringFilter'{type = A, substrings = Ss}}) -> Re = substrings_to_regexp(Ss), fun(KVs) -> substrings(A, Re, KVs) end; filter_to_fun({'not', F}) -> fun(KVs) -> not (filter_to_fun(F))(KVs) end. find_obj(DN, Tree) -> case ets:lookup(Tree, {dn, str:tokens(DN, <<",">>)}) of [{_, Obj}|_] -> {ok, Obj}; [] -> error end. present(A, R) -> case keyfind(A, R) of [] -> false; _ -> true end. equalityMatch(A, V, R) -> Vs = keyfind(A, R), member(V, Vs). lessOrEqual(A, V, R) -> lists:any( fun(X) -> str:to_lower(X) =< str:to_lower(V) end, keyfind(A, R)). greaterOrEqual(A, V, R) -> lists:any( fun(X) -> str:to_lower(X) >= str:to_lower(V) end, keyfind(A, R)). approxMatch(A, V, R) -> equalityMatch(A, V, R). substrings(A, Re, R) -> lists:any( fun(V) -> case re:run(str:to_lower(V), Re) of {match, _} -> true; _ -> false end end, keyfind(A, R)). substrings_to_regexp(Ss) -> ReS = lists:map( fun({initial, S}) -> [S, <<".*">>]; ({any, S}) -> [<<".*">>, S, <<".*">>]; ({final, S}) -> [<<".*">>, S] end, Ss), ReS1 = str:to_lower(list_to_binary([$^, ReS, $$])), {ok, Re} = re:compile(ReS1), Re. filter(F, BaseDN, Tree, Level) -> KVs = case ets:lookup(Tree, {dn, BaseDN}) of [{_, _KVs}|_] -> _KVs; [] -> [] end, Rest = case Level of baseObject -> []; _ -> NewLevel = if Level /= wholeSubtree -> baseObject; true -> Level end, lists:flatmap( fun({_, D}) -> NewDN = if BaseDN == [] -> D; true -> [D|BaseDN] end, filter(F, NewDN, Tree, NewLevel) end, ets:lookup(Tree, BaseDN)) end, if BaseDN == [], Level /= baseObject -> Rest; true -> case F(KVs) of true -> [BaseDN|Rest]; false -> Rest end end. keyfind(K, KVs) -> keyfind(str:to_lower(K), KVs, []). keyfind(K, [{K1, V}|T], Acc) -> case str:to_lower(K1) of K -> keyfind(K, T, [V|Acc]); _ -> keyfind(K, T, Acc) end; keyfind(_, [], Acc) -> Acc. member(E, Es) -> member1(str:to_lower(E), Es). member1(E, [H|T]) -> case str:to_lower(H) of E -> true; _ -> member1(E, T) end; member1(_, []) -> false. load_ldif(Path) -> case file:open(Path, [read, binary]) of {ok, Fd} -> {ok, resort(format(read_lines(Fd, []), [], []))}; Err -> ?ERROR_MSG("failed to read LDIF file: ~p", [Err]), Err end. read_lines(Fd, Acc) -> case file:read_line(Fd) of {ok, Str} -> Line = process_line(str:strip(Str, right, $\n)), read_lines(Fd, [Line|Acc]); eof -> Acc; Err -> Err end. process_line(<<C, _/binary>> = L) when C/=$ , C/=$\t, C/=$\n -> case str:chr(L, $:) of 0 -> <<>>; Pos -> NewPos = Pos - 1, case L of <<Val:NewPos/binary, $:, $:, Rest/binary>> -> {Val, base64, str:strip(Rest, left, $ )}; <<Val:NewPos/binary, $:, Rest/binary>> -> {Val, plain, str:strip(Rest, left, $ )} end end; process_line([_|L]) -> L; process_line(_) -> <<>>. format([{Val, Type, L}|T], Ls, Acc) -> Str1 = iolist_to_binary([L|Ls]), Str2 = case Type of plain -> Str1; base64 -> base64:decode(Str1) end, format(T, [], [{Val, Str2}|Acc]); format([<<"-">>|T], Ls, Acc) -> format(T, Ls, Acc); format([L|T], Ls, Acc) -> format(T, [L|Ls], Acc); format([], _, Acc) -> lists:reverse(Acc). resort(T) -> resort(T, [], [], ets:new(ldap_tree, [named_table, public, bag])). resort([{<<"dn">>, S}|T], Ls, DNs, Tree) -> case proplists:get_value(<<"changetype">>, Ls, <<"add">>) of <<"add">> -> [H|Rest] = DN = str:tokens(S, <<",">>), ets:insert(Tree, {{dn, DN}, Ls}), ets:insert(Tree, {Rest, H}), resort(T, [], [DN|DNs], Tree); _ -> resort(T, [], DNs, Tree) end; resort([AttrVal|T], Ls, DNs, Acc) -> resort(T, [AttrVal|Ls], DNs, Acc); resort([], _, DNs, Tree) -> {_, TopDNs} = lists:foldl( fun(D, {L, Acc}) -> NewL = length(D), if NewL < L -> {NewL, [D]}; NewL == L -> {L, [D|Acc]}; true -> {L, Acc} end end, {unlimited, []}, DNs), Attrs = lists:map( fun(TopDN) -> ets:insert(Tree, {[], TopDN}), {<<"namingContexts">>, str:join(TopDN, <<",">>)} end, TopDNs), Attrs1 = [{<<"supportedLDAPVersion">>, <<"3">>}, {<<"objectClass">>, <<"top">>}|Attrs], ets:insert(Tree, {{dn, []}, Attrs1}), Tree. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/suite.hrl�����������������������������������������������������������������������0000644�0002322�0002322�00000007345�14154362354�016462� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-include_lib("common_test/include/ct.hrl"). -include_lib("fast_xml/include/fxml.hrl"). -include_lib("xmpp/include/jid.hrl"). -include_lib("xmpp/include/ns.hrl"). -include_lib("xmpp/include/xmpp_codec.hrl"). -include("mod_proxy65.hrl"). -define(STREAM_TRAILER, <<"</stream:stream>">>). -define(PUBSUB(Node), <<(?NS_PUBSUB)/binary, "#", Node>>). -define(EJABBERD_CT_URI, <<"http://www.process-one.net/en/ejabberd_ct/">>). -define(recv1(P1), P1 = (fun() -> V = suite:recv(Config), case V of P1 -> V; _ -> suite:match_failure([V], [??P1]) end end)()). -define(recv2(P1, P2), (fun() -> case {R1 = suite:recv(Config), R2 = suite:recv(Config)} of {P1, P2} -> {R1, R2}; {P2, P1} -> {R2, R1}; {P1, V1} -> suite:match_failure([V1], [P2]); {P2, V2} -> suite:match_failure([V2], [P1]); {V3, P1} -> suite:match_failure([V3], [P2]); {V4, P2} -> suite:match_failure([V4], [P1]); {V5, V6} -> suite:match_failure([V5, V6], [P1, P2]) end end)()). -define(recv3(P1, P2, P3), (fun() -> case R3 = suite:recv(Config) of P1 -> insert(R3, 1, ?recv2(P2, P3)); P2 -> insert(R3, 2, ?recv2(P1, P3)); P3 -> insert(R3, 3, ?recv2(P1, P2)); V -> suite:match_failure([V], [P1, P2, P3]) end end)()). -define(recv4(P1, P2, P3, P4), (fun() -> case R4 = suite:recv(Config) of P1 -> insert(R4, 1, ?recv3(P2, P3, P4)); P2 -> insert(R4, 2, ?recv3(P1, P3, P4)); P3 -> insert(R4, 3, ?recv3(P1, P2, P4)); P4 -> insert(R4, 4, ?recv3(P1, P2, P3)); V -> suite:match_failure([V], [P1, P2, P3, P4]) end end)()). -define(recv5(P1, P2, P3, P4, P5), (fun() -> case R5 = suite:recv(Config) of P1 -> insert(R5, 1, ?recv4(P2, P3, P4, P5)); P2 -> insert(R5, 2, ?recv4(P1, P3, P4, P5)); P3 -> insert(R5, 3, ?recv4(P1, P2, P4, P5)); P4 -> insert(R5, 4, ?recv4(P1, P2, P3, P5)); P5 -> insert(R5, 5, ?recv4(P1, P2, P3, P4)); V -> suite:match_failure([V], [P1, P2, P3, P4, P5]) end end)()). -define(match(Pattern, Result), (fun() -> case Result of Pattern -> ok; Mismatch -> suite:match_failure([Mismatch], [??Pattern]) end end)()). -define(match(Pattern, Result, PatternRes), (fun() -> case Result of Pattern -> PatternRes; Mismatch -> suite:match_failure([Mismatch], [??Pattern]) end end)()). -define(send_recv(Send, Recv), ?match(Recv, suite:send_recv(Config, Send))). -define(COMMON_VHOST, <<"localhost">>). -define(MNESIA_VHOST, <<"mnesia.localhost">>). -define(REDIS_VHOST, <<"redis.localhost">>). -define(MYSQL_VHOST, <<"mysql.localhost">>). -define(MSSQL_VHOST, <<"mssql.localhost">>). -define(PGSQL_VHOST, <<"pgsql.localhost">>). -define(SQLITE_VHOST, <<"sqlite.localhost">>). -define(LDAP_VHOST, <<"ldap.localhost">>). -define(EXTAUTH_VHOST, <<"extauth.localhost">>). -define(S2S_VHOST, <<"s2s.localhost">>). -define(UPLOAD_VHOST, <<"upload.localhost">>). -define(BACKENDS, [mnesia, redis, mysql, mssql, odbc, pgsql, sqlite, ldap, extauth]). insert(Val, N, Tuple) -> L = tuple_to_list(Tuple), {H, T} = lists:split(N-1, L), list_to_tuple(H ++ [Val|T]). �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/replaced_tests.erl��������������������������������������������������������������0000644�0002322�0002322�00000005172�14154362354�020323� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(replaced_tests). %% API -compile(export_all). -import(suite, [bind/1, wait_for_slave/1, wait_for_master/1, recv/1, close_socket/1, disconnect/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {replaced_single, [sequence], []}. %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {replaced_master_slave, [sequence], [master_slave_test(conflict)]}. conflict_master(Config0) -> Config = bind(Config0), wait_for_slave(Config), #stream_error{reason = conflict} = recv(Config), {xmlstreamend, <<"stream:stream">>} = recv(Config), close_socket(Config). conflict_slave(Config0) -> wait_for_master(Config0), Config = bind(Config0), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("replaced_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("replaced_" ++ atom_to_list(T)), [parallel], [list_to_atom("replaced_" ++ atom_to_list(T) ++ "_master"), list_to_atom("replaced_" ++ atom_to_list(T) ++ "_slave")]}. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/push_tests.erl������������������������������������������������������������������0000644�0002322�0002322�00000020233�14154362354�017516� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Created : 15 Jul 2017 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(push_tests). %% API -compile(export_all). -import(suite, [close_socket/1, connect/1, disconnect/1, get_event/1, get_features/2, make_iq_result/1, my_jid/1, put_event/2, recv/1, recv_iq/1, recv_message/1, self_presence/2, send/2, send_recv/2, server_jid/1]). -include("suite.hrl"). -define(PUSH_NODE, <<"d3v1c3">>). -define(PUSH_XDATA_FIELDS, [#xdata_field{var = <<"FORM_TYPE">>, values = [?NS_PUBSUB_PUBLISH_OPTIONS]}, #xdata_field{var = <<"secret">>, values = [<<"c0nf1d3nt14l">>]}]). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {push_single, [sequence], [single_test(feature_enabled), single_test(unsupported_iq)]}. feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), Features = get_features(Config, BareMyJID), true = lists:member(?NS_PUSH_0, Features), disconnect(Config). unsupported_iq(Config) -> PushJID = my_jid(Config), lists:foreach( fun(SubEl) -> #iq{type = error} = send_recv(Config, #iq{type = get, sub_els = [SubEl]}) end, [#push_enable{jid = PushJID}, #push_disable{jid = PushJID}]), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {push_master_slave, [sequence], [master_slave_test(sm), master_slave_test(offline), master_slave_test(mam)]}. sm_master(Config) -> ct:comment("Waiting for the slave to close the socket"), peer_down = get_event(Config), ct:comment("Waiting a bit in order to test the keepalive feature"), ct:sleep(5000), % Without mod_push_keepalive, the session would time out. ct:comment("Sending message to the slave"), send_test_message(Config), ct:comment("Handling push notification"), handle_notification(Config), ct:comment("Receiving bounced message from the slave"), #message{type = error} = recv_message(Config), ct:comment("Closing the connection"), disconnect(Config). sm_slave(Config) -> ct:comment("Enabling push notifications"), ok = enable_push(Config), ct:comment("Enabling stream management"), ok = enable_sm(Config), ct:comment("Closing the socket"), close_socket(Config). offline_master(Config) -> ct:comment("Waiting for the slave to be ready"), ready = get_event(Config), ct:comment("Sending message to the slave"), send_test_message(Config), % No push notification, slave is online. ct:comment("Waiting for the slave to disconnect"), peer_down = get_event(Config), ct:comment("Sending message to offline storage"), send_test_message(Config), ct:comment("Handling push notification for offline message"), handle_notification(Config), ct:comment("Closing the connection"), disconnect(Config). offline_slave(Config) -> ct:comment("Re-enabling push notifications"), ok = enable_push(Config), ct:comment("Letting the master know that we're ready"), put_event(Config, ready), ct:comment("Receiving message from the master"), recv_test_message(Config), ct:comment("Closing the connection"), disconnect(Config). mam_master(Config) -> ct:comment("Waiting for the slave to be ready"), ready = get_event(Config), ct:comment("Sending message to the slave"), send_test_message(Config), ct:comment("Handling push notification for MAM message"), handle_notification(Config), ct:comment("Closing the connection"), disconnect(Config). mam_slave(Config) -> self_presence(Config, available), ct:comment("Receiving message from offline storage"), recv_test_message(Config), %% Don't re-enable push notifications, otherwise the notification would be %% suppressed while the slave is online. ct:comment("Enabling MAM"), ok = enable_mam(Config), ct:comment("Letting the master know that we're ready"), put_event(Config, ready), ct:comment("Receiving message from the master"), recv_test_message(Config), ct:comment("Waiting for the master to disconnect"), peer_down = get_event(Config), ct:comment("Disabling push notifications"), ok = disable_push(Config), ct:comment("Closing the connection and cleaning up"), clean(disconnect(Config)). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("push_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("push_" ++ atom_to_list(T)), [parallel], [list_to_atom("push_" ++ atom_to_list(T) ++ "_master"), list_to_atom("push_" ++ atom_to_list(T) ++ "_slave")]}. enable_sm(Config) -> send(Config, #sm_enable{xmlns = ?NS_STREAM_MGMT_3, resume = true}), case recv(Config) of #sm_enabled{resume = true} -> ok; #sm_failed{reason = Reason} -> Reason end. enable_mam(Config) -> case send_recv( Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = ?NS_MAM_1, default = always}]}) of #iq{type = result} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. enable_push(Config) -> %% Usually, the push JID would be a server JID (such as push.example.com). %% We specify the peer's full user JID instead, so the push notifications %% will be sent to the peer. PushJID = ?config(peer, Config), XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, case send_recv( Config, #iq{type = set, sub_els = [#push_enable{jid = PushJID, node = ?PUSH_NODE, xdata = XData}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. disable_push(Config) -> PushJID = ?config(peer, Config), case send_recv( Config, #iq{type = set, sub_els = [#push_disable{jid = PushJID, node = ?PUSH_NODE}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. send_test_message(Config) -> Peer = ?config(peer, Config), Msg = #message{to = Peer, body = [#text{data = <<"test">>}]}, send(Config, Msg). recv_test_message(Config) -> Peer = ?config(peer, Config), #message{from = Peer, body = [#text{data = <<"test">>}]} = recv_message(Config). handle_notification(Config) -> From = server_jid(Config), Item = #ps_item{sub_els = [xmpp:encode(#push_notification{})]}, Publish = #ps_publish{node = ?PUSH_NODE, items = [Item]}, XData = #xdata{type = submit, fields = ?PUSH_XDATA_FIELDS}, PubSub = #pubsub{publish = Publish, publish_options = XData}, IQ = #iq{type = set, from = From, sub_els = [PubSub]} = recv_iq(Config), send(Config, make_iq_result(IQ)). clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_push:remove_user(U, S), mod_mam:remove_user(U, S), Config. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/vcard_tests.erl�����������������������������������������������������������������0000644�0002322�0002322�00000013753�14154362354�017647� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(vcard_tests). %% API -compile(export_all). -import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, is_feature_advertised/3, server_jid/1, my_jid/1, wait_for_slave/1, wait_for_master/1, recv_presence/1, recv/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {vcard_single, [sequence], [single_test(feature_enabled), single_test(get_set), single_test(service_vcard)]}. feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), true = is_feature_advertised(Config, ?NS_VCARD), true = is_feature_advertised(Config, ?NS_VCARD, BareMyJID), disconnect(Config). get_set(Config) -> VCard = #vcard_temp{fn = <<"Peter Saint-Andre">>, n = #vcard_name{family = <<"Saint-Andre">>, given = <<"Peter">>}, nickname = <<"stpeter">>, bday = <<"1966-08-06">>, adr = [#vcard_adr{work = true, extadd = <<"Suite 600">>, street = <<"1899 Wynkoop Street">>, locality = <<"Denver">>, region = <<"CO">>, pcode = <<"80202">>, ctry = <<"USA">>}, #vcard_adr{home = true, locality = <<"Denver">>, region = <<"CO">>, pcode = <<"80209">>, ctry = <<"USA">>}], tel = [#vcard_tel{work = true,voice = true, number = <<"303-308-3282">>}, #vcard_tel{home = true,voice = true, number = <<"303-555-1212">>}], email = [#vcard_email{internet = true,pref = true, userid = <<"stpeter@jabber.org">>}], jabberid = <<"stpeter@jabber.org">>, title = <<"Executive Director">>,role = <<"Patron Saint">>, org = #vcard_org{name = <<"XMPP Standards Foundation">>}, url = <<"http://www.xmpp.org/xsf/people/stpeter.shtml">>, desc = <<"More information about me is located on my " "personal website: http://www.saint-andre.com/">>}, #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [VCard]}), %% TODO: check if VCard == VCard1. #iq{type = result, sub_els = [_VCard1]} = send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), disconnect(Config). service_vcard(Config) -> JID = server_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), VCard = mod_vcard_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {vcard_master_slave, [sequence], []}. %%[master_slave_test(xupdate)]}. xupdate_master(Config) -> Img = <<137, "PNG\r\n", 26, $\n>>, ImgHash = p1_sha:sha(Img), MyJID = my_jid(Config), Peer = ?config(slave, Config), wait_for_slave(Config), #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), #presence{from = Peer, type = available} = recv_presence(Config), VCard = #vcard_temp{photo = #vcard_photo{type = <<"image/png">>, binval = Img}}, #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [VCard]}), #presence{from = MyJID, type = available, sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [#vcard_temp{}]}), ?recv2(#presence{from = MyJID, type = available, sub_els = [#vcard_xupdate{hash = undefined}]}, #presence{from = Peer, type = unavailable}), disconnect(Config). xupdate_slave(Config) -> Img = <<137, "PNG\r\n", 26, $\n>>, ImgHash = p1_sha:sha(Img), MyJID = my_jid(Config), Peer = ?config(master, Config), #presence{from = MyJID, type = available} = send_recv(Config, #presence{}), wait_for_master(Config), #presence{from = Peer, type = available} = recv_presence(Config), #presence{from = Peer, type = available, sub_els = [#vcard_xupdate{hash = ImgHash}]} = recv_presence(Config), #presence{from = Peer, type = available, sub_els = [#vcard_xupdate{hash = undefined}]} = recv_presence(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("vcard_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("vcard_" ++ atom_to_list(T)), [parallel], [list_to_atom("vcard_" ++ atom_to_list(T) ++ "_master"), list_to_atom("vcard_" ++ atom_to_list(T) ++ "_slave")]}. ���������������������ejabberd-21.12/test/roster_tests.erl����������������������������������������������������������������0000644�0002322�0002322�00000050531�14154362354�020061� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 22 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(roster_tests). %% API -compile(export_all). -import(suite, [send_recv/2, recv_iq/1, send/2, disconnect/1, del_roster/1, del_roster/2, make_iq_result/1, wait_for_slave/1, wait_for_master/1, recv_presence/1, self_presence/2, put_event/2, get_event/1, match_failure/2, get_roster/1]). -include("suite.hrl"). -include("mod_roster.hrl"). -record(state, {subscription = none :: none | from | to | both, peer_available = false, pending_in = false :: boolean(), pending_out = false :: boolean()}). %%%=================================================================== %%% API %%%=================================================================== init(_TestCase, Config) -> Config. stop(_TestCase, Config) -> Config. %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {roster_single, [sequence], [single_test(feature_enabled), single_test(iq_set_many_items), single_test(iq_set_duplicated_groups), single_test(iq_get_item), single_test(iq_unexpected_element), single_test(iq_set_ask), single_test(set_item), single_test(version)]}. feature_enabled(Config) -> ct:comment("Checking if roster versioning stream feature is set"), true = ?config(rosterver, Config), disconnect(Config). set_item(Config) -> JID = jid:decode(<<"nurse@example.com">>), Item = #roster_item{jid = JID}, {V1, Item} = set_items(Config, [Item]), {V1, [Item]} = get_items(Config), ItemWithGroups = Item#roster_item{groups = [<<"G1">>, <<"G2">>]}, {V2, ItemWithGroups} = set_items(Config, [ItemWithGroups]), {V2, [ItemWithGroups]} = get_items(Config), {V3, Item} = set_items(Config, [Item]), {V3, [Item]} = get_items(Config), ItemWithName = Item#roster_item{name = <<"some name">>}, {V4, ItemWithName} = set_items(Config, [ItemWithName]), {V4, [ItemWithName]} = get_items(Config), ItemRemoved = Item#roster_item{subscription = remove}, {V5, ItemRemoved} = set_items(Config, [ItemRemoved]), {V5, []} = get_items(Config), del_roster(disconnect(Config), JID). iq_set_many_items(Config) -> J1 = jid:decode(<<"nurse1@example.com">>), J2 = jid:decode(<<"nurse2@example.com">>), ct:comment("Trying to send roster-set with many <item/> elements"), Items = [#roster_item{jid = J1}, #roster_item{jid = J2}], #stanza_error{reason = 'bad-request'} = set_items(Config, Items), disconnect(Config). iq_set_duplicated_groups(Config) -> JID = jid:decode(<<"nurse@example.com">>), G = p1_rand:get_string(), ct:comment("Trying to send roster-set with duplicated groups"), Item = #roster_item{jid = JID, groups = [G, G]}, #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), disconnect(Config). iq_set_ask(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send roster-set with 'ask' included"), Item = #roster_item{jid = JID, ask = subscribe}, #stanza_error{reason = 'bad-request'} = set_items(Config, [Item]), disconnect(Config). iq_get_item(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send roster-get with <item/> element"), #iq{type = error} = Err3 = send_recv(Config, #iq{type = get, sub_els = [#roster_query{ items = [#roster_item{jid = JID}]}]}), #stanza_error{reason = 'bad-request'} = xmpp:get_error(Err3), disconnect(Config). iq_unexpected_element(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Trying to send IQs with unexpected element"), lists:foreach( fun(Type) -> #iq{type = error} = Err4 = send_recv(Config, #iq{type = Type, sub_els = [#roster_item{jid = JID}]}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err4) end, [get, set]), disconnect(Config). version(Config) -> JID = jid:decode(<<"nurse@example.com">>), ct:comment("Requesting roster"), {InitialVersion, _} = get_items(Config, <<"">>), ct:comment("Requesting roster with initial version"), {empty, []} = get_items(Config, InitialVersion), ct:comment("Adding JID to the roster"), {NewVersion, _} = set_items(Config, [#roster_item{jid = JID}]), ct:comment("Requesting roster with initial version"), {NewVersion, _} = get_items(Config, InitialVersion), ct:comment("Requesting roster with new version"), {empty, []} = get_items(Config, NewVersion), del_roster(disconnect(Config), JID). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {roster_master_slave, [sequence], [master_slave_test(subscribe)]}. subscribe_master(Config) -> Actions = actions(), process_subscriptions_master(Config, Actions), del_roster(disconnect(Config)). subscribe_slave(Config) -> process_subscriptions_slave(Config), del_roster(disconnect(Config)). process_subscriptions_master(Config, Actions) -> EnumeratedActions = lists:zip(lists:seq(1, length(Actions)), Actions), self_presence(Config, available), Peer = ?config(peer, Config), lists:foldl( fun({N, {Dir, Type}}, State) -> if Dir == out -> put_event(Config, {N, in, Type}); Dir == in -> put_event(Config, {N, out, Type}) end, Roster = get_roster(Config), ct:pal("Performing ~s-~s (#~p) " "in state:~n~s~nwith roster:~n~s", [Dir, Type, N, pp(State), pp(Roster)]), check_roster(Roster, Config, State), wait_for_slave(Config), Id = mk_id(N, Dir, Type), NewState = transition(Id, Config, Dir, Type, State), wait_for_slave(Config), send_recv(Config, #iq{type = get, to = Peer, id = Id, sub_els = [#ping{}]}), check_roster_item(Config, NewState), NewState end, #state{}, EnumeratedActions), put_event(Config, done), wait_for_slave(Config), Config. process_subscriptions_slave(Config) -> self_presence(Config, available), process_subscriptions_slave(Config, get_event(Config), #state{}). process_subscriptions_slave(Config, done, _State) -> wait_for_master(Config), Config; process_subscriptions_slave(Config, {N, Dir, Type}, State) -> Roster = get_roster(Config), ct:pal("Performing ~s-~s (#~p) " "in state:~n~s~nwith roster:~n~s", [Dir, Type, N, pp(State), pp(Roster)]), check_roster(Roster, Config, State), wait_for_master(Config), NewState = transition(mk_id(N, Dir, Type), Config, Dir, Type, State), wait_for_master(Config), send(Config, xmpp:make_iq_result(recv_iq(Config))), check_roster_item(Config, NewState), process_subscriptions_slave(Config, get_event(Config), NewState). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("roster_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("roster_" ++ atom_to_list(T)), [parallel], [list_to_atom("roster_" ++ atom_to_list(T) ++ "_master"), list_to_atom("roster_" ++ atom_to_list(T) ++ "_slave")]}. get_items(Config) -> get_items(Config, <<"">>). get_items(Config, Version) -> case send_recv(Config, #iq{type = get, sub_els = [#roster_query{ver = Version}]}) of #iq{type = result, sub_els = [#roster_query{ver = NewVersion, items = Items}]} -> {NewVersion, normalize_items(Items)}; #iq{type = result, sub_els = []} -> {empty, []}; #iq{type = error} = Err -> xmpp:get_error(Err) end. normalize_items(Items) -> Items2 = lists:map( fun(I) -> I#roster_item{groups = lists:sort(I#roster_item.groups)} end, Items), lists:sort(Items2). get_item(Config, JID) -> case get_items(Config) of {_Ver, Items} when is_list(Items) -> lists:keyfind(JID, #roster_item.jid, Items); _ -> false end. set_items(Config, Items) -> case send_recv(Config, #iq{type = set, sub_els = [#roster_query{items = Items}]}) of #iq{type = result, sub_els = []} -> recv_push(Config); #iq{type = error} = Err -> xmpp:get_error(Err) end. recv_push(Config) -> ct:comment("Receiving roster push"), Push = #iq{type = set, sub_els = [#roster_query{ver = Ver, items = [PushItem]}]} = recv_iq(Config), send(Config, make_iq_result(Push)), {Ver, PushItem}. recv_push(Config, Subscription, Ask) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), Match = #roster_item{jid = PeerBareJID, subscription = Subscription, ask = Ask, groups = [], name = <<"">>}, ct:comment("Receiving roster push"), Push = #iq{type = set, sub_els = [#roster_query{items = [Item]}]} = recv_iq(Config), case Item of Match -> send(Config, make_iq_result(Push)); _ -> match_failure(Item, Match) end. recv_presence(Config, Type) -> PeerJID = ?config(peer, Config), case recv_presence(Config) of #presence{from = PeerJID, type = Type} -> ok; Pres -> match_failure(Pres, #presence{from = PeerJID, type = Type}) end. recv_subscription(Config, Type) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), case recv_presence(Config) of #presence{from = PeerBareJID, type = Type} -> ok; Pres -> match_failure(Pres, #presence{from = PeerBareJID, type = Type}) end. pp(Term) -> io_lib_pretty:print(Term, fun pp/2). pp(state, N) -> Fs = record_info(fields, state), try N = length(Fs), Fs catch _:_ -> no end; pp(roster, N) -> Fs = record_info(fields, roster), try N = length(Fs), Fs catch _:_ -> no end; pp(_, _) -> no. mk_id(N, Dir, Type) -> list_to_binary([integer_to_list(N), $-, atom_to_list(Dir), $-, atom_to_list(Type)]). check_roster([], _Config, _State) -> ok; check_roster([Roster], _Config, State) -> case {Roster#roster.subscription == State#state.subscription, Roster#roster.ask, State#state.pending_in, State#state.pending_out} of {true, both, true, true} -> ok; {true, in, true, false} -> ok; {true, out, false, true} -> ok; {true, none, false, false} -> ok; _ -> ct:fail({roster_mismatch, State, Roster}) end. check_roster_item(Config, State) -> Peer = jid:remove_resource(?config(peer, Config)), RosterItem = case get_item(Config, Peer) of false -> #roster_item{}; Item -> Item end, case {RosterItem#roster_item.subscription == State#state.subscription, RosterItem#roster_item.ask, State#state.pending_out} of {true, subscribe, true} -> ok; {true, undefined, false} -> ok; _ -> ct:fail({roster_item_mismatch, State, RosterItem}) end. %% RFC6121, A.2.1 transition(Id, Config, out, subscribe, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = subscribe}), case {Sub, Out, In} of {none, false, _} -> recv_push(Config, none, subscribe), State#state{pending_out = true}; {none, true, false} -> %% BUG: we should not receive roster push here recv_push(Config, none, subscribe), State; {from, false, false} -> recv_push(Config, from, subscribe), State#state{pending_out = true}; _ -> State end; %% RFC6121, A.2.2 transition(Id, Config, out, unsubscribe, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribe}), case {Sub, Out, In} of {none, true, _} -> recv_push(Config, none, undefined), State#state{pending_out = false}; {to, false, _} -> recv_push(Config, none, undefined), recv_presence(Config, unavailable), State#state{subscription = none, peer_available = false}; {from, true, false} -> recv_push(Config, from, undefined), State#state{pending_out = false}; {both, false, false} -> recv_push(Config, from, undefined), recv_presence(Config, unavailable), State#state{subscription = from, peer_available = false}; _ -> State end; %% RFC6121, A.2.3 transition(Id, Config, out, subscribed, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = subscribed}), case {Sub, Out, In} of {none, false, true} -> recv_push(Config, from, undefined), State#state{subscription = from, pending_in = false}; {none, true, true} -> recv_push(Config, from, subscribe), State#state{subscription = from, pending_in = false}; {to, false, true} -> recv_push(Config, both, undefined), State#state{subscription = both, pending_in = false}; {to, false, _} -> %% BUG: we should not transition to 'both' state recv_push(Config, both, undefined), State#state{subscription = both}; _ -> State end; %% RFC6121, A.2.4 transition(Id, Config, out, unsubscribed, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), send(Config, #presence{id = Id, to = PeerBareJID, type = unsubscribed}), case {Sub, Out, In} of {none, false, true} -> State#state{subscription = none, pending_in = false}; {none, true, true} -> recv_push(Config, none, subscribe), State#state{subscription = none, pending_in = false}; {to, _, true} -> State#state{pending_in = false}; {from, false, _} -> recv_push(Config, none, undefined), State#state{subscription = none}; {from, true, _} -> recv_push(Config, none, subscribe), State#state{subscription = none}; {both, _, _} -> recv_push(Config, to, undefined), State#state{subscription = to}; _ -> State end; %% RFC6121, A.3.1 transition(_, Config, in, subscribe = Type, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of {none, false, false} -> recv_subscription(Config, Type), State#state{pending_in = true}; {none, true, false} -> recv_push(Config, none, subscribe), recv_subscription(Config, Type), State#state{pending_in = true}; {to, false, false} -> %% BUG: we should not receive roster push in this state! recv_push(Config, to, undefined), recv_subscription(Config, Type), State#state{pending_in = true}; _ -> State end; %% RFC6121, A.3.2 transition(_, Config, in, unsubscribe = Type, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of {none, _, true} -> State#state{pending_in = false}; {to, _, true} -> recv_push(Config, to, undefined), recv_subscription(Config, Type), State#state{pending_in = false}; {from, false, _} -> recv_push(Config, none, undefined), recv_subscription(Config, Type), State#state{subscription = none}; {from, true, _} -> recv_push(Config, none, subscribe), recv_subscription(Config, Type), State#state{subscription = none}; {both, _, _} -> recv_push(Config, to, undefined), recv_subscription(Config, Type), State#state{subscription = to}; _ -> State end; %% RFC6121, A.3.3 transition(_, Config, in, subscribed = Type, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of {none, true, _} -> recv_push(Config, to, undefined), recv_subscription(Config, Type), recv_presence(Config, available), State#state{subscription = to, pending_out = false, peer_available = true}; {from, true, _} -> recv_push(Config, both, undefined), recv_subscription(Config, Type), recv_presence(Config, available), State#state{subscription = both, pending_out = false, peer_available = true}; {from, false, _} -> %% BUG: we should not transition to 'both' in this state recv_push(Config, both, undefined), recv_subscription(Config, Type), recv_presence(Config, available), State#state{subscription = both, pending_out = false, peer_available = true}; _ -> State end; %% RFC6121, A.3.4 transition(_, Config, in, unsubscribed = Type, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of {none, true, true} -> %% BUG: we should receive roster push in this state! recv_subscription(Config, Type), State#state{subscription = none, pending_out = false}; {none, true, false} -> recv_push(Config, none, undefined), recv_subscription(Config, Type), State#state{subscription = none, pending_out = false}; {none, false, false} -> State; {to, false, _} -> recv_push(Config, none, undefined), recv_presence(Config, unavailable), recv_subscription(Config, Type), State#state{subscription = none, peer_available = false}; {from, true, false} -> recv_push(Config, from, undefined), recv_subscription(Config, Type), State#state{subscription = from, pending_out = false}; {both, _, _} -> recv_push(Config, from, undefined), recv_presence(Config, unavailable), recv_subscription(Config, Type), State#state{subscription = from, peer_available = false}; _ -> State end; %% Outgoing roster remove transition(Id, Config, out, remove, #state{subscription = Sub, pending_in = In, pending_out = Out}) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), Item = #roster_item{jid = PeerBareJID, subscription = remove}, #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, id = Id, sub_els = [#roster_query{items = [Item]}]}), recv_push(Config, remove, undefined), case {Sub, Out, In} of {to, _, _} -> recv_presence(Config, unavailable); {both, _, _} -> recv_presence(Config, unavailable); _ -> ok end, #state{}; %% Incoming roster remove transition(_, Config, in, remove, #state{subscription = Sub, pending_in = In, pending_out = Out} = State) -> case {Sub, Out, In} of {none, true, _} -> ok; {from, false, _} -> recv_push(Config, none, undefined), recv_subscription(Config, unsubscribe); {from, true, _} -> recv_push(Config, none, subscribe), recv_subscription(Config, unsubscribe); {to, false, _} -> %% BUG: we should receive push here %% recv_push(Config, none, undefined), recv_presence(Config, unavailable), recv_subscription(Config, unsubscribed); {both, _, _} -> recv_presence(Config, unavailable), recv_push(Config, to, undefined), recv_subscription(Config, unsubscribe), recv_push(Config, none, undefined), recv_subscription(Config, unsubscribed); _ -> ok end, State#state{subscription = none}. actions() -> States = [{Dir, Type} || Dir <- [out, in], Type <- [subscribe, subscribed, unsubscribe, unsubscribed, remove]], Actions = lists:flatten([[X, Y] || X <- States, Y <- States]), remove_dups(Actions, []). remove_dups([X|T], [X,X|_] = Acc) -> remove_dups(T, Acc); remove_dups([X|T], Acc) -> remove_dups(T, [X|Acc]); remove_dups([], Acc) -> lists:reverse(Acc). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE.erl��������������������������������������������������������������0000644�0002322�0002322�00000111646�14154362354�020035� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 2 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as %%% published by the Free Software Foundation; either version 2 of the %%% License, or (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU %%% General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License along %%% with this program; if not, write to the Free Software Foundation, Inc., %%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. %%% %%%---------------------------------------------------------------------- -module(ejabberd_SUITE). -compile(export_all). -import(suite, [init_config/1, connect/1, disconnect/1, recv_message/1, recv/1, recv_presence/1, send/2, send_recv/2, my_jid/1, server_jid/1, pubsub_jid/1, proxy_jid/1, muc_jid/1, muc_room_jid/1, my_muc_jid/1, peer_muc_jid/1, mix_jid/1, mix_room_jid/1, get_features/2, recv_iq/1, re_register/1, is_feature_advertised/2, subscribe_to_events/1, is_feature_advertised/3, set_opt/3, auth_SASL/2, auth_SASL/3, auth_SASL/4, wait_for_master/1, wait_for_slave/1, flush/1, make_iq_result/1, start_event_relay/0, alt_room_jid/1, stop_event_relay/1, put_event/2, get_event/1, bind/1, auth/1, auth/2, open_session/1, open_session/2, zlib/1, starttls/1, starttls/2, close_socket/1, init_stream/1, auth_legacy/2, auth_legacy/3, tcp_connect/1, send_text/2, set_roster/3, del_roster/1]). -include("suite.hrl"). suite() -> [{timetrap, {seconds, 120}}]. init_per_suite(Config) -> NewConfig = init_config(Config), DataDir = proplists:get_value(data_dir, NewConfig), {ok, CWD} = file:get_cwd(), ExtAuthScript = filename:join([DataDir, "extauth.py"]), LDIFFile = filename:join([DataDir, "ejabberd.ldif"]), {ok, _} = file:copy(ExtAuthScript, filename:join([CWD, "extauth.py"])), {ok, _} = ldap_srv:start(LDIFFile), inet_db:add_host({127,0,0,1}, [binary_to_list(?S2S_VHOST), binary_to_list(?MNESIA_VHOST), binary_to_list(?UPLOAD_VHOST)]), inet_db:set_domain(binary_to_list(p1_rand:get_string())), inet_db:set_lookup([file, native]), start_ejabberd(NewConfig), NewConfig. start_ejabberd(_) -> {ok, _} = application:ensure_all_started(ejabberd, transient). end_per_suite(_Config) -> application:stop(ejabberd). init_per_group(Group, Config) -> case lists:member(Group, ?BACKENDS) of false -> %% Not a backend related group, do default init: do_init_per_group(Group, Config); true -> case proplists:get_value(backends, Config) of all -> %% All backends enabled do_init_per_group(Group, Config); Backends -> %% Skipped backends that were not explicitely enabled case lists:member(Group, Backends) of true -> do_init_per_group(Group, Config); false -> {skip, {disabled_backend, Group}} end end end. do_init_per_group(no_db, Config) -> re_register(Config), set_opt(persistent_room, false, Config); do_init_per_group(mnesia, Config) -> mod_muc:shutdown_rooms(?MNESIA_VHOST), set_opt(server, ?MNESIA_VHOST, Config); do_init_per_group(redis, Config) -> mod_muc:shutdown_rooms(?REDIS_VHOST), set_opt(server, ?REDIS_VHOST, Config); do_init_per_group(mysql, Config) -> case catch ejabberd_sql:sql_query(?MYSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MYSQL_VHOST), clear_sql_tables(mysql, ?config(base_dir, Config)), set_opt(server, ?MYSQL_VHOST, Config); Err -> {skip, {mysql_not_available, Err}} end; do_init_per_group(mssql, Config) -> case catch ejabberd_sql:sql_query(?MSSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?MSSQL_VHOST), clear_sql_tables(mssql, ?config(base_dir, Config)), set_opt(server, ?MSSQL_VHOST, Config); Err -> {skip, {mssql_not_available, Err}} end; do_init_per_group(pgsql, Config) -> case catch ejabberd_sql:sql_query(?PGSQL_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?PGSQL_VHOST), clear_sql_tables(pgsql, ?config(base_dir, Config)), set_opt(server, ?PGSQL_VHOST, Config); Err -> {skip, {pgsql_not_available, Err}} end; do_init_per_group(sqlite, Config) -> case catch ejabberd_sql:sql_query(?SQLITE_VHOST, [<<"select 1;">>]) of {selected, _, _} -> mod_muc:shutdown_rooms(?SQLITE_VHOST), set_opt(server, ?SQLITE_VHOST, Config); Err -> {skip, {sqlite_not_available, Err}} end; do_init_per_group(ldap, Config) -> set_opt(server, ?LDAP_VHOST, Config); do_init_per_group(extauth, Config) -> set_opt(server, ?EXTAUTH_VHOST, Config); do_init_per_group(s2s, Config) -> ejabberd_config:set_option({s2s_use_starttls, ?COMMON_VHOST}, required), ejabberd_config:set_option(ca_file, "ca.pem"), Port = ?config(s2s_port, Config), set_opt(server, ?COMMON_VHOST, set_opt(xmlns, ?NS_SERVER, set_opt(type, server, set_opt(server_port, Port, set_opt(stream_from, ?S2S_VHOST, set_opt(lang, <<"">>, Config)))))); do_init_per_group(component, Config) -> Server = ?config(server, Config), Port = ?config(component_port, Config), set_opt(xmlns, ?NS_COMPONENT, set_opt(server, <<"component.", Server/binary>>, set_opt(type, component, set_opt(server_port, Port, set_opt(stream_version, undefined, set_opt(lang, <<"">>, Config)))))); do_init_per_group(GroupName, Config) -> Pid = start_event_relay(), NewConfig = set_opt(event_relay, Pid, Config), case GroupName of anonymous -> set_opt(anonymous, true, NewConfig); _ -> NewConfig end. end_per_group(mnesia, _Config) -> ok; end_per_group(redis, _Config) -> ok; end_per_group(mysql, _Config) -> ok; end_per_group(mssql, _Config) -> ok; end_per_group(pgsql, _Config) -> ok; end_per_group(sqlite, _Config) -> ok; end_per_group(no_db, _Config) -> ok; end_per_group(ldap, _Config) -> ok; end_per_group(extauth, _Config) -> ok; end_per_group(component, _Config) -> ok; end_per_group(s2s, Config) -> Server = ?config(server, Config), ejabberd_config:set_option({s2s_use_starttls, Server}, false); end_per_group(_GroupName, Config) -> stop_event_relay(Config), set_opt(anonymous, false, Config). init_per_testcase(stop_ejabberd, Config) -> NewConfig = set_opt(resource, <<"">>, set_opt(anonymous, true, Config)), open_session(bind(auth(connect(NewConfig)))); init_per_testcase(TestCase, OrigConfig) -> ct:print(80, "Testcase '~p' starting", [TestCase]), Test = atom_to_list(TestCase), IsMaster = lists:suffix("_master", Test), IsSlave = lists:suffix("_slave", Test), if IsMaster or IsSlave -> subscribe_to_events(OrigConfig); true -> ok end, TestGroup = proplists:get_value( name, ?config(tc_group_properties, OrigConfig)), Server = ?config(server, OrigConfig), Resource = case TestGroup of anonymous -> <<"">>; legacy_auth -> p1_rand:get_string(); _ -> ?config(resource, OrigConfig) end, MasterResource = ?config(master_resource, OrigConfig), SlaveResource = ?config(slave_resource, OrigConfig), Mode = if IsSlave -> slave; IsMaster -> master; true -> single end, IsCarbons = lists:prefix("carbons_", Test), IsReplaced = lists:prefix("replaced_", Test), User = if IsReplaced -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; IsCarbons and not (IsMaster or IsSlave) -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">>; IsMaster or IsCarbons -> <<"test_master!#$%^*()`~+-;_=[]{}|\\">>; IsSlave -> <<"test_slave!#$%^*()`~+-;_=[]{}|\\">>; true -> <<"test_single!#$%^*()`~+-;_=[]{}|\\">> end, Nick = if IsSlave -> ?config(slave_nick, OrigConfig); IsMaster -> ?config(master_nick, OrigConfig); true -> ?config(nick, OrigConfig) end, MyResource = if IsMaster and IsCarbons -> MasterResource; IsSlave and IsCarbons -> SlaveResource; true -> Resource end, Slave = if IsCarbons -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, SlaveResource); IsReplaced -> jid:make(User, Server, Resource); true -> jid:make(<<"test_slave!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, Master = if IsCarbons -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, MasterResource); IsReplaced -> jid:make(User, Server, Resource); true -> jid:make(<<"test_master!#$%^*()`~+-;_=[]{}|\\">>, Server, Resource) end, Config1 = set_opt(user, User, set_opt(slave, Slave, set_opt(master, Master, set_opt(resource, MyResource, set_opt(nick, Nick, set_opt(mode, Mode, OrigConfig)))))), Config2 = if IsSlave -> set_opt(peer_nick, ?config(master_nick, Config1), Config1); IsMaster -> set_opt(peer_nick, ?config(slave_nick, Config1), Config1); true -> Config1 end, Config = if IsSlave -> set_opt(peer, Master, Config2); IsMaster -> set_opt(peer, Slave, Config2); true -> Config2 end, case Test of "test_connect" ++ _ -> Config; "webadmin_" ++ _ -> Config; "test_legacy_auth_feature" -> connect(Config); "test_legacy_auth" ++ _ -> init_stream(set_opt(stream_version, undefined, Config)); "test_auth" ++ _ -> connect(Config); "test_starttls" ++ _ -> connect(Config); "test_zlib" -> auth(connect(starttls(connect(Config)))); "test_register" -> connect(Config); "auth_md5" -> connect(Config); "auth_plain" -> connect(Config); "auth_external" ++ _ -> connect(Config); "unauthenticated_" ++ _ -> connect(Config); "test_bind" -> auth(connect(Config)); "sm_resume" -> auth(connect(Config)); "sm_resume_failed" -> auth(connect(Config)); "test_open_session" -> bind(auth(connect(Config))); "replaced" ++ _ -> auth(connect(Config)); _ when IsMaster or IsSlave -> Password = ?config(password, Config), ejabberd_auth:try_register(User, Server, Password), open_session(bind(auth(connect(Config)))); _ when TestGroup == s2s_tests -> auth(connect(starttls(connect(Config)))); _ -> open_session(bind(auth(connect(Config)))) end. end_per_testcase(_TestCase, _Config) -> ok. legacy_auth_tests() -> {legacy_auth, [parallel], [test_legacy_auth_feature, test_legacy_auth, test_legacy_auth_digest, test_legacy_auth_no_resource, test_legacy_auth_bad_jid, test_legacy_auth_fail]}. no_db_tests() -> [{anonymous, [parallel], [test_connect_bad_xml, test_connect_unexpected_xml, test_connect_unknown_ns, test_connect_bad_xmlns, test_connect_bad_ns_stream, test_connect_bad_lang, test_connect_bad_to, test_connect_missing_to, test_connect, unauthenticated_iq, unauthenticated_message, unauthenticated_presence, test_starttls, test_auth, test_zlib, test_bind, test_open_session, codec_failure, unsupported_query, bad_nonza, invalid_from, ping, version, time, stats, disco]}, {presence_and_s2s, [sequence], [test_auth_fail, presence, s2s_dialback, s2s_optional, s2s_required]}, auth_external, auth_external_no_jid, auth_external_no_user, auth_external_malformed_jid, auth_external_wrong_jid, auth_external_wrong_server, auth_external_invalid_cert, jidprep_tests:single_cases(), sm_tests:single_cases(), sm_tests:master_slave_cases(), muc_tests:single_cases(), muc_tests:master_slave_cases(), proxy65_tests:single_cases(), proxy65_tests:master_slave_cases(), stundisco_tests:single_cases(), replaced_tests:master_slave_cases(), upload_tests:single_cases(), carbons_tests:single_cases(), carbons_tests:master_slave_cases()]. db_tests(DB) when DB == mnesia; DB == redis -> [{single_user, [sequence], [test_register, legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, webadmin_tests:single_cases(), roster_tests:single_cases(), private_tests:single_cases(), privacy_tests:single_cases(), vcard_tests:single_cases(), pubsub_tests:single_cases(), muc_tests:single_cases(), offline_tests:single_cases(), mam_tests:single_cases(), csi_tests:single_cases(), push_tests:single_cases(), test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_tests:master_slave_cases(), roster_tests:master_slave_cases(), offline_tests:master_slave_cases(DB), mam_tests:master_slave_cases(), vcard_tests:master_slave_cases(), announce_tests:master_slave_cases(), csi_tests:master_slave_cases(), push_tests:master_slave_cases()]; db_tests(DB) -> [{single_user, [sequence], [test_register, legacy_auth_tests(), auth_plain, auth_md5, presence_broadcast, last, webadmin_tests:single_cases(), roster_tests:single_cases(), private_tests:single_cases(), privacy_tests:single_cases(), vcard_tests:single_cases(), pubsub_tests:single_cases(), muc_tests:single_cases(), offline_tests:single_cases(), mam_tests:single_cases(), push_tests:single_cases(), test_unregister]}, muc_tests:master_slave_cases(), privacy_tests:master_slave_cases(), pubsub_tests:master_slave_cases(), roster_tests:master_slave_cases(), offline_tests:master_slave_cases(DB), mam_tests:master_slave_cases(), vcard_tests:master_slave_cases(), announce_tests:master_slave_cases(), push_tests:master_slave_cases()]. ldap_tests() -> [{ldap_tests, [sequence], [test_auth, test_auth_fail, vcard_get, ldap_shared_roster_get]}]. extauth_tests() -> [{extauth_tests, [sequence], [test_auth, test_auth_fail, test_unregister]}]. component_tests() -> [{component_connect, [parallel], [test_connect_bad_xml, test_connect_unexpected_xml, test_connect_unknown_ns, test_connect_bad_xmlns, test_connect_bad_ns_stream, test_connect_missing_to, test_connect, test_auth, test_auth_fail]}, {component_tests, [sequence], [test_missing_from, test_missing_to, test_invalid_from, test_component_send, bad_nonza, codec_failure]}]. s2s_tests() -> [{s2s_connect, [parallel], [test_connect_bad_xml, test_connect_unexpected_xml, test_connect_unknown_ns, test_connect_bad_xmlns, test_connect_bad_ns_stream, test_connect, test_connect_s2s_starttls_required, test_starttls, test_connect_s2s_unauthenticated_iq, test_auth_starttls]}, {s2s_tests, [sequence], [test_missing_from, test_missing_to, test_invalid_from, bad_nonza, codec_failure]}]. groups() -> [{ldap, [sequence], ldap_tests()}, {extauth, [sequence], extauth_tests()}, {no_db, [sequence], no_db_tests()}, {component, [sequence], component_tests()}, {s2s, [sequence], s2s_tests()}, {mnesia, [sequence], db_tests(mnesia)}, {redis, [sequence], db_tests(redis)}, {mysql, [sequence], db_tests(mysql)}, {mssql, [sequence], db_tests(mssql)}, {pgsql, [sequence], db_tests(pgsql)}, {sqlite, [sequence], db_tests(sqlite)}]. all() -> [{group, ldap}, {group, no_db}, {group, mnesia}, {group, redis}, {group, mysql}, {group, mssql}, {group, pgsql}, {group, sqlite}, {group, extauth}, {group, component}, {group, s2s}, stop_ejabberd]. stop_ejabberd(Config) -> ok = application:stop(ejabberd), ?recv1(#stream_error{reason = 'system-shutdown'}), case suite:recv(Config) of {xmlstreamend, <<"stream:stream">>} -> ok; closed -> ok; Other -> suite:match_failure([Other], [closed]) end, Config. test_connect_bad_xml(Config) -> Config0 = tcp_connect(Config), send_text(Config0, <<"<'/>">>), Version = ?config(stream_version, Config0), ?recv1(#stream_start{version = Version}), ?recv1(#stream_error{reason = 'not-well-formed'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_unexpected_xml(Config) -> Config0 = tcp_connect(Config), send(Config0, #caps{}), Version = ?config(stream_version, Config0), ?recv1(#stream_start{version = Version}), ?recv1(#stream_error{reason = 'invalid-xml'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_unknown_ns(Config) -> Config0 = init_stream(set_opt(xmlns, <<"wrong">>, Config)), ?recv1(#stream_error{reason = 'invalid-xml'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_bad_xmlns(Config) -> NS = case ?config(type, Config) of client -> ?NS_SERVER; _ -> ?NS_CLIENT end, Config0 = init_stream(set_opt(xmlns, NS, Config)), ?recv1(#stream_error{reason = 'invalid-namespace'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_bad_ns_stream(Config) -> Config0 = init_stream(set_opt(ns_stream, <<"wrong">>, Config)), ?recv1(#stream_error{reason = 'invalid-namespace'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_bad_lang(Config) -> Lang = iolist_to_binary(lists:duplicate(36, $x)), Config0 = init_stream(set_opt(lang, Lang, Config)), ?recv1(#stream_error{reason = 'invalid-xml'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_bad_to(Config) -> Config0 = init_stream(set_opt(server, <<"wrong.com">>, Config)), ?recv1(#stream_error{reason = 'host-unknown'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect_missing_to(Config) -> Config0 = init_stream(set_opt(server, <<"">>, Config)), ?recv1(#stream_error{reason = 'improper-addressing'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config0). test_connect(Config) -> disconnect(connect(Config)). test_connect_s2s_starttls_required(Config) -> Config1 = connect(Config), send(Config1, #presence{}), ?recv1(#stream_error{reason = 'policy-violation'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config1). test_connect_s2s_unauthenticated_iq(Config) -> Config1 = connect(starttls(connect(Config))), unauthenticated_iq(Config1). test_starttls(Config) -> case ?config(starttls, Config) of true -> disconnect(connect(starttls(Config))); _ -> {skipped, 'starttls_not_available'} end. test_zlib(Config) -> case ?config(compression, Config) of [_|_] = Ms -> case lists:member(<<"zlib">>, Ms) of true -> disconnect(zlib(Config)); false -> {skipped, 'zlib_not_available'} end; _ -> {skipped, 'compression_not_available'} end. test_register(Config) -> case ?config(register, Config) of true -> disconnect(register(Config)); _ -> {skipped, 'registration_not_available'} end. register(Config) -> #iq{type = result, sub_els = [#register{username = <<>>, password = <<>>}]} = send_recv(Config, #iq{type = get, to = server_jid(Config), sub_els = [#register{}]}), #iq{type = result, sub_els = []} = send_recv( Config, #iq{type = set, sub_els = [#register{username = ?config(user, Config), password = ?config(password, Config)}]}), Config. test_unregister(Config) -> case ?config(register, Config) of true -> try_unregister(Config); _ -> {skipped, 'registration_not_available'} end. try_unregister(Config) -> true = is_feature_advertised(Config, ?NS_REGISTER), #iq{type = result, sub_els = []} = send_recv( Config, #iq{type = set, sub_els = [#register{remove = true}]}), ?recv1(#stream_error{reason = conflict}), Config. unauthenticated_presence(Config) -> unauthenticated_packet(Config, #presence{}). unauthenticated_message(Config) -> unauthenticated_packet(Config, #message{}). unauthenticated_iq(Config) -> IQ = #iq{type = get, sub_els = [#disco_info{}]}, unauthenticated_packet(Config, IQ). unauthenticated_packet(Config, Pkt) -> From = my_jid(Config), To = server_jid(Config), send(Config, xmpp:set_from_to(Pkt, From, To)), #stream_error{reason = 'not-authorized'} = recv(Config), {xmlstreamend, <<"stream:stream">>} = recv(Config), close_socket(Config). bad_nonza(Config) -> %% Unsupported and invalid nonza should be silently dropped. send(Config, #caps{}), send(Config, #stanza_error{type = wrong}), disconnect(Config). invalid_from(Config) -> send(Config, #message{from = jid:make(p1_rand:get_string())}), ?recv1(#stream_error{reason = 'invalid-from'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). test_missing_from(Config) -> Server = server_jid(Config), send(Config, #message{to = Server}), ?recv1(#stream_error{reason = 'improper-addressing'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). test_missing_to(Config) -> Server = server_jid(Config), send(Config, #message{from = Server}), ?recv1(#stream_error{reason = 'improper-addressing'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). test_invalid_from(Config) -> From = jid:make(p1_rand:get_string()), To = jid:make(p1_rand:get_string()), send(Config, #message{from = From, to = To}), ?recv1(#stream_error{reason = 'invalid-from'}), ?recv1({xmlstreamend, <<"stream:stream">>}), close_socket(Config). test_component_send(Config) -> To = jid:make(?COMMON_VHOST), From = server_jid(Config), #iq{type = result, from = To, to = From} = send_recv(Config, #iq{type = get, to = To, from = From, sub_els = [#ping{}]}), disconnect(Config). s2s_dialback(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), ejabberd_config:set_option({s2s_use_starttls, Server}, false), ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, false), ejabberd_config:set_option(ca_file, pkix:get_cafile()), s2s_ping(Config). s2s_optional(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), ejabberd_config:set_option({s2s_use_starttls, Server}, optional), ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, optional), ejabberd_config:set_option(ca_file, pkix:get_cafile()), s2s_ping(Config). s2s_required(Config) -> Server = ?config(server, Config), ejabberd_s2s:stop_s2s_connections(), gen_mod:stop_module(Server, mod_s2s_dialback), gen_mod:stop_module(?MNESIA_VHOST, mod_s2s_dialback), ejabberd_config:set_option({s2s_use_starttls, Server}, required), ejabberd_config:set_option({s2s_use_starttls, ?MNESIA_VHOST}, required), ejabberd_config:set_option(ca_file, "ca.pem"), s2s_ping(Config). s2s_ping(Config) -> From = my_jid(Config), To = jid:make(?MNESIA_VHOST), ID = p1_rand:get_string(), ejabberd_s2s:route(#iq{from = From, to = To, id = ID, type = get, sub_els = [#ping{}]}), #iq{type = result, id = ID, sub_els = []} = recv_iq(Config), disconnect(Config). auth_md5(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"DIGEST-MD5">>, Mechs) of true -> disconnect(auth_SASL(<<"DIGEST-MD5">>, Config)); false -> disconnect(Config), {skipped, 'DIGEST-MD5_not_available'} end. auth_plain(Config) -> Mechs = ?config(mechs, Config), case lists:member(<<"PLAIN">>, Mechs) of true -> disconnect(auth_SASL(<<"PLAIN">>, Config)); false -> disconnect(Config), {skipped, 'PLAIN_not_available'} end. auth_external(Config0) -> Config = connect(starttls(Config0)), disconnect(auth_SASL(<<"EXTERNAL">>, Config)). auth_external_no_jid(Config0) -> Config = connect(starttls(Config0)), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShoudFail = false, {<<"">>, <<"">>, <<"">>})). auth_external_no_user(Config0) -> Config = set_opt(user, <<"">>, connect(starttls(Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config)). auth_external_malformed_jid(Config0) -> Config = connect(starttls(Config0)), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, {<<"">>, <<"@">>, <<"">>})). auth_external_wrong_jid(Config0) -> Config = set_opt(user, <<"wrong">>, connect(starttls(Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). auth_external_wrong_server(Config0) -> Config = connect(starttls(Config0)), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true, {<<"">>, <<"wrong.com">>, <<"">>})). auth_external_invalid_cert(Config0) -> Config = connect(starttls( set_opt(certfile, "self-signed-cert.pem", Config0))), disconnect(auth_SASL(<<"EXTERNAL">>, Config, _ShouldFail = true)). test_legacy_auth_feature(Config) -> true = ?config(legacy_auth, Config), disconnect(Config). test_legacy_auth(Config) -> disconnect(auth_legacy(Config, _Digest = false)). test_legacy_auth_digest(Config) -> disconnect(auth_legacy(Config, _Digest = true)). test_legacy_auth_no_resource(Config0) -> Config = set_opt(resource, <<"">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). test_legacy_auth_bad_jid(Config0) -> Config = set_opt(user, <<"@">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). test_legacy_auth_fail(Config0) -> Config = set_opt(user, <<"wrong">>, Config0), disconnect(auth_legacy(Config, _Digest = false, _ShouldFail = true)). test_auth(Config) -> disconnect(auth(Config)). test_auth_starttls(Config) -> disconnect(auth(connect(starttls(Config)))). test_auth_fail(Config0) -> Config = set_opt(user, <<"wrong">>, set_opt(password, <<"wrong">>, Config0)), disconnect(auth(Config, _ShouldFail = true)). test_bind(Config) -> disconnect(bind(Config)). test_open_session(Config) -> disconnect(open_session(Config, true)). codec_failure(Config) -> JID = my_jid(Config), #iq{type = error} = send_recv(Config, #iq{type = wrong, from = JID, to = JID}), disconnect(Config). unsupported_query(Config) -> ServerJID = server_jid(Config), #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID}), #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [#caps{}]}), #iq{type = error} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [#roster_query{}, #disco_info{}, #privacy_query{}]}), disconnect(Config). presence(Config) -> JID = my_jid(Config), #presence{from = JID, to = JID} = send_recv(Config, #presence{}), disconnect(Config). presence_broadcast(Config) -> Feature = <<"p1:tmp:", (p1_rand:get_string())/binary>>, Ver = crypto:hash(sha, ["client", $/, "bot", $/, "en", $/, "ejabberd_ct", $<, Feature, $<]), B64Ver = base64:encode(Ver), Node = <<(?EJABBERD_CT_URI)/binary, $#, B64Ver/binary>>, Server = ?config(server, Config), Info = #disco_info{identities = [#identity{category = <<"client">>, type = <<"bot">>, lang = <<"en">>, name = <<"ejabberd_ct">>}], node = Node, features = [Feature]}, Caps = #caps{hash = <<"sha-1">>, node = ?EJABBERD_CT_URI, version = B64Ver}, send(Config, #presence{sub_els = [Caps]}), JID = my_jid(Config), %% We receive: %% 1) disco#info iq request for CAPS %% 2) welcome message %% 3) presence broadcast IQ = #iq{type = get, from = JID, sub_els = [#disco_info{node = Node}]} = recv_iq(Config), #message{type = normal} = recv_message(Config), #presence{from = JID, to = JID} = recv_presence(Config), send(Config, #iq{type = result, id = IQ#iq.id, to = JID, sub_els = [Info]}), %% We're trying to read our feature from ejabberd database %% with exponential back-off as our IQ response may be delayed. [Feature] = lists:foldl( fun(Time, []) -> timer:sleep(Time), mod_caps:get_features(Server, Caps); (_, Acc) -> Acc end, [], [0, 100, 200, 2000, 5000, 10000]), disconnect(Config). ping(Config) -> true = is_feature_advertised(Config, ?NS_PING), #iq{type = result, sub_els = []} = send_recv( Config, #iq{type = get, sub_els = [#ping{}], to = server_jid(Config)}), disconnect(Config). version(Config) -> true = is_feature_advertised(Config, ?NS_VERSION), #iq{type = result, sub_els = [#version{}]} = send_recv( Config, #iq{type = get, sub_els = [#version{}], to = server_jid(Config)}), disconnect(Config). time(Config) -> true = is_feature_advertised(Config, ?NS_TIME), #iq{type = result, sub_els = [#time{}]} = send_recv(Config, #iq{type = get, sub_els = [#time{}], to = server_jid(Config)}), disconnect(Config). disco(Config) -> true = is_feature_advertised(Config, ?NS_DISCO_INFO), true = is_feature_advertised(Config, ?NS_DISCO_ITEMS), #iq{type = result, sub_els = [#disco_items{items = Items}]} = send_recv( Config, #iq{type = get, sub_els = [#disco_items{}], to = server_jid(Config)}), lists:foreach( fun(#disco_item{jid = JID, node = Node}) -> #iq{type = result} = send_recv(Config, #iq{type = get, to = JID, sub_els = [#disco_info{node = Node}]}) end, Items), disconnect(Config). last(Config) -> true = is_feature_advertised(Config, ?NS_LAST), #iq{type = result, sub_els = [#last{}]} = send_recv(Config, #iq{type = get, sub_els = [#last{}], to = server_jid(Config)}), disconnect(Config). vcard_get(Config) -> true = is_feature_advertised(Config, ?NS_VCARD), %% TODO: check if VCard corresponds to LDIF data from ejabberd.ldif #iq{type = result, sub_els = [_VCard]} = send_recv(Config, #iq{type = get, sub_els = [#vcard_temp{}]}), disconnect(Config). ldap_shared_roster_get(Config) -> Item = #roster_item{jid = jid:decode(<<"user2@ldap.localhost">>), name = <<"Test User 2">>, groups = [<<"group1">>], subscription = both}, #iq{type = result, sub_els = [#roster_query{items = [Item]}]} = send_recv(Config, #iq{type = get, sub_els = [#roster_query{}]}), disconnect(Config). stats(Config) -> #iq{type = result, sub_els = [#stats{list = Stats}]} = send_recv(Config, #iq{type = get, sub_els = [#stats{}], to = server_jid(Config)}), lists:foreach( fun(#stat{} = Stat) -> #iq{type = result, sub_els = [_|_]} = send_recv(Config, #iq{type = get, sub_els = [#stats{list = [Stat]}], to = server_jid(Config)}) end, Stats), disconnect(Config). %%%=================================================================== %%% Aux functions %%%=================================================================== bookmark_conference() -> #bookmark_conference{name = <<"Some name">>, autojoin = true, jid = jid:make( <<"some">>, <<"some.conference.org">>, <<>>)}. '$handle_undefined_function'(F, [Config]) when is_list(Config) -> case re:split(atom_to_list(F), "_", [{return, list}, {parts, 2}]) of [M, T] -> Module = list_to_atom(M ++ "_tests"), Function = list_to_atom(T), case erlang:function_exported(Module, Function, 1) of true -> Module:Function(Config); false -> erlang:error({undef, F}) end; _ -> erlang:error({undef, F}) end; '$handle_undefined_function'(_, _) -> erlang:error(undef). %%%=================================================================== %%% SQL stuff %%%=================================================================== clear_sql_tables(sqlite, _BaseDir) -> ok; clear_sql_tables(Type, BaseDir) -> {VHost, File} = case Type of mysql -> Path = case ejabberd_sql:use_new_schema() of true -> "mysql.new.sql"; false -> "mysql.sql" end, {?MYSQL_VHOST, Path}; mssql -> Path = case ejabberd_sql:use_new_schema() of true -> "mssql.new.sql"; false -> "mssql.sql" end, {?MSSQL_VHOST, Path}; pgsql -> Path = case ejabberd_sql:use_new_schema() of true -> "pg.new.sql"; false -> "pg.sql" end, {?PGSQL_VHOST, Path} end, SQLFile = filename:join([BaseDir, "sql", File]), CreationQueries = read_sql_queries(SQLFile), ClearTableQueries = clear_table_queries(CreationQueries), case ejabberd_sql:sql_transaction( VHost, ClearTableQueries) of {atomic, ok} -> ok; Err -> ct:fail({failed_to_clear_sql_tables, Type, Err}) end. read_sql_queries(File) -> case file:open(File, [read, binary]) of {ok, Fd} -> read_lines(Fd, File, []); Err -> ct:fail({open_file_failed, File, Err}) end. clear_table_queries(Queries) -> lists:foldl( fun(Query, Acc) -> case split(str:to_lower(Query)) of [<<"create">>, <<"table">>, Table|_] -> [<<"DELETE FROM ", Table/binary, ";">>|Acc]; _ -> Acc end end, [], Queries). read_lines(Fd, File, Acc) -> case file:read_line(Fd) of {ok, Line} -> NewAcc = case str:strip(str:strip(Line, both, $\r), both, $\n) of <<"--", _/binary>> -> Acc; <<>> -> Acc; _ -> [Line|Acc] end, read_lines(Fd, File, NewAcc); eof -> QueryList = str:tokens(list_to_binary(lists:reverse(Acc)), <<";">>), lists:flatmap( fun(Query) -> case str:strip(str:strip(Query, both, $\r), both, $\n) of <<>> -> []; Q -> [<<Q/binary, $;>>] end end, QueryList); {error, _} = Err -> ct:fail({read_file_failed, File, Err}) end. split(Data) -> lists:filter( fun(<<>>) -> false; (_) -> true end, re:split(Data, <<"\s">>)). ������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�017350� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/ejabberd_logger.exs�����������������������������������������������0000644�0002322�0002322�00000002336�14154362354�023172� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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_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_configure, _mod_time] = Store.get(:modules) fun = fn -> mod_configure |> Validation.validate |> EjabberdLogger.log_errors end assert capture_io(fun) == error_msg end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/attr_test.exs�����������������������������������������������������0000644�0002322�0002322�00000004767�14154362354�022120� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ���������ejabberd-21.12/test/elixir-config/validation_test.exs�����������������������������������������������0000644�0002322�0002322�00000001525�14154362354�023265� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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_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 end end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/config_test.exs���������������������������������������������������0000644�0002322�0002322�00000004226�14154362354�022401� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/shared/�����������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�020616� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/shared/ejabberd.exs�����������������������������������������������0000644�0002322�0002322�00000001044�14154362354�023074� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/shared/ejabberd_for_validation.exs��������������������������������0000644�0002322�0002322�00000000415�14154362354�026155� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/elixir-config/shared/ejabberd_different_from_default.exs������������������������0000644�0002322�0002322�00000000223�14154362354�027647� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������defmodule Ejabberd.ConfigFile do use Ejabberd.Config def start do [loglevel: 4, language: "en", hosts: ["localhost"]] end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/privacy_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000073116�14154362354�020224� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 18 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(privacy_tests). %% API -compile(export_all). -import(suite, [disconnect/1, send_recv/2, get_event/1, put_event/2, recv_iq/1, recv_presence/1, recv_message/1, recv/1, send/2, my_jid/1, server_jid/1, get_features/1, set_roster/3, del_roster/1, get_roster/1]). -include("suite.hrl"). -include("mod_roster.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single cases %%%=================================================================== single_cases() -> {privacy_single, [sequence], [single_test(feature_enabled), single_test(set_get_list), single_test(get_list_non_existent), single_test(get_empty_lists), single_test(set_default), single_test(del_default), single_test(set_default_non_existent), single_test(set_active), single_test(del_active), single_test(set_active_non_existent), single_test(remove_list), single_test(remove_default_list), single_test(remove_active_list), single_test(remove_list_non_existent), single_test(allow_local_server), single_test(malformed_iq_query), single_test(malformed_get), single_test(malformed_set), single_test(malformed_type_value), single_test(set_get_block)]}. feature_enabled(Config) -> Features = get_features(Config), true = lists:member(?NS_PRIVACY, Features), true = lists:member(?NS_BLOCKING, Features), disconnect(Config). set_get_list(Config) -> ListName = <<"set-get-list">>, Items = [#privacy_item{order = 0, action = deny, type = jid, value = <<"user@jabber.org">>, iq = true}, #privacy_item{order = 1, action = allow, type = group, value = <<"group">>, message = true}, #privacy_item{order = 2, action = allow, type = subscription, value = <<"both">>, presence_in = true}, #privacy_item{order = 3, action = deny, type = subscription, value = <<"from">>, presence_out = true}, #privacy_item{order = 4, action = deny, type = subscription, value = <<"to">>, iq = true, message = true}, #privacy_item{order = 5, action = deny, type = subscription, value = <<"none">>, _ = true}, #privacy_item{order = 6, action = deny}], ok = set_items(Config, ListName, Items), #privacy_list{name = ListName, items = Items1} = get_list(Config, ListName), Items = lists:keysort(#privacy_item.order, Items1), del_privacy(disconnect(Config)). get_list_non_existent(Config) -> ListName = <<"get-list-non-existent">>, #stanza_error{reason = 'item-not-found'} = get_list(Config, ListName), disconnect(Config). get_empty_lists(Config) -> #privacy_query{default = none, active = none, lists = []} = get_lists(Config), disconnect(Config). set_default(Config) -> ListName = <<"set-default">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_default(Config, ListName), #privacy_query{default = ListName} = get_lists(Config), del_privacy(disconnect(Config)). del_default(Config) -> ListName = <<"del-default">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_default(Config, ListName), #privacy_query{default = ListName} = get_lists(Config), ok = set_default(Config, none), #privacy_query{default = none} = get_lists(Config), del_privacy(disconnect(Config)). set_default_non_existent(Config) -> ListName = <<"set-default-non-existent">>, #stanza_error{reason = 'item-not-found'} = set_default(Config, ListName), disconnect(Config). set_active(Config) -> ListName = <<"set-active">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_active(Config, ListName), #privacy_query{active = ListName} = get_lists(Config), del_privacy(disconnect(Config)). del_active(Config) -> ListName = <<"del-active">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_active(Config, ListName), #privacy_query{active = ListName} = get_lists(Config), ok = set_active(Config, none), #privacy_query{active = none} = get_lists(Config), del_privacy(disconnect(Config)). set_active_non_existent(Config) -> ListName = <<"set-active-non-existent">>, #stanza_error{reason = 'item-not-found'} = set_active(Config, ListName), disconnect(Config). remove_list(Config) -> ListName = <<"remove-list">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = del_list(Config, ListName), #privacy_query{lists = []} = get_lists(Config), del_privacy(disconnect(Config)). remove_active_list(Config) -> ListName = <<"remove-active-list">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_active(Config, ListName), #stanza_error{reason = 'conflict'} = del_list(Config, ListName), del_privacy(disconnect(Config)). remove_default_list(Config) -> ListName = <<"remove-default-list">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_default(Config, ListName), #stanza_error{reason = 'conflict'} = del_list(Config, ListName), del_privacy(disconnect(Config)). remove_list_non_existent(Config) -> ListName = <<"remove-list-non-existent">>, #stanza_error{reason = 'item-not-found'} = del_list(Config, ListName), disconnect(Config). allow_local_server(Config) -> ListName = <<"allow-local-server">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_active(Config, ListName), %% Whatever privacy rules are set, we should always communicate %% with our home server server_send_iqs(Config), server_recv_iqs(Config), send_stanzas_to_server_resource(Config), del_privacy(disconnect(Config)). malformed_iq_query(Config) -> lists:foreach( fun(Type) -> #iq{type = error} = send_recv(Config, #iq{type = Type, sub_els = [#privacy_list{name = <<"foo">>}]}) end, [get, set]), disconnect(Config). malformed_get(Config) -> JID = jid:make(p1_rand:get_string()), Item = #block_item{jid = JID}, lists:foreach( fun(SubEl) -> #iq{type = error} = send_recv(Config, #iq{type = get, sub_els = [SubEl]}) end, [#privacy_query{active = none}, #privacy_query{default = none}, #privacy_query{lists = [#privacy_list{name = <<"1">>}, #privacy_list{name = <<"2">>}]}, #block{items = [Item]}, #unblock{items = [Item]}, #block{}, #unblock{}]), disconnect(Config). malformed_set(Config) -> lists:foreach( fun(SubEl) -> #iq{type = error} = send_recv(Config, #iq{type = set, sub_els = [SubEl]}) end, [#privacy_query{active = none, default = none}, #privacy_query{lists = [#privacy_list{name = <<"1">>}, #privacy_list{name = <<"2">>}]}, #block{}, #block_list{}, #block_list{ items = [#block_item{ jid = jid:make(p1_rand:get_string())}]}]), disconnect(Config). malformed_type_value(Config) -> Item = #privacy_item{order = 0, action = deny}, #stanza_error{reason = 'bad-request'} = set_items(Config, <<"malformed-jid">>, [Item#privacy_item{type = jid, value = <<"@bad">>}]), #stanza_error{reason = 'bad-request'} = set_items(Config, <<"malformed-group">>, [Item#privacy_item{type = group, value = <<"">>}]), #stanza_error{reason = 'bad-request'} = set_items(Config, <<"malformed-subscription">>, [Item#privacy_item{type = subscription, value = <<"bad">>}]), disconnect(Config). set_get_block(Config) -> J1 = jid:make(p1_rand:get_string(), p1_rand:get_string()), J2 = jid:make(p1_rand:get_string(), p1_rand:get_string()), {ok, ListName} = set_block(Config, [J1, J2]), JIDs = get_block(Config), JIDs = lists:sort([J1, J2]), {ok, ListName} = set_unblock(Config, [J2, J1]), [] = get_block(Config), del_privacy(disconnect(Config)). %%%=================================================================== %%% Master-slave cases %%%=================================================================== master_slave_cases() -> {privacy_master_slave, [sequence], [master_slave_test(deny_bare_jid), master_slave_test(deny_full_jid), master_slave_test(deny_server_bare_jid), master_slave_test(deny_server_full_jid), master_slave_test(deny_group), master_slave_test(deny_sub_both), master_slave_test(deny_sub_from), master_slave_test(deny_sub_to), master_slave_test(deny_sub_none), master_slave_test(deny_all), master_slave_test(deny_offline), master_slave_test(block), master_slave_test(unblock), master_slave_test(unblock_all)]}. deny_bare_jid_master(Config) -> PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), deny_master(Config, {jid, jid:encode(PeerBareJID)}). deny_bare_jid_slave(Config) -> deny_slave(Config). deny_full_jid_master(Config) -> PeerJID = ?config(peer, Config), deny_master(Config, {jid, jid:encode(PeerJID)}). deny_full_jid_slave(Config) -> deny_slave(Config). deny_server_bare_jid_master(Config) -> {_, Server, _} = jid:tolower(?config(peer, Config)), deny_master(Config, {jid, Server}). deny_server_bare_jid_slave(Config) -> deny_slave(Config). deny_server_full_jid_master(Config) -> {_, Server, Resource} = jid:tolower(?config(peer, Config)), deny_master(Config, {jid, jid:encode({<<"">>, Server, Resource})}). deny_server_full_jid_slave(Config) -> deny_slave(Config). deny_group_master(Config) -> Group = p1_rand:get_string(), deny_master(Config, {group, Group}). deny_group_slave(Config) -> deny_slave(Config). deny_sub_both_master(Config) -> deny_master(Config, {subscription, <<"both">>}). deny_sub_both_slave(Config) -> deny_slave(Config, 2). deny_sub_from_master(Config) -> deny_master(Config, {subscription, <<"from">>}). deny_sub_from_slave(Config) -> deny_slave(Config, 1). deny_sub_to_master(Config) -> deny_master(Config, {subscription, <<"to">>}). deny_sub_to_slave(Config) -> deny_slave(Config, 2). deny_sub_none_master(Config) -> deny_master(Config, {subscription, <<"none">>}). deny_sub_none_slave(Config) -> deny_slave(Config). deny_all_master(Config) -> deny_master(Config, {undefined, <<"">>}). deny_all_slave(Config) -> deny_slave(Config). deny_master(Config, {Type, Value}) -> Sub = if Type == subscription -> erlang:binary_to_atom(Value, utf8); true -> both end, Groups = if Type == group -> [Value]; true -> [] end, set_roster(Config, Sub, Groups), lists:foreach( fun(Opts) -> ct:pal("Set list for ~s, ~s, ~w", [Type, Value, Opts]), ListName = p1_rand:get_string(), Item = #privacy_item{order = 0, action = deny, iq = proplists:get_bool(iq, Opts), message = proplists:get_bool(message, Opts), presence_in = proplists:get_bool(presence_in, Opts), presence_out = proplists:get_bool(presence_out, Opts), type = Type, value = Value}, ok = set_items(Config, ListName, [Item]), ok = set_active(Config, ListName), put_event(Config, Opts), case is_presence_in_blocked(Opts) of true -> ok; false -> recv_presences(Config) end, case is_iq_in_blocked(Opts) of true -> ok; false -> recv_iqs(Config) end, case is_message_in_blocked(Opts) of true -> ok; false -> recv_messages(Config) end, ct:comment("Waiting for 'send' command from the slave"), send = get_event(Config), case is_presence_out_blocked(Opts) of true -> check_presence_blocked(Config, 'not-acceptable'); false -> ok end, case is_iq_out_blocked(Opts) of true -> check_iq_blocked(Config, 'not-acceptable'); false -> send_iqs(Config) end, case is_message_out_blocked(Opts) of true -> check_message_blocked(Config, 'not-acceptable'); false -> send_messages(Config) end, case is_other_blocked(Opts) of true -> check_other_blocked(Config, 'not-acceptable', Value); false -> ok end, ct:comment("Waiting for slave to finish processing our stanzas"), done = get_event(Config) end, [[iq], [message], [presence_in], [presence_out], [iq, message, presence_in, presence_out], []]), put_event(Config, disconnect), clean_up(disconnect(Config)). deny_slave(Config) -> deny_slave(Config, 0). deny_slave(Config, RosterPushesCount) -> set_roster(Config, both, []), deny_slave(Config, RosterPushesCount, get_event(Config)). deny_slave(Config, RosterPushesCount, disconnect) -> recv_roster_pushes(Config, RosterPushesCount), clean_up(disconnect(Config)); deny_slave(Config, RosterPushesCount, Opts) -> send_presences(Config), case is_iq_in_blocked(Opts) of true -> check_iq_blocked(Config, 'service-unavailable'); false -> send_iqs(Config) end, case is_message_in_blocked(Opts) of true -> check_message_blocked(Config, 'service-unavailable'); false -> send_messages(Config) end, put_event(Config, send), case is_iq_out_blocked(Opts) of true -> ok; false -> recv_iqs(Config) end, case is_message_out_blocked(Opts) of true -> ok; false -> recv_messages(Config) end, put_event(Config, done), deny_slave(Config, RosterPushesCount, get_event(Config)). deny_offline_master(Config) -> set_roster(Config, both, []), ListName = <<"deny-offline">>, Item = #privacy_item{order = 0, action = deny}, ok = set_items(Config, ListName, [Item]), ok = set_default(Config, ListName), NewConfig = disconnect(Config), put_event(NewConfig, send), ct:comment("Waiting for the slave to finish"), done = get_event(NewConfig), clean_up(NewConfig). deny_offline_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from the master"), send = get_event(Config), send_presences(Config), check_iq_blocked(Config, 'service-unavailable'), check_message_blocked(Config, 'service-unavailable'), put_event(Config, done), clean_up(disconnect(Config)). block_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), {ok, _} = set_block(Config, [PeerJID]), check_presence_blocked(Config, 'not-acceptable'), check_iq_blocked(Config, 'not-acceptable'), check_message_blocked(Config, 'not-acceptable'), check_other_blocked(Config, 'not-acceptable', other), %% We should always be able to communicate with our home server server_send_iqs(Config), server_recv_iqs(Config), send_stanzas_to_server_resource(Config), put_event(Config, send), done = get_event(Config), clean_up(disconnect(Config)). block_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), send = get_event(Config), send_presences(Config), check_iq_blocked(Config, 'service-unavailable'), check_message_blocked(Config, 'service-unavailable'), put_event(Config, done), clean_up(disconnect(Config)). unblock_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), {ok, ListName} = set_block(Config, [PeerJID]), {ok, ListName} = set_unblock(Config, [PeerJID]), put_event(Config, send), recv_presences(Config), recv_iqs(Config), recv_messages(Config), clean_up(disconnect(Config)). unblock_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), send = get_event(Config), send_presences(Config), send_iqs(Config), send_messages(Config), clean_up(disconnect(Config)). unblock_all_master(Config) -> PeerJID = ?config(peer, Config), set_roster(Config, both, []), {ok, ListName} = set_block(Config, [PeerJID]), {ok, ListName} = set_unblock(Config, []), put_event(Config, send), recv_presences(Config), recv_iqs(Config), recv_messages(Config), clean_up(disconnect(Config)). unblock_all_slave(Config) -> set_roster(Config, both, []), ct:comment("Waiting for 'send' command from master"), send = get_event(Config), send_presences(Config), send_iqs(Config), send_messages(Config), clean_up(disconnect(Config)). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("privacy_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("privacy_" ++ atom_to_list(T)), [parallel], [list_to_atom("privacy_" ++ atom_to_list(T) ++ "_master"), list_to_atom("privacy_" ++ atom_to_list(T) ++ "_slave")]}. set_items(Config, Name, Items) -> ct:comment("Setting privacy list ~s with items = ~p", [Name, Items]), case send_recv( Config, #iq{type = set, sub_els = [#privacy_query{ lists = [#privacy_list{ name = Name, items = Items}]}]}) of #iq{type = result, sub_els = []} -> ct:comment("Receiving privacy list push"), #iq{type = set, id = ID, sub_els = [#privacy_query{lists = [#privacy_list{ name = Name}]}]} = recv_iq(Config), send(Config, #iq{type = result, id = ID}), ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. get_list(Config, Name) -> ct:comment("Requesting privacy list ~s", [Name]), case send_recv(Config, #iq{type = get, sub_els = [#privacy_query{ lists = [#privacy_list{name = Name}]}]}) of #iq{type = result, sub_els = [#privacy_query{lists = [List]}]} -> List; #iq{type = error} = Err -> xmpp:get_error(Err) end. get_lists(Config) -> ct:comment("Requesting privacy lists"), case send_recv(Config, #iq{type = get, sub_els = [#privacy_query{}]}) of #iq{type = result, sub_els = [SubEl]} -> SubEl; #iq{type = error} = Err -> xmpp:get_error(Err) end. del_list(Config, Name) -> case send_recv( Config, #iq{type = set, sub_els = [#privacy_query{ lists = [#privacy_list{ name = Name}]}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. set_active(Config, Name) -> ct:comment("Setting active privacy list ~s", [Name]), case send_recv( Config, #iq{type = set, sub_els = [#privacy_query{active = Name}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. set_default(Config, Name) -> ct:comment("Setting default privacy list ~s", [Name]), case send_recv( Config, #iq{type = set, sub_els = [#privacy_query{default = Name}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. get_block(Config) -> case send_recv(Config, #iq{type = get, sub_els = [#block_list{}]}) of #iq{type = result, sub_els = [#block_list{items = Items}]} -> lists:sort([JID || #block_item{jid = JID} <- Items]); #iq{type = error} = Err -> xmpp:get_error(Err) end. set_block(Config, JIDs) -> Items = [#block_item{jid = JID} || JID <- JIDs], case send_recv(Config, #iq{type = set, sub_els = [#block{items = Items}]}) of #iq{type = result, sub_els = []} -> {#iq{id = I1, sub_els = [#block{items = Items1}]}, #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = ?recv2(#iq{type = set, sub_els = [#block{}]}, #iq{type = set, sub_els = [#privacy_query{}]}), send(Config, #iq{type = result, id = I1}), send(Config, #iq{type = result, id = I2}), ct:comment("Checking if all JIDs present in the push"), true = lists:sort(Items) == lists:sort(Items1), ct:comment("Getting name of the corresponding privacy list"), [#privacy_list{name = Name}] = Lists, {ok, Name}; #iq{type = error} = Err -> xmpp:get_error(Err) end. set_unblock(Config, JIDs) -> ct:comment("Unblocking ~p", [JIDs]), Items = [#block_item{jid = JID} || JID <- JIDs], case send_recv(Config, #iq{type = set, sub_els = [#unblock{items = Items}]}) of #iq{type = result, sub_els = []} -> {#iq{id = I1, sub_els = [#unblock{items = Items1}]}, #iq{id = I2, sub_els = [#privacy_query{lists = Lists}]}} = ?recv2(#iq{type = set, sub_els = [#unblock{}]}, #iq{type = set, sub_els = [#privacy_query{}]}), send(Config, #iq{type = result, id = I1}), send(Config, #iq{type = result, id = I2}), ct:comment("Checking if all JIDs present in the push"), true = lists:sort(Items) == lists:sort(Items1), ct:comment("Getting name of the corresponding privacy list"), [#privacy_list{name = Name}] = Lists, {ok, Name}; #iq{type = error} = Err -> xmpp:get_error(Err) end. del_privacy(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), ct:comment("Removing all privacy data"), mod_privacy:remove_user(U, S), Config. clean_up(Config) -> del_privacy(del_roster(Config)). check_iq_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), ct:comment("Checking if all IQs are blocked"), lists:foreach( fun(Type) -> send(Config, #iq{type = Type, to = PeerJID}) end, [error, result]), lists:foreach( fun(Type) -> #iq{type = error} = Err = send_recv(Config, #iq{type = Type, to = PeerJID, sub_els = [#ping{}]}), #stanza_error{reason = Reason} = xmpp:get_error(Err) end, [set, get]). check_message_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), ct:comment("Checking if all messages are blocked"), %% TODO: do something with headline and groupchat. %% The hack from 64d96778b452aad72349b21d2ac94e744617b07a %% screws this up. lists:foreach( fun(Type) -> send(Config, #message{type = Type, to = PeerJID}) end, [error]), lists:foreach( fun(Type) -> #message{type = error} = Err = send_recv(Config, #message{type = Type, to = PeerJID}), #stanza_error{reason = Reason} = xmpp:get_error(Err) end, [chat, normal]). check_presence_blocked(Config, Reason) -> PeerJID = ?config(peer, Config), ct:comment("Checking if all presences are blocked"), lists:foreach( fun(Type) -> #presence{type = error} = Err = send_recv(Config, #presence{type = Type, to = PeerJID}), #stanza_error{reason = Reason} = xmpp:get_error(Err) end, [available, unavailable]). recv_roster_pushes(_Config, 0) -> ok; recv_roster_pushes(Config, Count) -> receive #iq{type = set, sub_els = [#roster_query{}]} -> recv_roster_pushes(Config, Count - 1) end. recv_err_and_roster_pushes(Config, Count) -> recv_roster_pushes(Config, Count), recv_presence(Config). check_other_blocked(Config, Reason, Subscription) -> PeerJID = ?config(peer, Config), ct:comment("Checking if subscriptions and presence-errors are blocked"), send(Config, #presence{type = error, to = PeerJID}), {ErrorFor, PushFor} = case Subscription of <<"both">> -> {[subscribe, subscribed], [unsubscribe, unsubscribed]}; <<"from">> -> {[subscribe, subscribed, unsubscribe], [subscribe, unsubscribe, unsubscribed]}; <<"to">> -> {[unsubscribe], [subscribed, unsubscribe, unsubscribed]}; <<"none">> -> {[subscribe, subscribed, unsubscribe, unsubscribed], [subscribe, unsubscribe]}; _ -> {[subscribe, subscribed, unsubscribe, unsubscribed], [unsubscribe, unsubscribed]} end, lists:foreach( fun(Type) -> send(Config, #presence{type = Type, to = PeerJID}), Count = case lists:member(Type, PushFor) of true -> 1; _ -> 0 end, case lists:member(Type, ErrorFor) of true -> Err = recv_err_and_roster_pushes(Config, Count), #stanza_error{reason = Reason} = xmpp:get_error(Err); _ -> recv_roster_pushes(Config, Count) end end, [subscribe, subscribed, unsubscribe, unsubscribed]). send_presences(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of presences to the peer"), lists:foreach( fun(Type) -> send(Config, #presence{type = Type, to = PeerJID}) end, [available, unavailable]). send_iqs(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of IQs to the peer"), lists:foreach( fun(Type) -> send(Config, #iq{type = Type, to = PeerJID}) end, [set, get, error, result]). send_messages(Config) -> PeerJID = ?config(peer, Config), ct:comment("Sending all types of messages to the peer"), lists:foreach( fun(Type) -> send(Config, #message{type = Type, to = PeerJID}) end, [chat, error, groupchat, headline, normal]). recv_presences(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> #presence{type = Type, from = PeerJID} = recv_presence(Config) end, [available, unavailable]). recv_iqs(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> #iq{type = Type, from = PeerJID} = recv_iq(Config) end, [set, get, error, result]). recv_messages(Config) -> PeerJID = ?config(peer, Config), lists:foreach( fun(Type) -> #message{type = Type, from = PeerJID} = recv_message(Config) end, [chat, error, groupchat, headline, normal]). match_all(Opts) -> IQ = proplists:get_bool(iq, Opts), Message = proplists:get_bool(message, Opts), PresenceIn = proplists:get_bool(presence_in, Opts), PresenceOut = proplists:get_bool(presence_out, Opts), not (IQ or Message or PresenceIn or PresenceOut). is_message_in_blocked(Opts) -> proplists:get_bool(message, Opts) or match_all(Opts). is_message_out_blocked(Opts) -> match_all(Opts). is_iq_in_blocked(Opts) -> proplists:get_bool(iq, Opts) or match_all(Opts). is_iq_out_blocked(Opts) -> match_all(Opts). is_presence_in_blocked(Opts) -> proplists:get_bool(presence_in, Opts) or match_all(Opts). is_presence_out_blocked(Opts) -> proplists:get_bool(presence_out, Opts) or match_all(Opts). is_other_blocked(Opts) -> %% 'other' means subscriptions and presence-errors match_all(Opts). server_send_iqs(Config) -> ServerJID = server_jid(Config), MyJID = my_jid(Config), ct:comment("Sending IQs from ~s to ~s", [jid:encode(ServerJID), jid:encode(MyJID)]), lists:foreach( fun(Type) -> ejabberd_router:route( #iq{from = ServerJID, to = MyJID, type = Type}) end, [error, result]), lists:foreach( fun(Type) -> ejabberd_local:route_iq( #iq{from = ServerJID, to = MyJID, type = Type}, fun(#iq{type = result, sub_els = []}) -> ok; (IQ) -> ct:fail({unexpected_iq_result, IQ}) end) end, [set, get]). server_recv_iqs(Config) -> ServerJID = server_jid(Config), ct:comment("Receiving IQs from ~s", [jid:encode(ServerJID)]), lists:foreach( fun(Type) -> #iq{type = Type, from = ServerJID} = recv_iq(Config) end, [error, result]), lists:foreach( fun(Type) -> #iq{type = Type, from = ServerJID, id = I} = recv_iq(Config), send(Config, #iq{to = ServerJID, type = result, id = I}) end, [set, get]). send_stanzas_to_server_resource(Config) -> ServerJID = server_jid(Config), ServerJIDResource = jid:replace_resource(ServerJID, <<"resource">>), %% All stanzas sent should be handled by local_send_to_resource_hook %% and should be bounced with item-not-found error ct:comment("Sending IQs to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> #iq{type = error} = Err = send_recv(Config, #iq{type = Type, to = ServerJIDResource}), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) end, [set, get]), ct:comment("Sending messages to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> #message{type = error} = Err = send_recv(Config, #message{type = Type, to = ServerJIDResource}), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) end, [normal, chat, groupchat, headline]), ct:comment("Sending presences to ~s", [jid:encode(ServerJIDResource)]), lists:foreach( fun(Type) -> #presence{type = error} = Err = send_recv(Config, #presence{type = Type, to = ServerJIDResource}), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err) end, [available, unavailable]). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/mam_tests.erl�������������������������������������������������������������������0000644�0002322�0002322�00000056331�14154362354�017321� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 14 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(mam_tests). %% API -compile(export_all). -import(suite, [get_features/1, disconnect/1, my_jid/1, send_recv/2, wait_for_slave/1, server_jid/1, send/2, get_features/2, wait_for_master/1, recv_message/1, recv_iq/1, muc_room_jid/1, muc_jid/1, is_feature_advertised/3, get_event/1, put_event/2]). -include("suite.hrl"). -define(VERSIONS, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2]). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {mam_single, [sequence], [single_test(feature_enabled), single_test(get_set_prefs), single_test(get_form), single_test(fake_by)]}. feature_enabled(Config) -> BareMyJID = jid:remove_resource(my_jid(Config)), RequiredFeatures = sets:from_list(?VERSIONS), ServerFeatures = sets:from_list(get_features(Config)), UserFeatures = sets:from_list(get_features(Config, BareMyJID)), MUCFeatures = get_features(Config, muc_jid(Config)), ct:comment("Checking if all MAM server features are enabled"), true = sets:is_subset(RequiredFeatures, ServerFeatures), ct:comment("Checking if all MAM user features are enabled"), true = sets:is_subset(RequiredFeatures, UserFeatures), ct:comment("Checking if all MAM conference service features are enabled"), true = lists:member(?NS_MAM_1, MUCFeatures), true = lists:member(?NS_MAM_2, MUCFeatures), clean(disconnect(Config)). fake_by(Config) -> BareServerJID = server_jid(Config), FullServerJID = jid:replace_resource(BareServerJID, p1_rand:get_string()), FullMyJID = my_jid(Config), BareMyJID = jid:remove_resource(FullMyJID), Fakes = lists:flatmap( fun(JID) -> [#mam_archived{id = p1_rand:get_string(), by = JID}, #stanza_id{id = p1_rand:get_string(), by = JID}] end, [BareServerJID, FullServerJID, BareMyJID, FullMyJID]), Body = xmpp:mk_text(<<"body">>), ForeignJID = jid:make(p1_rand:get_string()), Archived = #mam_archived{id = p1_rand:get_string(), by = ForeignJID}, StanzaID = #stanza_id{id = p1_rand:get_string(), by = ForeignJID}, #message{body = Body, sub_els = SubEls} = send_recv(Config, #message{to = FullMyJID, body = Body, sub_els = [Archived, StanzaID|Fakes]}), ct:comment("Checking if only foreign tags present"), [ForeignJID, ForeignJID] = lists:flatmap( fun(#mam_archived{by = By}) -> [By]; (#stanza_id{by = By}) -> [By]; (_) -> [] end, SubEls), clean(disconnect(Config)). get_set_prefs(Config) -> Range = [{JID, #mam_prefs{xmlns = NS, default = Default, always = Always, never = Never}} || JID <- [undefined, server_jid(Config)], NS <- ?VERSIONS, Default <- [always, never, roster], Always <- [[], [jid:decode(<<"foo@bar.baz">>)]], Never <- [[], [jid:decode(<<"baz@bar.foo">>)]]], lists:foreach( fun({To, Prefs}) -> NS = Prefs#mam_prefs.xmlns, #iq{type = result, sub_els = [Prefs]} = send_recv(Config, #iq{type = set, to = To, sub_els = [Prefs]}), #iq{type = result, sub_els = [Prefs]} = send_recv(Config, #iq{type = get, to = To, sub_els = [#mam_prefs{xmlns = NS}]}) end, Range), clean(disconnect(Config)). get_form(Config) -> ServerJID = server_jid(Config), Range = [{JID, NS} || JID <- [undefined, ServerJID], NS <- ?VERSIONS -- [?NS_MAM_TMP]], lists:foreach( fun({To, NS}) -> #iq{type = result, sub_els = [#mam_query{xmlns = NS, xdata = #xdata{} = X}]} = send_recv(Config, #iq{type = get, to = To, sub_els = [#mam_query{xmlns = NS}]}), [NS] = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X), true = xmpp_util:has_xdata_var(<<"with">>, X), true = xmpp_util:has_xdata_var(<<"start">>, X), true = xmpp_util:has_xdata_var(<<"end">>, X) end, Range), clean(disconnect(Config)). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {mam_master_slave, [sequence], [master_slave_test(archived_and_stanza_id), master_slave_test(query_all), master_slave_test(query_with), master_slave_test(query_rsm_max), master_slave_test(query_rsm_after), master_slave_test(query_rsm_before), master_slave_test(muc), master_slave_test(mucsub), master_slave_test(mucsub_from_muc), master_slave_test(mucsub_from_muc_non_persistent)]}. archived_and_stanza_id_master(Config) -> #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), clean(disconnect(Config)). archived_and_stanza_id_slave(Config) -> ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), clean(disconnect(Config)). query_all_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), query_all(Config, MyJID, Peer), clean(disconnect(Config)). query_all_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), query_all(Config, Peer, MyJID), clean(disconnect(Config)). query_with_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), query_with(Config, MyJID, Peer), clean(disconnect(Config)). query_with_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), query_with(Config, Peer, MyJID), clean(disconnect(Config)). query_rsm_max_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), query_rsm_max(Config, MyJID, Peer), clean(disconnect(Config)). query_rsm_max_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), query_rsm_max(Config, Peer, MyJID), clean(disconnect(Config)). query_rsm_after_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), query_rsm_after(Config, MyJID, Peer), clean(disconnect(Config)). query_rsm_after_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), query_rsm_after(Config, Peer, MyJID), clean(disconnect(Config)). query_rsm_before_master(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_slave(Config), send_messages(Config, lists:seq(1, 5)), query_rsm_before(Config, MyJID, Peer), clean(disconnect(Config)). query_rsm_before_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = set_default(Config, always), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), recv_messages(Config, lists:seq(1, 5)), query_rsm_before(Config, Peer, MyJID), clean(disconnect(Config)). muc_master(Config) -> Room = muc_room_jid(Config), %% Joining ok = muc_tests:join_new(Config), %% MAM feature should not be advertised at this point, %% because MAM is not enabled so far false = is_feature_advertised(Config, ?NS_MAM_1, Room), false = is_feature_advertised(Config, ?NS_MAM_2, Room), %% Fill in some history send_messages_to_room(Config, lists:seq(1, 21)), %% We now should be able to retrieve those via MAM, even though %% MAM is disabled. However, only last 20 messages should be received. recv_messages_from_room(Config, lists:seq(2, 21)), %% Now enable MAM for the conference %% Retrieve config first CfgOpts = muc_tests:get_config(Config), %% Find the MAM field in the config true = proplists:is_defined(mam, CfgOpts), %% Enable MAM [104] = muc_tests:set_config(Config, [{mam, true}]), %% Check if MAM has been enabled true = is_feature_advertised(Config, ?NS_MAM_1, Room), true = is_feature_advertised(Config, ?NS_MAM_2, Room), %% We now sending some messages again send_messages_to_room(Config, lists:seq(1, 5)), %% And retrieve them via MAM again. recv_messages_from_room(Config, lists:seq(1, 5)), put_event(Config, disconnect), muc_tests:leave(Config), clean(disconnect(Config)). muc_slave(Config) -> disconnect = get_event(Config), clean(disconnect(Config)). mucsub_master(Config) -> Room = muc_room_jid(Config), Peer = ?config(peer, Config), wait_for_slave(Config), ct:comment("Joining muc room"), ok = muc_tests:join_new(Config), ct:comment("Enabling mam in room"), CfgOpts = muc_tests:get_config(Config), %% Find the MAM field in the config ?match(true, proplists:is_defined(mam, CfgOpts)), ?match(true, proplists:is_defined(allow_subscription, CfgOpts)), %% Enable MAM [104] = muc_tests:set_config(Config, [{mam, true}, {allow_subscription, true}]), ct:comment("Subscribing peer to room"), ?send_recv(#iq{to = Room, type = set, sub_els = [ #muc_subscribe{jid = Peer, nick = <<"peer">>, events = [?NS_MUCSUB_NODES_MESSAGES]} ]}, #iq{type = result}), ct:comment("Sending messages to room"), send_messages_to_room(Config, lists:seq(1, 5)), ct:comment("Retrieving messages from room mam storage"), recv_messages_from_room(Config, lists:seq(1, 5)), ct:comment("Cleaning up"), put_event(Config, ready), ready = get_event(Config), muc_tests:leave(Config), clean(disconnect(Config)). mucsub_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), MyJIDBare = jid:remove_resource(MyJID), ok = set_default(Config, always), send_recv(Config, #presence{}), wait_for_master(Config), ct:comment("Receiving mucsub events"), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), Msg = ?match(#message{from = Room, type = normal} = Msg, recv_message(Config), Msg), PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ #ps_item{} = PS ]}}, xmpp:get_subtag(Msg, #ps_event{}), PS), ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) end, lists:seq(1, 5)), ct:comment("Retrieving personal mam archive"), QID = p1_rand:get_string(), I = send(Config, #iq{type = set, sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), Forw = ?match(#message{ to = MyJID, from = MyJIDBare, sub_els = [#mam_result{ xmlns = ?NS_MAM_2, queryid = QID, sub_els = [#forwarded{ delay = #delay{}} = Forw]}]}, recv_message(Config), Forw), IMsg = ?match(#message{ to = MyJIDBare, from = Room} = IMsg, xmpp:get_subtag(Forw, #message{}), IMsg), PS = ?match(#ps_event{items = #ps_items{node = ?NS_MUCSUB_NODES_MESSAGES, items = [ #ps_item{} = PS ]}}, xmpp:get_subtag(IMsg, #ps_event{}), PS), ?match(#message{type = groupchat, body = Body}, xmpp:get_subtag(PS, #message{})) end, lists:seq(1, 5)), RSM = ?match(#iq{from = MyJIDBare, id = I, type = result, sub_els = [#mam_fin{xmlns = ?NS_MAM_2, rsm = RSM, complete = true}]}, recv_iq(Config), RSM), match_rsm_count(RSM, 5), % Wait for master exit ready = get_event(Config), % Unsubscribe yourself ?send_recv(#iq{to = Room, type = set, sub_els = [ #muc_unsubscribe{} ]}, #iq{type = result}), put_event(Config, ready), clean(disconnect(Config)). mucsub_from_muc_master(Config) -> mucsub_master(Config). mucsub_from_muc_slave(Config) -> Server = ?config(server, Config), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => true}), Config2 = mucsub_slave(Config), gen_mod:update_module(Server, mod_mam, #{user_mucsub_from_muc_archive => false}), Config2. mucsub_from_muc_non_persistent_master(Config) -> Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), Config2 = mucsub_from_muc_master(Config1), lists:keydelete(persistent_room, 1, Config2). mucsub_from_muc_non_persistent_slave(Config) -> Config1 = lists:keystore(persistent_room, 1, Config, {persistent_room, false}), Config2 = mucsub_from_muc_slave(Config1), lists:keydelete(persistent_room, 1, Config2). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("mam_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("mam_" ++ atom_to_list(T)), [parallel], [list_to_atom("mam_" ++ atom_to_list(T) ++ "_master"), list_to_atom("mam_" ++ atom_to_list(T) ++ "_slave")]}. clean(Config) -> {U, S, _} = jid:tolower(my_jid(Config)), mod_mam:remove_user(U, S), Config. set_default(Config, Default) -> lists:foreach( fun(NS) -> ct:comment("Setting default preferences of '~s' to '~s'", [NS, Default]), #iq{type = result, sub_els = [#mam_prefs{xmlns = NS, default = Default}]} = send_recv(Config, #iq{type = set, sub_els = [#mam_prefs{xmlns = NS, default = Default}]}) end, ?VERSIONS). send_messages(Config, Range) -> Peer = ?config(peer, Config), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), send(Config, #message{to = Peer, body = Body}) end, Range). recv_messages(Config, Range) -> Peer = ?config(peer, Config), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), #message{from = Peer, body = Body} = Msg = recv_message(Config), #mam_archived{by = BareMyJID} = xmpp:get_subtag(Msg, #mam_archived{}), #stanza_id{by = BareMyJID} = xmpp:get_subtag(Msg, #stanza_id{}) end, Range). recv_archived_messages(Config, From, To, QID, Range) -> MyJID = my_jid(Config), lists:foreach( fun(N) -> ct:comment("Retreiving ~pth message in range ~p", [N, Range]), Body = xmpp:mk_text(integer_to_binary(N)), #message{to = MyJID, sub_els = [#mam_result{ queryid = QID, sub_els = [#forwarded{ delay = #delay{}, sub_els = [El]}]}]} = recv_message(Config), #message{from = From, to = To, body = Body} = xmpp:decode(El) end, Range). maybe_recv_iq_result(Config, ?NS_MAM_0, I) -> #iq{type = result, id = I} = recv_iq(Config); maybe_recv_iq_result(_, _, _) -> ok. query_iq_type(?NS_MAM_TMP) -> get; query_iq_type(_) -> set. send_query(Config, #mam_query{xmlns = NS} = Query) -> Type = query_iq_type(NS), I = send(Config, #iq{type = Type, sub_els = [Query]}), maybe_recv_iq_result(Config, NS, I), I. recv_fin(Config, I, QueryID, NS, IsComplete) when NS == ?NS_MAM_1; NS == ?NS_MAM_2 -> ct:comment("Receiving fin iq for namespace '~s'", [NS]), #iq{type = result, id = I, sub_els = [#mam_fin{xmlns = NS, complete = Complete, rsm = RSM}]} = recv_iq(Config), ct:comment("Checking if complete is ~s", [IsComplete]), ?match(IsComplete, Complete), RSM; recv_fin(Config, I, QueryID, ?NS_MAM_TMP = NS, _IsComplete) -> ct:comment("Receiving fin iq for namespace '~s'", [NS]), #iq{type = result, id = I, sub_els = [#mam_query{xmlns = NS, rsm = RSM, id = QueryID}]} = recv_iq(Config), RSM; recv_fin(Config, _, QueryID, ?NS_MAM_0 = NS, IsComplete) -> ct:comment("Receiving fin message for namespace '~s'", [NS]), #message{} = FinMsg = recv_message(Config), #mam_fin{xmlns = NS, id = QueryID, complete = Complete, rsm = RSM} = xmpp:get_subtag(FinMsg, #mam_fin{xmlns = NS}), ct:comment("Checking if complete is ~s", [IsComplete]), ?match(IsComplete, Complete), RSM. send_messages_to_room(Config, Range) -> MyNick = ?config(master_nick, Config), Room = muc_room_jid(Config), MyNickJID = jid:replace_resource(Room, MyNick), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), #message{from = MyNickJID, type = groupchat, body = Body} = send_recv(Config, #message{to = Room, body = Body, type = groupchat}) end, Range). recv_messages_from_room(Config, Range) -> MyNick = ?config(master_nick, Config), Room = muc_room_jid(Config), MyNickJID = jid:replace_resource(Room, MyNick), MyJID = my_jid(Config), QID = p1_rand:get_string(), I = send(Config, #iq{type = set, to = Room, sub_els = [#mam_query{xmlns = ?NS_MAM_2, id = QID}]}), lists:foreach( fun(N) -> Body = xmpp:mk_text(integer_to_binary(N)), #message{ to = MyJID, from = Room, sub_els = [#mam_result{ xmlns = ?NS_MAM_2, queryid = QID, sub_els = [#forwarded{ delay = #delay{}, sub_els = [El]}]}]} = recv_message(Config), #message{from = MyNickJID, type = groupchat, body = Body} = xmpp:decode(El) end, Range), #iq{from = Room, id = I, type = result, sub_els = [#mam_fin{xmlns = ?NS_MAM_2, rsm = RSM, complete = true}]} = recv_iq(Config), match_rsm_count(RSM, length(Range)). query_all(Config, From, To) -> lists:foreach( fun(NS) -> query_all(Config, From, To, NS) end, ?VERSIONS). query_all(Config, From, To, NS) -> QID = p1_rand:get_string(), Range = lists:seq(1, 5), ID = send_query(Config, #mam_query{xmlns = NS, id = QID}), recv_archived_messages(Config, From, To, QID, Range), RSM = recv_fin(Config, ID, QID, NS, _Complete = true), match_rsm_count(RSM, 5). query_with(Config, From, To) -> lists:foreach( fun(NS) -> query_with(Config, From, To, NS) end, ?VERSIONS). query_with(Config, From, To, NS) -> Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), QID = p1_rand:get_string(), Range = lists:seq(1, 5), lists:foreach( fun(JID) -> ct:comment("Sending query with jid ~s", [jid:encode(JID)]), Query = if NS == ?NS_MAM_TMP -> #mam_query{xmlns = NS, with = JID, id = QID}; true -> Fs = mam_query:encode([{with, JID}]), #mam_query{xmlns = NS, id = QID, xdata = #xdata{type = submit, fields = Fs}} end, ID = send_query(Config, Query), recv_archived_messages(Config, From, To, QID, Range), RSM = recv_fin(Config, ID, QID, NS, true), match_rsm_count(RSM, 5) end, [Peer, BarePeer]). query_rsm_max(Config, From, To) -> lists:foreach( fun(NS) -> query_rsm_max(Config, From, To, NS) end, ?VERSIONS). query_rsm_max(Config, From, To, NS) -> lists:foreach( fun(Max) -> QID = p1_rand:get_string(), Range = lists:sublist(lists:seq(1, Max), 5), Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{max = Max}}, ID = send_query(Config, Query), recv_archived_messages(Config, From, To, QID, Range), IsComplete = Max >= 5, RSM = recv_fin(Config, ID, QID, NS, IsComplete), match_rsm_count(RSM, 5) end, lists:seq(0, 6)). query_rsm_after(Config, From, To) -> lists:foreach( fun(NS) -> query_rsm_after(Config, From, To, NS) end, ?VERSIONS). query_rsm_after(Config, From, To, NS) -> lists:foldl( fun(Range, #rsm_first{data = After}) -> ct:comment("Retrieving ~p messages after '~s'", [length(Range), After]), QID = p1_rand:get_string(), Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{'after' = After}}, ID = send_query(Config, Query), recv_archived_messages(Config, From, To, QID, Range), RSM = #rsm_set{first = First} = recv_fin(Config, ID, QID, NS, true), match_rsm_count(RSM, 5), First end, #rsm_first{data = undefined}, [lists:seq(N, 5) || N <- lists:seq(1, 6)]). query_rsm_before(Config, From, To) -> lists:foreach( fun(NS) -> query_rsm_before(Config, From, To, NS) end, ?VERSIONS). query_rsm_before(Config, From, To, NS) -> lists:foldl( fun(Range, Before) -> ct:comment("Retrieving ~p messages before '~s'", [length(Range), Before]), QID = p1_rand:get_string(), Query = #mam_query{xmlns = NS, id = QID, rsm = #rsm_set{before = Before}}, ID = send_query(Config, Query), recv_archived_messages(Config, From, To, QID, Range), RSM = #rsm_set{last = Last} = recv_fin(Config, ID, QID, NS, true), match_rsm_count(RSM, 5), Last end, <<"">>, lists:reverse([lists:seq(1, N) || N <- lists:seq(0, 5)])). match_rsm_count(#rsm_set{count = undefined}, _) -> %% The backend doesn't support counting ok; match_rsm_count(#rsm_set{count = Count1}, Count2) -> ct:comment("Checking if RSM 'count' is ~p", [Count2]), ?match(Count2, Count1). �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/carbons_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000016040�14154362354�020167� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(carbons_tests). %% API -compile(export_all). -import(suite, [is_feature_advertised/2, disconnect/1, send_recv/2, recv_presence/1, send/2, get_event/1, recv_message/1, my_jid/1, wait_for_slave/1, wait_for_master/1, put_event/2]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {carbons_single, [sequence], [single_test(feature_enabled), single_test(unsupported_iq)]}. feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_CARBONS_2), disconnect(Config). unsupported_iq(Config) -> lists:foreach( fun({Type, SubEl}) -> #iq{type = error} = send_recv(Config, #iq{type = Type, sub_els = [SubEl]}) end, [{Type, SubEl} || Type <- [get, set], SubEl <- [#carbons_sent{forwarded = #forwarded{}}, #carbons_received{forwarded = #forwarded{}}, #carbons_private{}]] ++ [{get, SubEl} || SubEl <- [#carbons_enable{}, #carbons_disable{}]]), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {carbons_master_slave, [sequence], [master_slave_test(send_recv), master_slave_test(enable_disable)]}. send_recv_master(Config) -> Peer = ?config(peer, Config), prepare_master(Config), ct:comment("Waiting for the peer to be ready"), ready = get_event(Config), send_messages(Config), ct:comment("Waiting for the peer to disconnect"), #presence{from = Peer, type = unavailable} = recv_presence(Config), disconnect(Config). send_recv_slave(Config) -> prepare_slave(Config), ok = enable(Config), put_event(Config, ready), recv_carbons(Config), disconnect(Config). enable_disable_master(Config) -> prepare_master(Config), ct:comment("Waiting for the peer to be ready"), ready = get_event(Config), send_messages(Config), disconnect(Config). enable_disable_slave(Config) -> Peer = ?config(peer, Config), prepare_slave(Config), ok = enable(Config), ok = disable(Config), put_event(Config, ready), ct:comment("Waiting for the peer to disconnect"), #presence{from = Peer, type = unavailable} = recv_presence(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("carbons_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("carbons_" ++ atom_to_list(T)), [parallel], [list_to_atom("carbons_" ++ atom_to_list(T) ++ "_master"), list_to_atom("carbons_" ++ atom_to_list(T) ++ "_slave")]}. prepare_master(Config) -> MyJID = my_jid(Config), Peer = ?config(peer, Config), #presence{from = MyJID} = send_recv(Config, #presence{priority = 10}), wait_for_slave(Config), ct:comment("Receiving initial presence from the peer"), #presence{from = Peer} = recv_presence(Config), Config. prepare_slave(Config) -> Peer = ?config(peer, Config), MyJID = my_jid(Config), ok = enable(Config), wait_for_master(Config), #presence{from = MyJID} = send_recv(Config, #presence{priority = 5}), ct:comment("Receiving initial presence from the peer"), #presence{from = Peer} = recv_presence(Config), Config. send_messages(Config) -> Server = ?config(server, Config), MyJID = my_jid(Config), JID = jid:make(p1_rand:get_string(), Server), lists:foreach( fun({send, #message{type = Type} = Msg}) -> I = send(Config, Msg#message{to = JID}), if Type /= error -> #message{id = I, type = error} = recv_message(Config); true -> ok end; ({recv, #message{} = Msg}) -> ejabberd_router:route( Msg#message{from = JID, to = MyJID}), ct:comment("Receiving message ~s", [xmpp:pp(Msg)]), #message{} = recv_message(Config) end, message_iterator(Config)). recv_carbons(Config) -> Peer = ?config(peer, Config), BarePeer = jid:remove_resource(Peer), MyJID = my_jid(Config), lists:foreach( fun({_, #message{sub_els = [#hint{type = 'no-copy'}]}}) -> ok; ({_, #message{sub_els = [#carbons_private{}]}}) -> ok; ({_, #message{type = T}}) when T /= normal, T /= chat -> ok; ({Dir, #message{type = T, body = Body} = M}) when (T == chat) or (T == normal andalso Body /= []) -> ct:comment("Receiving carbon ~s", [xmpp:pp(M)]), #message{from = BarePeer, to = MyJID} = CarbonMsg = recv_message(Config), case Dir of send -> #carbons_sent{forwarded = #forwarded{sub_els = [El]}} = xmpp:get_subtag(CarbonMsg, #carbons_sent{}), #message{body = Body} = xmpp:decode(El); recv -> #carbons_received{forwarded = #forwarded{sub_els = [El]}}= xmpp:get_subtag(CarbonMsg, #carbons_received{}), #message{body = Body} = xmpp:decode(El) end; (_) -> false end, message_iterator(Config)). enable(Config) -> case send_recv( Config, #iq{type = set, sub_els = [#carbons_enable{}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. disable(Config) -> case send_recv( Config, #iq{type = set, sub_els = [#carbons_disable{}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. message_iterator(_Config) -> [{Dir, #message{type = Type, body = Body, sub_els = Els}} || Dir <- [send, recv], Type <- [error, chat, normal, groupchat, headline], Body <- [[], xmpp:mk_text(<<"body">>)], Els <- [[], [#hint{type = 'no-copy'}], [#carbons_private{}]]]. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�020311� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/macros.yml��������������������������������������������������0000644�0002322�0002322�00000005737�14154362354�022334� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: CERTFILE: cert.pem CAFILE: ca.pem C2S_PORT: @@c2s_port@@ S2S_PORT: @@s2s_port@@ WEB_PORT: @@web_port@@ STUN_PORT: @@stun_port@@ COMPONENT_PORT: @@component_port@@ PROXY_PORT: @@proxy_port@@ PASSWORD: >- @@password@@ LOGLEVEL: @@loglevel@@ PRIV_DIR: "@@priv_dir@@" PUT_URL: "http://upload.@HOST@:@@web_port@@/upload" GET_URL: "http://upload.@HOST@:@@web_port@@/upload" NEW_SCHEMA: @@new_schema@@ MYSQL_USER: "@@mysql_user@@" MYSQL_SERVER: "@@mysql_server@@" MYSQL_PORT: @@mysql_port@@ MYSQL_PASS: "@@mysql_pass@@" MYSQL_DB: "@@mysql_db@@" MSSQL_USER: "@@mssql_user@@" MSSQL_SERVER: "@@mssql_server@@" MSSQL_PORT: @@mssql_port@@ MSSQL_PASS: "@@mssql_pass@@" MSSQL_DB: "@@mssql_db@@" PGSQL_USER: "@@pgsql_user@@" PGSQL_SERVER: "@@pgsql_server@@" PGSQL_PORT: @@pgsql_port@@ PGSQL_PASS: "@@pgsql_pass@@" PGSQL_DB: "@@pgsql_db@@" VCARD: version: "1.0" fn: Full Name n: family: Family given: Given middle: Middle prefix: Prefix suffix: Suffix nickname: Nickname photo: type: image/png extval: https://domain.tld/photo.png binval: >- iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== bday: 2000-01-01 adr: - home: true work: true postal: true parcel: true dom: true intl: true pref: true pobox: Pobox extadd: Extadd street: Street locality: Locality region: Region pcode: Pcode ctry: Ctry label: - home: true work: true postal: true parcel: true dom: true intl: true pref: true line: - Line1 - Line2 tel: - home: true work: true voice: true fax: true pager: true msg: true cell: true video: true bbs: true modem: true isdn: true pcs: true pref: true number: +7-900-01-02 email: - home: true work: true internet: true pref: true x400: true userid: user@domain.tld jabberid: user@domain.tld mailer: Mailer tz: TZ geo: lat: "12.0" lon: "21.0" title: Title role: Role logo: type: image/png extval: https://domain.tld/logo.png binval: >- iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== categories: - Cat1 - Cat2 note: Note prodid: ProdID rev: Rev sort_string: SortString sound: phonetic: Phonetic extval: https://domain.tld/sound.ogg binval: >- iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAA AACklEQVR4AWNoAAAAggCBTBfX3wAAAABJRU5ErkJggg== uid: UID url: https://domain.tld class: public key: type: Type cred: Cred desc: Desc ���������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.sqlite.yml�����������������������������������������0000644�0002322�0002322�00000002645�14154362354�024101� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: SQLITE_CONFIG: sql_type: sqlite sql_pool_size: 1 auth_method: sql sm_db_type: sql modules: mod_announce: db_type: sql access: local mod_blocking: [] mod_caps: db_type: sql mod_last: db_type: sql mod_muc: db_type: sql ram_db_type: sql vcard: VCARD mod_offline: db_type: sql mod_privacy: db_type: sql mod_private: db_type: sql mod_pubsub: db_type: sql access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: sql mod_mam: db_type: sql mod_vcard: db_type: sql vcard: VCARD mod_vcard_xupdate: [] mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_push: db_type: sql include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] �������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.pgsql.yml������������������������������������������0000644�0002322�0002322�00000003106�14154362354�023717� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: PGSQL_CONFIG: sql_username: PGSQL_USER sql_type: pgsql sql_server: PGSQL_SERVER sql_port: PGSQL_PORT sql_pool_size: 1 sql_password: PGSQL_PASS sql_database: PGSQL_DB auth_method: sql sm_db_type: sql modules: mod_announce: db_type: sql access: local mod_blocking: [] mod_caps: db_type: sql mod_last: db_type: sql mod_muc: db_type: sql ram_db_type: sql vcard: VCARD mod_offline: use_cache: true db_type: sql mod_privacy: db_type: sql mod_private: db_type: sql mod_pubsub: db_type: sql access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: sql mod_mam: db_type: sql mod_vcard: db_type: sql vcard: VCARD mod_vcard_xupdate: [] mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_push: db_type: sql include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.mssql.yml������������������������������������������0000644�0002322�0002322�00000003060�14154362354�023727� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: MSSQL_CONFIG: sql_username: MSSQL_USER sql_type: mssql sql_server: MSSQL_SERVER sql_port: MSSQL_PORT sql_pool_size: 1 sql_password: MSSQL_PASS sql_database: MSSQL_DB auth_method: sql sm_db_type: sql modules: mod_announce: db_type: sql access: local mod_blocking: [] mod_caps: db_type: sql mod_last: db_type: sql mod_muc: db_type: sql ram_db_type: sql vcard: VCARD mod_offline: use_cache: true db_type: sql mod_privacy: db_type: sql mod_private: db_type: sql mod_pubsub: db_type: sql access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: sql mod_mam: db_type: sql mod_vcard: db_type: sql vcard: VCARD mod_vcard_xupdate: [] mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: [] mod_push: db_type: sql include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.mysql.yml������������������������������������������0000644�0002322�0002322�00000003106�14154362354�023736� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: MYSQL_CONFIG: sql_username: MYSQL_USER sql_type: mysql sql_server: MYSQL_SERVER sql_port: MYSQL_PORT sql_pool_size: 1 sql_password: MYSQL_PASS sql_database: MYSQL_DB auth_method: sql sm_db_type: sql modules: mod_announce: db_type: sql access: local mod_blocking: [] mod_caps: db_type: sql mod_last: db_type: sql mod_muc: db_type: sql ram_db_type: sql vcard: VCARD mod_offline: use_cache: true db_type: sql mod_privacy: db_type: sql mod_private: db_type: sql mod_pubsub: db_type: sql access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: sql mod_mam: db_type: sql mod_vcard: db_type: sql vcard: VCARD mod_vcard_xupdate: [] mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_push: db_type: sql include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.ldap.yml�������������������������������������������0000644�0002322�0002322�00000001713�14154362354�023513� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: LDAP_CONFIG: queue_type: ram ldap_servers: - "localhost" ldap_rootdn: "cn=admin,dc=localhost" ldap_port: 1389 ldap_password: "password" ldap_base: "ou=users,dc=localhost" auth_method: ldap modules: mod_vcard: db_type: ldap mod_roster: [] # mod_roster is required by mod_shared_roster mod_shared_roster_ldap: ldap_auth_check: off ldap_base: "dc=localhost" ldap_rfilter: "(objectClass=posixGroup)" ldap_gfilter: "(&(objectClass=posixGroup)(cn=%g))" ldap_memberattr: "memberUid" ldap_ufilter: "(uid=%u)" ldap_userdesc: "cn" mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] �����������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.ldif�����������������������������������������������0000644�0002322�0002322�00000002327�14154362354�022713� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dn: dc=localhost dc: localhost objectclass: dcObject dn: cn=admin,dc=localhost cn: admin objectclass: organizationalRole dn: ou=users,dc=localhost ou: users objectClass: organizationalUnit dn: ou=groups,dc=localhost ou: groups objectClass: organizationalUnit dn: uid=test_single,ou=users,dc=localhost uid: test_single!#$%^*()`~+-;_=[]{}|\ mail: test_single!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Single password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: uid=test_master,ou=users,dc=localhost uid: test_master!#$%^*()`~+-;_=[]{}|\ mail: test_master!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Master password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: uid=test_slave,ou=users,dc=localhost uid: test_slave!#$%^*()`~+-;_=[]{}|\ mail: test_slave!#$%^*()`~+-;_=[]{}|\@localhost objectClass: person jpegPhoto:: /9g= cn: Test Slave password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: uid=user2,ou=users,dc=localhost uid: user2 mail: user2@localhost objectClass: person cn: Test User 2 password: password!@#$%^&*()'"`~<>+-/;:_=[]{}|\ dn: cn=group1,ou=groups,dc=localhost objectClass: posixGroup memberUid: test_single!#$%^*()`~+-;_=[]{}|\ memberUid: user2 cn: group1 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.yml������������������������������������������������0000644�0002322�0002322�00000005677�14154362354�022611� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������include_config_file: - macros.yml - ejabberd.extauth.yml - ejabberd.ldap.yml - ejabberd.mnesia.yml - ejabberd.mysql.yml - ejabberd.mssql.yml - ejabberd.pgsql.yml - ejabberd.redis.yml - ejabberd.sqlite.yml host_config: pgsql.localhost: PGSQL_CONFIG sqlite.localhost: SQLITE_CONFIG mysql.localhost: MYSQL_CONFIG mssql.localhost: MSSQL_CONFIG mnesia.localhost: MNESIA_CONFIG redis.localhost: REDIS_CONFIG ldap.localhost: LDAP_CONFIG extauth.localhost: EXTAUTH_CONFIG localhost: auth_method: - internal - anonymous hosts: - localhost - mnesia.localhost - redis.localhost - mysql.localhost - mssql.localhost - pgsql.localhost - extauth.localhost - ldap.localhost - sqlite.localhost shaper_rules: c2s_shaper: none: admin normal: all max_user_offline_messages: infinity: all max_user_sessions: 10: all s2s_shaper: fast: all access_rules: announce: allow: admin c2s: deny: blocked allow: all configure: allow: admin local: allow: local muc: allow: all muc_admin: allow: admin muc_create: allow: local pubsub_createnode: allow: local register: allow: all acl: local: user_regexp: "" admin: user: "admin" language: en listen: - port: C2S_PORT module: ejabberd_c2s max_stanza_size: 65536 zlib: true starttls: true tls_verify: true shaper: c2s_shaper access: c2s - port: S2S_PORT module: ejabberd_s2s_in - port: WEB_PORT module: ejabberd_http request_handlers: "/admin": ejabberd_web_admin "/api": mod_http_api "/upload": mod_http_upload "/captcha": ejabberd_captcha - port: STUN_PORT module: ejabberd_stun transport: udp use_turn: true turn_ipv4_address: "203.0.113.3" - port: COMPONENT_PORT module: ejabberd_service password: PASSWORD loglevel: LOGLEVEL max_fsm_queue: 1000 queue_type: file modules: mod_adhoc: [] mod_announce: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT vcard: VCARD mod_muc: vcard: VCARD mod_muc_admin: [] mod_carboncopy: [] mod_jidprep: [] mod_mam: [] mod_last: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_s2s_dialback: [] mod_legacy_auth: [] mod_stream_mgmt: max_ack_queue: 10 resume_timeout: 3 mod_stun_disco: secret: "cryptic" services: - host: "example.com" type: turns mod_time: [] mod_version: [] mod_http_upload: docroot: PRIV_DIR put_url: PUT_URL get_url: GET_URL max_size: 10000 vcard: VCARD registration_timeout: infinity s2s_use_starttls: false ca_file: CAFILE c2s_cafile: CAFILE outgoing_s2s_port: S2S_PORT shaper: fast: 50000 normal: 10000 certfiles: - CERTFILE new_sql_schema: NEW_SCHEMA api_permissions: "public commands": who: all what: "*" �����������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.extauth.yml����������������������������������������0000644�0002322�0002322�00000000167�14154362354�024257� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: EXTAUTH_CONFIG: queue_type: ram extauth_program: "python extauth.py" auth_method: external ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ca.key������������������������������������������������������0000644�0002322�0002322�00000003217�14154362354�021411� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA5WxWkSLK3iadpy2v57FVc7pK307aWHQqirg+q5PreRB1nLsr oW+TaXfgB5B1/GTFStnSbmczqpkuWyi4hIB9ZzM62kWuOpZPx0+w5hHx73VWxpsr YgaBkoQsn8BF84PfmRDNG76TOacuoLzeqnN1deWDgOGQ9a7ZesOQLuZBPF6oysfK OpAR035fQM6XaaR8Ti6Ko53DkCzw8MiySrAHJOkgxhmX11+hUMjldWCEiRs1VL/g rolajqe3B+wu0UdonZ/QUeVk4KRnDIAIJSKw8XmgcB4oI5cUrnDnOmv2784RgJZs ZxuGF0e5mz5v8BqXqKiFwH/CD1inUpMA89MATQIDAQABAoIBAQCc2O1x+ixhplrg AZ8iMp2uKe2oL5udH4Y6Im5OFSnGMdeGmHviuYo5b8gMw9m1/RrY6oQwEIRFHMaR cgx8IfAaDu8sbLkJutu98qCJGjmiMUFrNIh7UuFgztZHPUdVjZHfbpobXrX+k2qQ X6+HLrpeKNQ3136oSKrMgEjhl2+AGhe/uqFGw+nwCNzY3BnAJOWS8pipgV0IQ1Eo AdJU8SoW/LToo5RTZNodPhyqLl10D1tRJ8WSAndAkvaoMRHJasYQDrmz449+QiTZ SLRf9n/TtcKJQTaqwskV/dOdygeBUKnZQhq663TKgTWcTxF1dA5T3QxXv/7p+8Ow 9GxuxBjBAoGBAPRjb8OCLD8EAtxFXWRWBH5GWF3vGnDIq5FkPaue0uyDaw+TLgJE AKV7Ik0IRRZkUdc/xix22Bg83L0ErOD2qLHgZuUvuXtiv+Dq/D2BIb5M3zQy8giA vxdlE5O9i8aG647P+ACGOpYZ7a/K645HGxqOZpf8ZRmST5VzNY7qVxb9AoGBAPBS 4Bo66VMWf6BLd8RIK3DzOf0TWRRMCAwX9kCNTG22TX79imJHWB5lWQQam4yp4Cya wo08DT3YcffURW9bJTF2q+JZHMqlEr8q9kcjIJu8uQ7X9N4JsUfCcWaBSHHBNgx/ coved2h02NFcJmV3HuF2l/miah6p9rPJmGnvG1eRAoGBAKIEqju7OQot5peRhPDX 9fKhQERGGAldgCDLi/cTPFKAbaHNuVrXKnaKw5q+OM83gupo5UDlKS4oa08Eongi DoSeeJjIovch6IN8Re2ghnZbED7S55KriARChlAUAW6EU/ZB+fCfDIgmeGVq6e9R RK6+aVWphn0Feq1hy8gLo+EhAoGBAI/hvmRV4v2o2a5ZoJH2d3O/W3eGTu3U+3hq HDfXoOuKmukt2N0wQ7SnDt1jJL/ZsOpjmZk/W9osLUeoYg3ibuknWI9CtPcqT4f+ q8Y5ZLt5CP63EtagzO/enVA2lO3uNHLVFvpgrfLvCiSGXEKhR+7KtwBxWcGUFqzb RJIf4qnRAoGAR+c24S4MtVuw6+UVKyLxhjB6iDTvJijdIr/+ofbeM5TQHGsYzZzP HHNdZ5ECz5eDnaNzvAs4CCuy+75cqlUhAgzrLlCj+dJN/fYEJsD6AjWdto3Zorig XBFM8FtXP7VRjFNwCCbdhrFOcmgbAtz3ReS6Ts6drSw7OgyeDajam1U= -----END RSA PRIVATE KEY----- ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/extauth.py��������������������������������������������������0000755�0002322�0002322�00000002343�14154362354�022352� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������import sys import struct def read_from_stdin(bytes): if hasattr(sys.stdin, 'buffer'): return sys.stdin.buffer.read(bytes) else: return sys.stdin.read(bytes) def read(): (pkt_size,) = struct.unpack('>H', read_from_stdin(2)) pkt = sys.stdin.read(pkt_size) cmd = pkt.split(':')[0] if cmd == 'auth': u, s, p = pkt.split(':', 3)[1:] if u == "wrong": write(False) else: write(True) elif cmd == 'isuser': u, s = pkt.split(':', 2)[1:] if u == "wrong": write(False) else: write(True) elif cmd == 'setpass': u, s, p = pkt.split(':', 3)[1:] write(True) elif cmd == 'tryregister': u, s, p = pkt.split(':', 3)[1:] write(True) elif cmd == 'removeuser': u, s = pkt.split(':', 2)[1:] write(True) elif cmd == 'removeuser3': u, s, p = pkt.split(':', 3)[1:] write(True) else: write(False) read() def write(result): if result: sys.stdout.write('\x00\x02\x00\x01') else: sys.stdout.write('\x00\x02\x00\x00') sys.stdout.flush() if __name__ == "__main__": try: read() except struct.error: pass ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ca.pem������������������������������������������������������0000644�0002322�0002322�00000002335�14154362354�021402� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUUynLQejEU8NykU/YNfL1dyC7vxcwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xODA5MjQxMzE4MjRaFw00NjAy MDkxMzE4MjRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDlbFaRIsreJp2nLa/nsVVzukrfTtpYdCqKuD6rk+t5 EHWcuyuhb5Npd+AHkHX8ZMVK2dJuZzOqmS5bKLiEgH1nMzraRa46lk/HT7DmEfHv dVbGmytiBoGShCyfwEXzg9+ZEM0bvpM5py6gvN6qc3V15YOA4ZD1rtl6w5Au5kE8 XqjKx8o6kBHTfl9AzpdppHxOLoqjncOQLPDwyLJKsAck6SDGGZfXX6FQyOV1YISJ GzVUv+CuiVqOp7cH7C7RR2idn9BR5WTgpGcMgAglIrDxeaBwHigjlxSucOc6a/bv zhGAlmxnG4YXR7mbPm/wGpeoqIXAf8IPWKdSkwDz0wBNAgMBAAGjUzBRMB0GA1Ud DgQWBBQGU3AZGF8ahVEnpfHB5ETAW5uIBzAfBgNVHSMEGDAWgBQGU3AZGF8ahVEn pfHB5ETAW5uIBzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAK jIEjOh7k1xaEMBygQob9XGLmyLgmw1GEvWx7wiDpcdHXuAH9mLC4NPNSjOXPNK2V u4dh1KHy1z+dHJbt2apXejxtiwlcMWmPDF2EtKjstUN+KXecG7vjReArs71T9ir/ 7Xfwfg6TKD3H7efYFJaBb7d/lyneNP1Ive/rkRsGqCglkoX4ajcAm7MLkkFD8TCP NqFc7SdA4OsaeYiUmjnyTUDbKgG0bDAXymhsUzd6Pa9kKQx+dH4GPiCoNoypCXD7 RZSlETNGZ0vdxCjpdvT4eYxSIalG4rAU85turqPF/ovdzUzb72Sta0L5Hrf0rLa/ um3+Xel8qI+p3kErAG2v -----END CERTIFICATE----- ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.redis.yml������������������������������������������0000644�0002322�0002322�00000002761�14154362354�023705� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: REDIS_CONFIG: queue_type: ram auth_method: internal sm_db_type: redis modules: mod_announce: db_type: internal access: local mod_blocking: [] mod_caps: db_type: internal mod_last: db_type: internal mod_muc: db_type: internal vcard: VCARD mod_offline: db_type: internal mod_privacy: db_type: internal mod_private: db_type: internal mod_pubsub: access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: internal mod_mam: db_type: internal mod_vcard: db_type: internal vcard: VCARD mod_vcard_xupdate: [] mod_client_state: queue_presence: true queue_chat_states: true queue_pep: true mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_push: include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] ���������������ejabberd-21.12/test/ejabberd_SUITE_data/openssl.cnf�������������������������������������������������0000644�0002322�0002322�00000023060�14154362354�022465� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# # OpenSSL example configuration file. # This is mostly being used for generation of certificate requests. # # This definition stops the following lines choking if HOME isn't # defined. HOME = . RANDFILE = $ENV::HOME/.rnd # Extra OBJECT IDENTIFIER info: #oid_file = $ENV::HOME/.oid oid_section = new_oids # To use this configuration file with the "-extfile" option of the # "openssl x509" utility, name here the section containing the # X.509v3 extensions to use: extensions = v3_req # (Alternatively, use a configuration file that has only # X.509v3 extensions in its main [= default] section.) [ new_oids ] # We can add new OIDs in here for use by 'ca' and 'req'. # Add a simple OID like this: # testoid1=1.2.3.4 # Or use config file substitution like this: # testoid2=${testoid1}.5.6 #################################################################### [ ca ] default_ca = CA_default # The default ca section #################################################################### [ CA_default ] #dir = ./demoCA # Where everything is kept dir = ssl certs = $dir/certs # Where the issued certs are kept crl_dir = $dir/crl # Where the issued crl are kept database = $dir/index.txt # database index file. #unique_subject = no # Set to 'no' to allow creation of # several ctificates with same subject. new_certs_dir = $dir/newcerts # default place for new certs. certificate = $dir/cacert.pem # The CA certificate serial = $dir/serial # The current serial number crlnumber = $dir/crlnumber # the current crl number # must be commented out to leave a V1 CRL crl = $dir/crl.pem # The current CRL private_key = $dir/private/cakey.pem# The private key RANDFILE = $dir/private/.rand # private random number file x509_extensions = usr_cert # The extentions to add to the cert # Comment out the following two lines for the "traditional" # (and highly broken) format. name_opt = ca_default # Subject Name options cert_opt = ca_default # Certificate field options # Extension copying option: use with caution. copy_extensions = copy # Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs # so this is commented out by default to leave a V1 CRL. # crlnumber must also be commented out to leave a V1 CRL. # crl_extensions = crl_ext default_days = 365 # how long to certify for default_crl_days= 30 # how long before next CRL default_md = sha256 # which md to use. preserve = no # keep passed DN ordering # A few difference way of specifying how similar the request should look # For type CA, the listed attributes must be the same, and the optional # and supplied fields are just that :-) policy = policy_match # For the CA policy [ policy_match ] countryName = match stateOrProvinceName = match organizationName = match organizationalUnitName = optional commonName = optional emailAddress = optional # For the 'anything' policy # At this point in time, you must list all acceptable 'object' # types. [ policy_anything ] countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = optional emailAddress = optional #################################################################### [ req ] default_bits = 1024 default_keyfile = privkey.pem distinguished_name = req_distinguished_name attributes = req_attributes x509_extensions = v3_ca # The extentions to add to the self signed cert # Passwords for private keys if not present they will be prompted for # input_password = secret # output_password = secret # This sets a mask for permitted string types. There are several options. # default: PrintableString, T61String, BMPString. # pkix : PrintableString, BMPString. # utf8only: only UTF8Strings. # nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). # MASK:XXXX a literal mask value. # WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings # so use this option with caution! string_mask = nombstr req_extensions = v3_req # The extensions to add to a certificate request [ req_distinguished_name ] countryName = Country Name (2 letter code) countryName_default = AU countryName_min = 2 countryName_max = 2 stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = Some-State localityName = Locality Name (eg, city) 0.organizationName = Organization Name (eg, company) 0.organizationName_default = Internet Widgits Pty Ltd # we can do this but it is not needed normally :-) #1.organizationName = Second Organization Name (eg, company) #1.organizationName_default = World Wide Web Pty Ltd organizationalUnitName = Organizational Unit Name (eg, section) #organizationalUnitName_default = commonName = Common Name (eg, YOUR name) commonName_max = 64 emailAddress = Email Address emailAddress_max = 64 # SET-ex3 = SET extension number 3 [ req_attributes ] challengePassword = A challenge password challengePassword_min = 4 challengePassword_max = 20 unstructuredName = An optional company name [ usr_cert ] # These extensions are added when 'ca' signs a request. # This goes against PKIX guidelines but some CAs do it and some software # requires this to avoid interpreting an end user certificate as a CA. basicConstraints=CA:FALSE # Here are some examples of the usage of nsCertType. If it is omitted # the certificate can be used for anything *except* object signing. # This is OK for an SSL server. # nsCertType = server # For an object signing certificate this would be used. # nsCertType = objsign # For normal client use this is typical # nsCertType = client, email # and for everything including object signing: # nsCertType = client, email, objsign # This is typical in keyUsage for a client certificate. # keyUsage = nonRepudiation, digitalSignature, keyEncipherment # This will be displayed in Netscape's comment listbox. nsComment = "OpenSSL Generated Certificate" # PKIX recommendations harmless if included in all certificates. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer # This stuff is for subjectAltName and issuerAltname. # Import the email address. # subjectAltName=email:copy # An alternative to produce certificates that aren't # deprecated according to PKIX. # subjectAltName=email:move # Copy subject details # issuerAltName=issuer:copy #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem #nsBaseUrl #nsRevocationUrl #nsRenewalUrl #nsCaPolicyUrl #nsSslServerName crlDistributionPoints = URI:http://localhost:5280/data/crl.der authorityInfoAccess = OCSP;URI:http://localhost:5280/ocsp [ v3_req ] # Extensions to add to a certificate request basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = OCSPSigning,serverAuth,clientAuth subjectAltName = @alt_names [alt_names] DNS.1 = *.localhost otherName.1 = 1.3.6.1.5.5.7.8.5;UTF8:"test_single!#$%^*()`~+-;_=[]{}|\\@localhost" [ v3_ca ] crlDistributionPoints = URI:http://localhost:5280/data/crl.der # Extensions for a typical CA # PKIX recommendation. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer:always # This is what PKIX recommends but some broken software chokes on critical # extensions. #basicConstraints = critical,CA:true # So we do this instead. basicConstraints = CA:true # Key usage: this is typical for a CA certificate. However since it will # prevent it being used as an test self-signed certificate it is best # left out by default. # keyUsage = cRLSign, keyCertSign # Some might want this also # nsCertType = sslCA, emailCA # Include email address in subject alt name: another PKIX recommendation # subjectAltName=email:copy # Copy issuer details # issuerAltName=issuer:copy # DER hex encoding of an extension: beware experts only! # obj=DER:02:03 # Where 'obj' is a standard or added object # You can even override a supported extension: # basicConstraints= critical, DER:30:03:01:01:FF [ crl_ext ] # CRL extensions. # Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. # issuerAltName=issuer:copy authorityKeyIdentifier=keyid:always,issuer:always [ proxy_cert_ext ] # These extensions should be added when creating a proxy certificate # This goes against PKIX guidelines but some CAs do it and some software # requires this to avoid interpreting an end user certificate as a CA. basicConstraints=CA:FALSE # Here are some examples of the usage of nsCertType. If it is omitted # the certificate can be used for anything *except* object signing. # This is OK for an SSL server. # nsCertType = server # For an object signing certificate this would be used. # nsCertType = objsign # For normal client use this is typical # nsCertType = client, email # and for everything including object signing: # nsCertType = client, email, objsign # This is typical in keyUsage for a client certificate. # keyUsage = nonRepudiation, digitalSignature, keyEncipherment # This will be displayed in Netscape's comment listbox. nsComment = "OpenSSL Generated Certificate" # PKIX recommendations harmless if included in all certificates. subjectKeyIdentifier=hash authorityKeyIdentifier=keyid,issuer:always # This stuff is for subjectAltName and issuerAltname. # Import the email address. # subjectAltName=email:copy # An alternative to produce certificates that aren't # deprecated according to PKIX. # subjectAltName=email:move # Copy subject details # issuerAltName=issuer:copy #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem #nsBaseUrl #nsRevocationUrl #nsRenewalUrl #nsCaPolicyUrl #nsSslServerName # This really needs to be in place for it to be a proxy certificate. proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.cfg������������������������������������������������0000644�0002322�0002322�00000016761�14154362354�022543� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{loglevel, 4}. {hosts, ["localhost", "mnesia.localhost", "mysql.localhost", "mssql.localhost", "pgsql.localhost", "sqlite.localhost", "extauth.localhost", "ldap.localhost"]}. {define_macro, 'CERTFILE', "cert.pem"}. {listen, [ {5222, ejabberd_c2s, [ {access, c2s}, {shaper, c2s_shaper}, starttls, zlib, {certfile, 'CERTFILE'}, {max_stanza_size, 65536} ]}, {5269, ejabberd_s2s_in, [ {shaper, s2s_shaper}, {max_stanza_size, 131072} ]}, {5280, ejabberd_http, [ captcha ]} ]}. {shaper, normal, {maxrate, 1000}}. {shaper, fast, {maxrate, 50000}}. {max_fsm_queue, 1000}. {acl, local, {user_regexp, ""}}. {access, max_user_sessions, [{10, all}]}. {access, max_user_offline_messages, [{5000, admin}, {100, all}]}. {access, local, [{allow, local}]}. {access, c2s, [{deny, blocked}, {allow, all}]}. {access, c2s_shaper, [{none, admin}, {normal, all}]}. {access, s2s_shaper, [{fast, all}]}. {access, announce, [{allow, admin}]}. {access, configure, [{allow, admin}]}. {access, muc_admin, [{allow, admin}]}. {access, muc_create, [{allow, local}]}. {access, muc, [{allow, all}]}. {access, pubsub_createnode, [{allow, local}]}. {access, register, [{allow, all}]}. {registration_timeout, infinity}. {language, "en"}. {modules, [ {mod_adhoc, []}, {mod_configure, []}, {mod_disco, []}, {mod_ping, []}, {mod_proxy65, []}, {mod_register, [ {welcome_message, {"Welcome!", "Hi.\nWelcome to this XMPP server."}} ]}, {mod_stats, []}, {mod_time, []}, {mod_version, []} ]}. {host_config, "localhost", [{auth_method, internal}]}. {host_config, "extauth.localhost", [{auth_method, external}, {extauth_program, "python extauth.py"}]}. {host_config, "mnesia.localhost", [{auth_method, internal}, {{add, modules}, [{mod_announce, [{db_type, internal}]}, {mod_blocking, [{db_type, internal}]}, {mod_caps, [{db_type, internal}]}, {mod_last, [{db_type, internal}]}, {mod_muc, [{db_type, internal}]}, {mod_offline, [{db_type, internal}]}, {mod_privacy, [{db_type, internal}]}, {mod_private, [{db_type, internal}]}, {mod_pubsub, [{access_createnode, pubsub_createnode}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {plugins, ["flat", "hometree", "pep"]}]}, {mod_roster, [{db_type, internal}]}, {mod_vcard, [{db_type, internal}]}]} ]}. {host_config, "mysql.localhost", [{auth_method, odbc}, {odbc_pool_size, 1}, {odbc_server, {mysql, "localhost", "ejabberd_test", "ejabberd_test", "ejabberd_test"}}, {{add, modules}, [{mod_announce, [{db_type, odbc}]}, {mod_blocking, [{db_type, odbc}]}, {mod_caps, [{db_type, odbc}]}, {mod_last, [{db_type, odbc}]}, {mod_muc, [{db_type, odbc}]}, {mod_offline, [{db_type, odbc}]}, {mod_privacy, [{db_type, odbc}]}, {mod_private, [{db_type, odbc}]}, {mod_pubsub, [{db_type, odbc}, {access_createnode, pubsub_createnode}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {plugins, ["flat", "hometree", "pep"]}]}, {mod_roster, [{db_type, odbc}]}, {mod_vcard, [{db_type, odbc}]}]} ]}. {host_config, "mssql.localhost", [{auth_method, odbc}, {odbc_pool_size, 1}, {odbc_server, {mssql, "localhost", "ejabberd_test", "ejabberd_test", "ejabberd_Test1"}}, {{add, modules}, [{mod_announce, [{db_type, odbc}]}, {mod_blocking, [{db_type, odbc}]}, {mod_caps, [{db_type, odbc}]}, {mod_last, [{db_type, odbc}]}, {mod_muc, [{db_type, odbc}]}, {mod_offline, [{db_type, odbc}]}, {mod_privacy, [{db_type, odbc}]}, {mod_private, [{db_type, odbc}]}, {mod_pubsub, [{db_type, odbc}, {access_createnode, pubsub_createnode}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {plugins, ["flat", "hometree", "pep"]}]}, {mod_roster, [{db_type, odbc}]}, {mod_vcard, [{db_type, odbc}]}]} ]}. {host_config, "pgsql.localhost", [{auth_method, odbc}, {odbc_pool_size, 1}, {odbc_server, {pgsql, "localhost", "ejabberd_test", "ejabberd_test", "ejabberd_test"}}, {{add, modules}, [{mod_announce, [{db_type, odbc}]}, {mod_blocking, [{db_type, odbc}]}, {mod_caps, [{db_type, odbc}]}, {mod_last, [{db_type, odbc}]}, {mod_muc, [{db_type, odbc}]}, {mod_offline, [{db_type, odbc}]}, {mod_privacy, [{db_type, odbc}]}, {mod_private, [{db_type, odbc}]}, {mod_pubsub, [{db_type, odbc}, {access_createnode, pubsub_createnode}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {plugins, ["flat", "hometree", "pep"]}]}, {mod_roster, [{db_type, odbc}]}, {mod_vcard, [{db_type, odbc}]}]} ]}. {host_config, "sqlite.localhost", [{auth_method, odbc}, {odbc_pool_size, 1}, {odbc_server, {sqlite, "/tmp/ejabberd_test.db"}}, {{add, modules}, [{mod_announce, [{db_type, odbc}]}, {mod_blocking, [{db_type, odbc}]}, {mod_caps, [{db_type, odbc}]}, {mod_last, [{db_type, odbc}]}, {mod_muc, [{db_type, odbc}]}, {mod_offline, [{db_type, odbc}]}, {mod_privacy, [{db_type, odbc}]}, {mod_private, [{db_type, odbc}]}, {mod_pubsub, [{db_type, odbc}, {access_createnode, pubsub_createnode}, {ignore_pep_from_offline, true}, {last_item_cache, false}, {plugins, ["flat", "hometree", "pep"]}]}, {mod_roster, [{db_type, odbc}]}, {mod_vcard, [{db_type, odbc}]}]} ]}. {host_config, "ldap.localhost", [{auth_method, ldap}, {ldap_servers, ["localhost"]}, {ldap_port, 1389}, {ldap_rootdn, "cn=admin,dc=localhost"}, {ldap_password, "password"}, {ldap_base, "ou=users,dc=localhost"}, {{add, modules}, [{mod_vcard_ldap, []}]} ]}. %%% Local Variables: %%% mode: erlang %%% End: %%% vim: set filetype=erlang tabstop=8 foldmarker=%%%',%%%. foldmethod=marker: ���������������ejabberd-21.12/test/ejabberd_SUITE_data/gencerts.sh�������������������������������������������������0000755�0002322�0002322�00000002025�14154362354�022461� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # Update openssl.cnf if needed (in particular section [alt_names]) rm -rf ssl mkdir -p ssl/newcerts touch ssl/index.txt echo 01 > ssl/serial echo 1000 > ssl/crlnumber openssl genrsa -out ca.key 2048 openssl req -new -days 10000 -x509 -key ca.key -out ca.pem -batch openssl genrsa -out ssl/client.key openssl req -new -key ssl/client.key -out ssl/client.csr -config openssl.cnf -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost openssl ca -keyfile ca.key -cert ca.pem -in ssl/client.csr -out ssl/client.crt -config openssl.cnf -days 10000 -batch -notext -policy policy_anything openssl req -new -key ssl/client.key -out ssl/self-signed-client.csr -batch -subj /C=AU/ST=Some-State/O=Internet\ Widgits\ Pty\ Ltd/CN=localhost openssl x509 -req -in ssl/self-signed-client.csr -signkey ssl/client.key -out ssl/self-signed-client.crt -days 10000 cat ssl/client.crt > cert.pem cat ssl/self-signed-client.crt > self-signed-cert.pem cat ssl/client.key >> cert.pem cat ssl/client.key >> self-signed-cert.pem rm -rf ssl �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/cert.pem����������������������������������������������������0000644�0002322�0002322�00000006366�14154362354�021764� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-----BEGIN CERTIFICATE----- MIIEjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTET MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ dHkgTHRkMB4XDTE4MDkyNDEzMTgyNFoXDTQ2MDIwOTEzMTgyNFowWTELMAkGA1UE BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdp ZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B AQEFAAOCAQ8AMIIBCgKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnK Fx0p+eJoNegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopE a/2sDmwxvUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbu lPFePw+fWM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJ tdlqwme2AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6Ct AvqzKtNNJzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABo4IBcjCCAW4w CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy dGlmaWNhdGUwHQYDVR0OBBYEFFvDi47v5xJKOsgQo8MP4JzY6cC/MB8GA1UdIwQY MBaAFAZTcBkYXxqFUSel8cHkRMBbm4gHMDMGA1UdHwQsMCowKKAmoCSGImh0dHA6 Ly9sb2NhbGhvc3Q6NTI4MC9kYXRhL2NybC5kZXIwNgYIKwYBBQUHAQEEKjAoMCYG CCsGAQUFBzABhhpodHRwOi8vbG9jYWxob3N0OjUyODAvb2NzcDALBgNVHQ8EBAMC BeAwJwYDVR0lBCAwHgYIKwYBBQUHAwkGCCsGAQUFBwMBBggrBgEFBQcDAjBQBgNV HREESTBHggsqLmxvY2FsaG9zdKA4BggrBgEFBQcIBaAsDCp0ZXN0X3NpbmdsZSEj JCVeKigpYH4rLTtfPVtde318XEBsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEB AEW8qvdyBMOSjCwJ1G178xsxf8Adw/9QN2ftBGKCo1C3YtmP5CvipChq5FTrOvRz XjoQxbKhlqEumkZQkfmLiM/DLbkFeNqGWpuy14lkyIPUknaLKNCJX++pXsJrPLGR btWnlB0cb+pLIB/UkG8OIpW07pNOZxHdHoHInRMMs89kgsmhIpn5OamzPWK/bqTB YjAPIdmdkYk9oxWfgjpJ4BG2PbGS6CnjA29j7vebuQ4ebVpFBMI9w77PY3NcuMK7 ML6MV6ez/+nPpz+E4zRxsVxmVAbSaiFDW3G3efAybDeT5QW1x/oJm2SpsJNIGHcp RecYNo9esOTG+Bg6wypg4WA= -----END CERTIFICATE----- -----BEGIN RSA PRIVATE KEY----- MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f 9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA 6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA 50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL -----END RSA PRIVATE KEY----- ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/self-signed-cert.pem����������������������������������������0000644�0002322�0002322�00000005453�14154362354�024156� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-----BEGIN CERTIFICATE----- MIIDOTCCAiECFHMoNo36Xx0BWkzS8nwvCPGnHnHRMA0GCSqGSIb3DQEBCwUAMFkx CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xODA5 MjQxMzE4MjRaFw00NjAyMDkxMzE4MjRaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQI DApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBANaEDDeDGf8BH+EjO3IcB2fspkOSp7eOVWdI5oKZyhcdKfniaDXoL78GP/ND Vk5nIGxp6q7iYjoeQBFDQ7Qg+Rv+9KM9lh4GQWZLi7KKRGv9rA5sMb1G79X/5g/I h3A2llLygMuE1BxXhw0C9vByaJvRO24GGnXroXm8GXLG7pTxXj8Pn1jO4y1sZDGA pX7Hc7Aa4Hq22VT5wLo++3Bl2UkOqfeozj4if5ozlQsFibXZasJntgAuAMCmHVs3 N2LMPJREv7mzGvpT9RIfWiPHnaRyJQuZ2DS1U1muF8OgrQL6syrTTSc8MqW0d33A 12lr7ztxmN8Dh1Qv8MgrC/El3O0CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhM+Q qt4IlM1SMb74L5GO2JKGVSUbZmaFJEZcjlrcHkw+Tfc5SMxaj7JpTPg7OGNY1L/3 HnUDdaDRZ5xVOxUF7gTBWDAgkO7En5YfvvEYXUYUk7wwpFrqUqQpluqQIxr+Zf6l pZFLhKIANa4wayKtZ9v4uBtRjnm9Hj7gQHeWN9sueIq7d4HO5lubYlzu1+6qeP+L M0ciNhsUPypCwVcLPB+1Eo925QBwAhXsvPD9yKFQg1M7XxcJSy0w3DwWQsTTsEbk 8c/vIF/IhkOJHQDTKa+VSJM+hZgmx/PsyVdbWRSCAusiZpjHKhzzTCNEloGp/Vbm 5y/OeAK2TGPTg9I91w== -----END CERTIFICATE----- -----BEGIN RSA PRIVATE KEY----- MIIEpgIBAAKCAQEA1oQMN4MZ/wEf4SM7chwHZ+ymQ5Knt45VZ0jmgpnKFx0p+eJo NegvvwY/80NWTmcgbGnqruJiOh5AEUNDtCD5G/70oz2WHgZBZkuLsopEa/2sDmwx vUbv1f/mD8iHcDaWUvKAy4TUHFeHDQL28HJom9E7bgYadeuhebwZcsbulPFePw+f WM7jLWxkMYClfsdzsBrgerbZVPnAuj77cGXZSQ6p96jOPiJ/mjOVCwWJtdlqwme2 AC4AwKYdWzc3Ysw8lES/ubMa+lP1Eh9aI8edpHIlC5nYNLVTWa4Xw6CtAvqzKtNN JzwypbR3fcDXaWvvO3GY3wOHVC/wyCsL8SXc7QIDAQABAoIBAQDUwGX1cHsJ5C2f 9ndwtsfJlHVZs0vPysR9CVpE0Q4TWoNVJ+0++abRB/vI4lHotHL90xZEmJXfGj1k YZf2QHWQBI7Qj7Yg1Qdr0yUbz/IIQLCyJTA3jvEzBvc/VByveBQi9Aw0zOopqc1x ZC1RT8bcMumEN11q8mVV/O4oXZAl+mQIbRRt6JIsRtoW8hpB1e2ipHItDMNpSnzA 6PqcddDyDDePgi5lMOaeV9un60A6pI/+uvmw16R1Io+DyYRnxds3HJ/ccI0Co1P1 khA75QLdnoniYO+oQrq/wGvm+Uq1seh6iuj+SOWvCdB03vPmGYxPKMSW9AtX8xbJ J9lboi3pAoGBAPBaiUYn9F+Zt9oJTHhAimZgs1ub5xVEFwVhYJtFBT3E1rQWRKuf kiU1JRq7TB3MGaC4zGi2ql12KV3AqFhwLKG6sKtlo/IJhJfe3DgWmBVYBBifkgYs mxmA6opgyjbjDEMn6RA+Jov5H267AsnaB4cCB1Jjra6GIdIoMvPghHZXAoGBAOR6 7VC6E+YX5VJPCZiN0h0aBT+Hl4drYQKvZHp5N8RIBkvmcQHEJgsrUKdirFZEXW6y WvepwI4C/Xl61y64/DZ7rum/gpAEPdzSkefKysHAiqkMRcIpjiRxTPJ547ZJycjP E+jzcYfLwQvCW9ZiYl+KdYRbpqBFQC8aWqixFxRbAoGBAJQTsy79vpiHY7V4tRwA 50NboCR4UE3RvT0bWSFPzILZmk0oyvXRQYCa1Vk6uxJAhCl4sLZyk1MxURrpbs3N jjG1itKNtAuRwZavPo1vnhLIPv3MkXIsWQHFYroOF4bpKszU8cmIAMeLm8nkfTtO kASlQ02HC6HSEVQgYAPP9svRAoGBANiOnwKl7Bhpy8TQ/zJmMaG9uP23IeuL3l4y KdVfsXjMH5OvLqtS5BAwFPkiMGBv2fMC/+/AKK8xrFiJEw3I7d0iK+6Hw1OHga8c soh1kOpF+ecyp6fZxU1LSniFCU0M8UHw7Fke7RueBzKDHJK9m6oczTgPuoYsPSKo IwfDGjIDAoGBAMJVkInntV8oDPT1WYpOAZ3Z0myCDZVBbjxx8kE4RSJIsFeNSiTO nhLWCqoG11PVTUzhpYItCjp4At/dG8OQY7WWm0DJJQB38fEqA6JKWpgeWwUdkk8j anCrNUBEuzt3UPSZ17DGCw2+J+mwsg1nevaFIXy0gN2zPtTBWtacznPL -----END RSA PRIVATE KEY----- ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/ejabberd_SUITE_data/ejabberd.mnesia.yml�����������������������������������������0000644�0002322�0002322�00000002734�14154362354�024053� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������define_macro: MNESIA_CONFIG: queue_type: ram auth_method: internal modules: mod_announce: db_type: internal access: local mod_blocking: [] mod_caps: db_type: internal mod_last: db_type: internal mod_muc: db_type: internal vcard: VCARD mod_offline: db_type: internal mod_privacy: db_type: internal mod_private: db_type: internal mod_pubsub: access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: - "flat" - "pep" vcard: VCARD mod_roster: versioning: true store_current_id: true db_type: internal mod_mam: db_type: internal mod_vcard: db_type: internal vcard: VCARD mod_vcard_xupdate: [] mod_client_state: queue_presence: true queue_chat_states: true queue_pep: true mod_adhoc: [] mod_configure: [] mod_disco: [] mod_ping: [] mod_proxy65: port: PROXY_PORT mod_push: include_body: false mod_push_keepalive: [] mod_s2s_dialback: [] mod_stream_mgmt: resume_timeout: 3 mod_legacy_auth: [] mod_register: welcome_message: subject: "Welcome!" body: "Hi. Welcome to this XMPP server." mod_stats: [] mod_time: [] mod_version: [] ������������������������������������ejabberd-21.12/test/private_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000007746�14154362354�020227� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 23 Nov 2018 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(private_tests). %% API -compile(export_all). -import(suite, [my_jid/1, server_jid/1, is_feature_advertised/3, send_recv/2, disconnect/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {private_single, [sequence], [single_test(test_features), single_test(test_no_namespace), single_test(test_set_get), single_test(test_published)]}. test_features(Config) -> Server = jid:encode(server_jid(Config)), MyJID = my_jid(Config), case gen_mod:is_loaded(Server, mod_pubsub) of true -> true = is_feature_advertised(Config, ?NS_BOOKMARKS_CONVERSION_0, jid:remove_resource(MyJID)); false -> ok end, disconnect(Config). test_no_namespace(Config) -> WrongEl = #xmlel{name = <<"wrong">>}, #iq{type = error} = send_recv(Config, #iq{type = get, sub_els = [#private{sub_els = [WrongEl]}]}), disconnect(Config). test_set_get(Config) -> Storage = bookmark_storage(), StorageXMLOut = xmpp:encode(Storage), #iq{type = result, sub_els = []} = send_recv( Config, #iq{type = set, sub_els = [#private{sub_els = [StorageXMLOut]}]}), #iq{type = result, sub_els = [#private{sub_els = [StorageXMLIn]}]} = send_recv( Config, #iq{type = get, sub_els = [#private{sub_els = [xmpp:encode( #bookmark_storage{})]}]}), Storage = xmpp:decode(StorageXMLIn), disconnect(Config). test_published(Config) -> Server = jid:encode(server_jid(Config)), case gen_mod:is_loaded(Server, mod_pubsub) of true -> Storage = bookmark_storage(), Node = xmpp:get_ns(Storage), #iq{type = result, sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} = send_recv( Config, #iq{type = get, sub_els = [#pubsub{items = #ps_items{node = Node}}]}), [#ps_item{sub_els = [StorageXMLIn]}] = Items, Storage = xmpp:decode(StorageXMLIn), #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}); false -> ok end, disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("private_" ++ atom_to_list(T)). conference_bookmark() -> #bookmark_conference{ name = <<"Some name">>, autojoin = true, jid = jid:make(<<"some">>, <<"some.conference.org">>)}. bookmark_storage() -> #bookmark_storage{conference = [conference_bookmark()]}. ��������������������������ejabberd-21.12/test/pubsub_tests.erl����������������������������������������������������������������0000644�0002322�0002322�00000066430�14154362354�020050� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(pubsub_tests). %% API -compile(export_all). -import(suite, [pubsub_jid/1, send_recv/2, get_features/2, disconnect/1, put_event/2, get_event/1, wait_for_master/1, wait_for_slave/1, recv_message/1, my_jid/1, send/2, recv_presence/1, recv/1]). -include("suite.hrl"). -include_lib("stdlib/include/assert.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {pubsub_single, [sequence], [single_test(test_features), single_test(test_vcard), single_test(test_create), single_test(test_configure), single_test(test_delete), single_test(test_get_affiliations), single_test(test_get_subscriptions), single_test(test_create_instant), single_test(test_default), single_test(test_create_configure), single_test(test_publish), single_test(test_auto_create), single_test(test_get_items), single_test(test_delete_item), single_test(test_purge), single_test(test_subscribe), single_test(test_subscribe_max_item_1), single_test(test_unsubscribe)]}. test_features(Config) -> PJID = pubsub_jid(Config), AllFeatures = sets:from_list(get_features(Config, PJID)), NeededFeatures = sets:from_list( [?NS_PUBSUB, ?PUBSUB("access-open"), ?PUBSUB("access-authorize"), ?PUBSUB("create-nodes"), ?PUBSUB("instant-nodes"), ?PUBSUB("config-node"), ?PUBSUB("retrieve-default"), ?PUBSUB("create-and-configure"), ?PUBSUB("publish"), ?PUBSUB("auto-create"), ?PUBSUB("retrieve-items"), ?PUBSUB("delete-items"), ?PUBSUB("subscribe"), ?PUBSUB("retrieve-affiliations"), ?PUBSUB("modify-affiliations"), ?PUBSUB("retrieve-subscriptions"), ?PUBSUB("manage-subscriptions"), ?PUBSUB("purge-nodes"), ?PUBSUB("delete-nodes")]), true = sets:is_subset(NeededFeatures, AllFeatures), disconnect(Config). test_vcard(Config) -> JID = pubsub_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), VCard = mod_pubsub_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). test_create(Config) -> Node = ?config(pubsub_node, Config), Node = create_node(Config, Node), disconnect(Config). test_create_instant(Config) -> Node = create_node(Config, <<>>), delete_node(Config, Node), disconnect(Config). test_configure(Config) -> Node = ?config(pubsub_node, Config), NodeTitle = ?config(pubsub_node_title, Config), NodeConfig = get_node_config(Config, Node), MyNodeConfig = set_opts(NodeConfig, [{title, NodeTitle}]), set_node_config(Config, Node, MyNodeConfig), NewNodeConfig = get_node_config(Config, Node), NodeTitle = proplists:get_value(title, NewNodeConfig, <<>>), disconnect(Config). test_default(Config) -> get_default_node_config(Config), disconnect(Config). test_create_configure(Config) -> NodeTitle = ?config(pubsub_node_title, Config), DefaultNodeConfig = get_default_node_config(Config), CustomNodeConfig = set_opts(DefaultNodeConfig, [{title, NodeTitle}]), Node = create_node(Config, <<>>, CustomNodeConfig), NodeConfig = get_node_config(Config, Node), NodeTitle = proplists:get_value(title, NodeConfig, <<>>), delete_node(Config, Node), disconnect(Config). test_publish(Config) -> Node = create_node(Config, <<>>), publish_item(Config, Node), delete_node(Config, Node), disconnect(Config). test_auto_create(Config) -> Node = p1_rand:get_string(), publish_item(Config, Node), delete_node(Config, Node), disconnect(Config). test_get_items(Config) -> Node = create_node(Config, <<>>), ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], ItemsOut = get_items(Config, Node), true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], delete_node(Config, Node), disconnect(Config). test_delete_item(Config) -> Node = create_node(Config, <<>>), #ps_item{id = I} = publish_item(Config, Node), [#ps_item{id = I}] = get_items(Config, Node), delete_item(Config, Node, I), [] = get_items(Config, Node), delete_node(Config, Node), disconnect(Config). test_subscribe(Config) -> Node = create_node(Config, <<>>), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), [#ps_subscription{node = Node}] = get_subscriptions(Config), delete_node(Config, Node), disconnect(Config). test_subscribe_max_item_1(Config) -> DefaultNodeConfig = get_default_node_config(Config), CustomNodeConfig = set_opts(DefaultNodeConfig, [{max_items, 1}]), Node = create_node(Config, <<>>, CustomNodeConfig), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), [#ps_subscription{node = Node}] = get_subscriptions(Config), delete_node(Config, Node), disconnect(Config). test_unsubscribe(Config) -> Node = create_node(Config, <<>>), subscribe_node(Config, Node), [#ps_subscription{node = Node}] = get_subscriptions(Config), unsubscribe_node(Config, Node), [] = get_subscriptions(Config), delete_node(Config, Node), disconnect(Config). test_get_affiliations(Config) -> Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), Affs = get_affiliations(Config), ?assertEqual(Nodes, lists:sort([Node || #ps_affiliation{node = Node, type = owner} <- Affs])), [delete_node(Config, Node) || Node <- Nodes], disconnect(Config). test_get_subscriptions(Config) -> Nodes = lists:sort([create_node(Config, <<>>) || _ <- lists:seq(1, 5)]), [subscribe_node(Config, Node) || Node <- Nodes], Subs = get_subscriptions(Config), ?assertEqual(Nodes, lists:sort([Node || #ps_subscription{node = Node} <- Subs])), [delete_node(Config, Node) || Node <- Nodes], disconnect(Config). test_purge(Config) -> Node = create_node(Config, <<>>), ItemsIn = [publish_item(Config, Node) || _ <- lists:seq(1, 5)], ItemsOut = get_items(Config, Node), true = [I || #ps_item{id = I} <- lists:sort(ItemsIn)] == [I || #ps_item{id = I} <- lists:sort(ItemsOut)], purge_node(Config, Node), [] = get_items(Config, Node), delete_node(Config, Node), disconnect(Config). test_delete(Config) -> Node = ?config(pubsub_node, Config), delete_node(Config, Node), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {pubsub_master_slave, [sequence], [master_slave_test(publish), master_slave_test(subscriptions), master_slave_test(affiliations), master_slave_test(authorize)]}. publish_master(Config) -> Node = create_node(Config, <<>>), put_event(Config, Node), ready = get_event(Config), #ps_item{id = ID} = publish_item(Config, Node), #ps_item{id = ID} = get_event(Config), delete_node(Config, Node), disconnect(Config). publish_slave(Config) -> Node = get_event(Config), subscribe_node(Config, Node), put_event(Config, ready), #message{ sub_els = [#ps_event{ items = #ps_items{node = Node, items = [Item]}}]} = recv_message(Config), put_event(Config, Item), disconnect(Config). subscriptions_master(Config) -> Peer = ?config(slave, Config), Node = ?config(pubsub_node, Config), Node = create_node(Config, Node), [] = get_subscriptions(Config, Node), wait_for_slave(Config), lists:foreach( fun(Type) -> ok = set_subscriptions(Config, Node, [{Peer, Type}]), #ps_item{} = publish_item(Config, Node), case get_subscriptions(Config, Node) of [] when Type == none; Type == pending -> ok; [#ps_subscription{jid = Peer, type = Type}] -> ok end end, [subscribed, unconfigured, pending, none]), delete_node(Config, Node), disconnect(Config). subscriptions_slave(Config) -> wait_for_master(Config), MyJID = my_jid(Config), Node = ?config(pubsub_node, Config), lists:foreach( fun(subscribed = Type) -> ?recv2(#message{ sub_els = [#ps_event{ subscription = #ps_subscription{ node = Node, jid = MyJID, type = Type}}]}, #message{sub_els = [#ps_event{}]}); (Type) -> #message{ sub_els = [#ps_event{ subscription = #ps_subscription{ node = Node, jid = MyJID, type = Type}}]} = recv_message(Config) end, [subscribed, unconfigured, pending, none]), disconnect(Config). affiliations_master(Config) -> Peer = ?config(slave, Config), BarePeer = jid:remove_resource(Peer), lists:foreach( fun(Aff) -> Node = <<(atom_to_binary(Aff, utf8))/binary, $-, (p1_rand:get_string())/binary>>, create_node(Config, Node, default_node_config(Config)), #ps_item{id = I} = publish_item(Config, Node), ok = set_affiliations(Config, Node, [{Peer, Aff}]), Affs = get_affiliations(Config, Node), case lists:keyfind(BarePeer, #ps_affiliation.jid, Affs) of false when Aff == none -> ok; #ps_affiliation{type = Aff} -> ok end, put_event(Config, {Aff, Node, I}), wait_for_slave(Config), delete_node(Config, Node) end, [outcast, none, member, publish_only, publisher, owner]), put_event(Config, disconnect), disconnect(Config). affiliations_slave(Config) -> affiliations_slave(Config, get_event(Config)). affiliations_slave(Config, {outcast, Node, ItemID}) -> #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), #stanza_error{} = unsubscribe_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_items(Config, Node), #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = set_affiliations(Config, Node, [{?config(master, Config), outcast}, {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {none, Node, ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), %% This violates the affiliation char from section 4.1 [_|_] = get_items(Config, Node), #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = set_affiliations(Config, Node, [{?config(master, Config), outcast}, {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {member, Node, ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), [_|_] = get_items(Config, Node), #stanza_error{reason = 'forbidden'} = publish_item(Config, Node), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = set_affiliations(Config, Node, [{?config(master, Config), outcast}, {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {publish_only, Node, ItemID}) -> #stanza_error{reason = 'forbidden'} = subscribe_node(Config, Node), #stanza_error{} = unsubscribe_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_items(Config, Node), #ps_item{id = _MyItemID} = publish_item(Config, Node), %% BUG: This should be fixed %% ?match(ok, delete_item(Config, Node, MyItemID)), #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = set_affiliations(Config, Node, [{?config(master, Config), outcast}, {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {publisher, Node, _ItemID}) -> #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), [_|_] = get_items(Config, Node), #ps_item{id = MyItemID} = publish_item(Config, Node), ok = delete_item(Config, Node, MyItemID), %% BUG: this should be fixed %% #stanza_error{reason = 'forbidden'} = delete_item(Config, Node, ItemID), #stanza_error{reason = 'forbidden'} = purge_node(Config, Node), #stanza_error{reason = 'forbidden'} = get_node_config(Config, Node), #stanza_error{reason = 'forbidden'} = set_node_config(Config, Node, default_node_config(Config)), #stanza_error{reason = 'forbidden'} = get_subscriptions(Config, Node), #stanza_error{reason = 'forbidden'} = set_subscriptions(Config, Node, [{my_jid(Config), subscribed}]), #stanza_error{reason = 'forbidden'} = get_affiliations(Config, Node), #stanza_error{reason = 'forbidden'} = set_affiliations(Config, Node, [{?config(master, Config), outcast}, {my_jid(Config), owner}]), #stanza_error{reason = 'forbidden'} = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, {owner, Node, ItemID}) -> MyJID = my_jid(Config), Peer = ?config(master, Config), #ps_subscription{type = subscribed} = subscribe_node(Config, Node), ok = unsubscribe_node(Config, Node), [_|_] = get_items(Config, Node), #ps_item{id = MyItemID} = publish_item(Config, Node), ok = delete_item(Config, Node, MyItemID), ok = delete_item(Config, Node, ItemID), ok = purge_node(Config, Node), [_|_] = get_node_config(Config, Node), ok = set_node_config(Config, Node, default_node_config(Config)), ok = set_subscriptions(Config, Node, []), [] = get_subscriptions(Config, Node), ok = set_affiliations(Config, Node, [{Peer, outcast}, {MyJID, owner}]), [_, _] = get_affiliations(Config, Node), ok = delete_node(Config, Node), wait_for_master(Config), affiliations_slave(Config, get_event(Config)); affiliations_slave(Config, disconnect) -> disconnect(Config). authorize_master(Config) -> send(Config, #presence{}), #presence{} = recv_presence(Config), Peer = ?config(slave, Config), PJID = pubsub_jid(Config), NodeConfig = set_opts(default_node_config(Config), [{access_model, authorize}]), Node = ?config(pubsub_node, Config), Node = create_node(Config, Node, NodeConfig), wait_for_slave(Config), #message{sub_els = [#xdata{fields = F1}]} = recv_message(Config), C1 = pubsub_subscribe_authorization:decode(F1), Node = proplists:get_value(node, C1), Peer = proplists:get_value(subscriber_jid, C1), %% Deny it at first Deny = #xdata{type = submit, fields = pubsub_subscribe_authorization:encode( [{node, Node}, {subscriber_jid, Peer}, {allow, false}])}, send(Config, #message{to = PJID, sub_els = [Deny]}), %% We should not have any subscriptions [] = get_subscriptions(Config, Node), wait_for_slave(Config), #message{sub_els = [#xdata{fields = F2}]} = recv_message(Config), C2 = pubsub_subscribe_authorization:decode(F2), Node = proplists:get_value(node, C2), Peer = proplists:get_value(subscriber_jid, C2), %% Now we accept is as the peer is very insisting ;) Approve = #xdata{type = submit, fields = pubsub_subscribe_authorization:encode( [{node, Node}, {subscriber_jid, Peer}, {allow, true}])}, send(Config, #message{to = PJID, sub_els = [Approve]}), wait_for_slave(Config), delete_node(Config, Node), disconnect(Config). authorize_slave(Config) -> Node = ?config(pubsub_node, Config), MyJID = my_jid(Config), wait_for_master(Config), #ps_subscription{type = pending} = subscribe_node(Config, Node), %% We're denied at first #message{ sub_els = [#ps_event{ subscription = #ps_subscription{type = none, jid = MyJID}}]} = recv_message(Config), wait_for_master(Config), #ps_subscription{type = pending} = subscribe_node(Config, Node), %% Now much better! #message{ sub_els = [#ps_event{ subscription = #ps_subscription{type = subscribed, jid = MyJID}}]} = recv_message(Config), wait_for_master(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("pubsub_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("pubsub_" ++ atom_to_list(T)), [parallel], [list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_master"), list_to_atom("pubsub_" ++ atom_to_list(T) ++ "_slave")]}. set_opts(Config, Options) -> lists:foldl( fun({Opt, Val}, Acc) -> lists:keystore(Opt, 1, Acc, {Opt, Val}) end, Config, Options). create_node(Config, Node) -> create_node(Config, Node, undefined). create_node(Config, Node, Options) -> PJID = pubsub_jid(Config), NodeConfig = if is_list(Options) -> #xdata{type = submit, fields = pubsub_node_config:encode(Options)}; true -> undefined end, case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub{create = Node, configure = {<<>>, NodeConfig}}]}) of #iq{type = result, sub_els = [#pubsub{create = NewNode}]} -> NewNode; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. delete_node(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub_owner{delete = {Node, <<>>}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. purge_node(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub_owner{purge = Node}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_default_node_config(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub_owner{default = {<<>>, undefined}}]}) of #iq{type = result, sub_els = [#pubsub_owner{default = {<<>>, NodeConfig}}]} -> pubsub_node_config:decode(NodeConfig#xdata.fields); #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_node_config(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub_owner{configure = {Node, undefined}}]}) of #iq{type = result, sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]} -> pubsub_node_config:decode(NodeConfig#xdata.fields); #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. set_node_config(Config, Node, Options) -> PJID = pubsub_jid(Config), NodeConfig = #xdata{type = submit, fields = pubsub_node_config:encode(Options)}, case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub_owner{configure = {Node, NodeConfig}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. publish_item(Config, Node) -> PJID = pubsub_jid(Config), ItemID = p1_rand:get_string(), Item = #ps_item{id = ItemID, sub_els = [xmpp:encode(#presence{id = ItemID})]}, case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub{publish = #ps_publish{ node = Node, items = [Item]}}]}) of #iq{type = result, sub_els = [#pubsub{publish = #ps_publish{ node = Node, items = [#ps_item{id = ItemID}]}}]} -> Item; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_items(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub{items = #ps_items{node = Node}}]}) of #iq{type = result, sub_els = [#pubsub{items = #ps_items{node = Node, items = Items}}]} -> Items; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. delete_item(Config, Node, I) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub{retract = #ps_retract{ node = Node, items = [#ps_item{id = I}]}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. subscribe_node(Config, Node) -> PJID = pubsub_jid(Config), MyJID = my_jid(Config), case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub{subscribe = #ps_subscribe{ node = Node, jid = MyJID}}]}) of #iq{type = result, sub_els = [#pubsub{ subscription = #ps_subscription{ node = Node, jid = MyJID} = Sub}]} -> Sub; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. unsubscribe_node(Config, Node) -> PJID = pubsub_jid(Config), MyJID = my_jid(Config), case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub{ unsubscribe = #ps_unsubscribe{ node = Node, jid = MyJID}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_affiliations(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub{affiliations = {<<>>, []}}]}) of #iq{type = result, sub_els = [#pubsub{affiliations = {<<>>, Affs}}]} -> Affs; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_affiliations(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub_owner{affiliations = {Node, []}}]}) of #iq{type = result, sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]} -> Affs; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. set_affiliations(Config, Node, JTs) -> PJID = pubsub_jid(Config), Affs = [#ps_affiliation{jid = J, type = T} || {J, T} <- JTs], case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub_owner{affiliations = {Node, Affs}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_subscriptions(Config) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub{subscriptions = {<<>>, []}}]}) of #iq{type = result, sub_els = [#pubsub{subscriptions = {<<>>, Subs}}]} -> Subs; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. get_subscriptions(Config, Node) -> PJID = pubsub_jid(Config), case send_recv(Config, #iq{type = get, to = PJID, sub_els = [#pubsub_owner{subscriptions = {Node, []}}]}) of #iq{type = result, sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]} -> Subs; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. set_subscriptions(Config, Node, JTs) -> PJID = pubsub_jid(Config), Subs = [#ps_subscription{jid = J, type = T} || {J, T} <- JTs], case send_recv(Config, #iq{type = set, to = PJID, sub_els = [#pubsub_owner{subscriptions = {Node, Subs}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = IQ -> xmpp:get_subtag(IQ, #stanza_error{}) end. default_node_config(Config) -> [{title, ?config(pubsub_node_title, Config)}, {notify_delete, false}, {send_last_published_item, never}]. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/README��������������������������������������������������������������������������0000644�0002322�0002322�00000003431�14154362354�015472� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������You need MySQL, MSSQL, PostgreSQL and Redis up and running. MySQL should be accepting TCP connections on localhost:3306. MSSQL should be accepting TCP connections on localhost:1433. PostgreSQL should be accepting TCP connections on localhost:5432. Redis should be accepting TCP connections on localhost:6379. MySQL and PostgreSQL should grant full access to user 'ejabberd_test' with password 'ejabberd_test' on database 'ejabberd_test'. MSSQL should grant full access to user 'ejabberd_test' with password 'ejabberd_Test1' on database 'ejabberd_test'. Here is a quick setup example: ------------------ PostgreSQL ------------------ $ psql template1 template1=# CREATE USER ejabberd_test WITH PASSWORD 'ejabberd_test'; template1=# CREATE DATABASE ejabberd_test; template1=# GRANT ALL PRIVILEGES ON DATABASE ejabberd_test TO ejabberd_test; $ psql ejabberd_test -f sql/pg.sql ------------------- MySQL ------------------- $ mysql mysql> CREATE USER 'ejabberd_test'@'localhost' IDENTIFIED BY 'ejabberd_test'; mysql> CREATE DATABASE ejabberd_test; mysql> GRANT ALL ON ejabberd_test.* TO 'ejabberd_test'@'localhost'; $ mysql ejabberd_test < sql/mysql.sql ------------------- MS SQL Server ------------------- $ sqlcmd -U SA -P ejabberd_Test1 -S localhost 1> CREATE DATABASE ejabberd_test; 2> GO 1> USE ejabberd_test; 2> GO Changed database context to 'ejabberd_test'. 1> CREATE LOGIN ejabberd_test WITH PASSWORD = 'ejabberd_Test1'; 2> GO 1> CREATE USER ejabberd_test FOR LOGIN ejabberd_test; 2> GO 1> GRANT ALL TO ejabberd_test; 2> GO The ALL permission is deprecated and maintained only for compatibility. It DOES NOT imply ALL permissions defined on the entity. 1> GRANT CONTROL ON SCHEMA ::dbo TO ejabberd_test; 2> GO $ sqlcmd -U ejabberd_test -P ejabberd_Test1 -S localhost -d ejabberd_test -i sql/mssql.sql ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/csi_tests.erl�������������������������������������������������������������������0000644�0002322�0002322�00000013044�14154362354�017317� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(csi_tests). %% API -compile(export_all). -import(suite, [disconnect/1, wait_for_slave/1, wait_for_master/1, send/2, send_recv/2, recv_presence/1, recv_message/1, server_jid/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {csi_single, [sequence], [single_test(feature_enabled)]}. feature_enabled(Config) -> true = ?config(csi, Config), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {csi_master_slave, [sequence], [master_slave_test(all)]}. all_master(Config) -> Peer = ?config(peer, Config), Presence = #presence{to = Peer}, ChatState = #message{to = Peer, thread = #message_thread{data = <<"1">>}, sub_els = [#chatstate{type = active}]}, Message = ChatState#message{body = [#text{data = <<"body">>}]}, PepPayload = xmpp:encode(#presence{}), PepOne = #message{ to = Peer, sub_els = [#ps_event{ items = #ps_items{ node = <<"foo-1">>, items = [#ps_item{ id = <<"pep-1">>, sub_els = [PepPayload]}]}}]}, PepTwo = #message{ to = Peer, sub_els = [#ps_event{ items = #ps_items{ node = <<"foo-2">>, items = [#ps_item{ id = <<"pep-2">>, sub_els = [PepPayload]}]}}]}, %% Wait for the slave to become inactive. wait_for_slave(Config), %% Should be queued (but see below): send(Config, Presence), %% Should replace the previous presence in the queue: send(Config, Presence#presence{type = unavailable}), %% The following two PEP stanzas should be queued (but see below): send(Config, PepOne), send(Config, PepTwo), %% The following two PEP stanzas should replace the previous two: send(Config, PepOne), send(Config, PepTwo), %% Should be queued (but see below): send(Config, ChatState), %% Should replace the previous chat state in the queue: send(Config, ChatState#message{sub_els = [#chatstate{type = composing}]}), %% Should be sent immediately, together with the queued stanzas: send(Config, Message), %% Wait for the slave to become active. wait_for_slave(Config), %% Should be delivered, as the client is active again: send(Config, ChatState), disconnect(Config). all_slave(Config) -> Peer = ?config(peer, Config), change_client_state(Config, inactive), wait_for_master(Config), #presence{from = Peer, type = unavailable, sub_els = [#delay{}]} = recv_presence(Config), #message{ from = Peer, sub_els = [#ps_event{ items = #ps_items{ node = <<"foo-1">>, items = [#ps_item{ id = <<"pep-1">>}]}}, #delay{}]} = recv_message(Config), #message{ from = Peer, sub_els = [#ps_event{ items = #ps_items{ node = <<"foo-2">>, items = [#ps_item{ id = <<"pep-2">>}]}}, #delay{}]} = recv_message(Config), #message{from = Peer, thread = #message_thread{data = <<"1">>}, sub_els = [#chatstate{type = composing}, #delay{}]} = recv_message(Config), #message{from = Peer, thread = #message_thread{data = <<"1">>}, body = [#text{data = <<"body">>}], sub_els = [#chatstate{type = active}]} = recv_message(Config), change_client_state(Config, active), wait_for_master(Config), #message{from = Peer, thread = #message_thread{data = <<"1">>}, sub_els = [#chatstate{type = active}]} = recv_message(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("csi_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("csi_" ++ atom_to_list(T)), [parallel], [list_to_atom("csi_" ++ atom_to_list(T) ++ "_master"), list_to_atom("csi_" ++ atom_to_list(T) ++ "_slave")]}. change_client_state(Config, NewState) -> send(Config, #csi{type = NewState}), send_recv(Config, #iq{type = get, to = server_jid(Config), sub_els = [#ping{}]}). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/suite.erl�����������������������������������������������������������������������0000644�0002322�0002322�00000073116�14154362354�016456� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 27 Jun 2013 by Evgeniy Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(suite). %% API -compile(export_all). -include("suite.hrl"). -include_lib("kernel/include/file.hrl"). -include("mod_roster.hrl"). %%%=================================================================== %%% API %%%=================================================================== init_config(Config) -> DataDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), [_, _|Tail] = lists:reverse(filename:split(DataDir)), BaseDir = filename:join(lists:reverse(Tail)), MacrosPathTpl = filename:join([DataDir, "macros.yml"]), ConfigPath = filename:join([DataDir, "ejabberd.yml"]), LogPath = filename:join([PrivDir, "ejabberd.log"]), SASLPath = filename:join([PrivDir, "sasl.log"]), MnesiaDir = filename:join([PrivDir, "mnesia"]), CertFile = filename:join([DataDir, "cert.pem"]), SelfSignedCertFile = filename:join([DataDir, "self-signed-cert.pem"]), CAFile = filename:join([DataDir, "ca.pem"]), {ok, CWD} = file:get_cwd(), {ok, _} = file:copy(CertFile, filename:join([CWD, "cert.pem"])), {ok, _} = file:copy(SelfSignedCertFile, filename:join([CWD, "self-signed-cert.pem"])), {ok, _} = file:copy(CAFile, filename:join([CWD, "ca.pem"])), {ok, MacrosContentTpl} = file:read_file(MacrosPathTpl), Password = <<"password!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>, Backends = get_config_backends(), MacrosContent = process_config_tpl( MacrosContentTpl, [{c2s_port, 5222}, {loglevel, 4}, {new_schema, false}, {s2s_port, 5269}, {stun_port, 3478}, {component_port, 5270}, {web_port, 5280}, {proxy_port, 7777}, {password, Password}, {mysql_server, <<"localhost">>}, {mysql_port, 3306}, {mysql_db, <<"ejabberd_test">>}, {mysql_user, <<"ejabberd_test">>}, {mysql_pass, <<"ejabberd_test">>}, {mssql_server, <<"localhost">>}, {mssql_port, 1433}, {mssql_db, <<"ejabberd_test">>}, {mssql_user, <<"ejabberd_test">>}, {mssql_pass, <<"ejabberd_Test1">>}, {pgsql_server, <<"localhost">>}, {pgsql_port, 5432}, {pgsql_db, <<"ejabberd_test">>}, {pgsql_user, <<"ejabberd_test">>}, {pgsql_pass, <<"ejabberd_test">>}, {priv_dir, PrivDir}]), MacrosPath = filename:join([CWD, "macros.yml"]), ok = file:write_file(MacrosPath, MacrosContent), copy_backend_configs(DataDir, CWD, Backends), setup_ejabberd_lib_path(Config), case application:load(sasl) of ok -> ok; {error, {already_loaded, _}} -> ok end, case application:load(mnesia) of ok -> ok; {error, {already_loaded, _}} -> ok end, case application:load(ejabberd) of ok -> ok; {error, {already_loaded, _}} -> ok end, application:set_env(ejabberd, config, ConfigPath), application:set_env(ejabberd, log_path, LogPath), application:set_env(sasl, sasl_error_logger, {file, SASLPath}), application:set_env(mnesia, dir, MnesiaDir), [{server_port, ct:get_config(c2s_port, 5222)}, {server_host, "localhost"}, {component_port, ct:get_config(component_port, 5270)}, {s2s_port, ct:get_config(s2s_port, 5269)}, {server, ?COMMON_VHOST}, {user, <<"test_single!#$%^*()`~+-;_=[]{}|\\">>}, {nick, <<"nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {master_nick, <<"master_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {slave_nick, <<"slave_nick!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {room_subject, <<"hello, world!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {certfile, CertFile}, {persistent_room, true}, {anonymous, false}, {type, client}, {xmlns, ?NS_CLIENT}, {ns_stream, ?NS_STREAM}, {stream_version, {1, 0}}, {stream_id, <<"">>}, {stream_from, <<"">>}, {db_xmlns, <<"">>}, {mechs, []}, {rosterver, false}, {lang, <<"en">>}, {base_dir, BaseDir}, {receiver, undefined}, {pubsub_node, <<"node!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {pubsub_node_title, <<"title!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {resource, <<"resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {master_resource, <<"master_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {slave_resource, <<"slave_resource!@#$%^&*()'\"`~<>+-/;:_=[]{}|\\">>}, {password, Password}, {backends, Backends} |Config]. copy_backend_configs(DataDir, CWD, Backends) -> Files = filelib:wildcard(filename:join([DataDir, "ejabberd.*.yml"])), lists:foreach( fun(Src) -> io:format("copying ~p", [Src]), File = filename:basename(Src), case string:tokens(File, ".") of ["ejabberd", SBackend, "yml"] -> Backend = list_to_atom(SBackend), Macro = list_to_atom(string:to_upper(SBackend) ++ "_CONFIG"), Dst = filename:join([CWD, File]), case lists:member(Backend, Backends) of true -> {ok, _} = file:copy(Src, Dst); false -> ok = file:write_file( Dst, fast_yaml:encode( [{define_macro, [{Macro, []}]}])) end; _ -> ok end end, Files). find_top_dir(Dir) -> case file:read_file_info(filename:join([Dir, ebin])) of {ok, #file_info{type = directory}} -> Dir; _ -> find_top_dir(filename:dirname(Dir)) end. setup_ejabberd_lib_path(Config) -> case code:lib_dir(ejabberd) of {error, _} -> DataDir = proplists:get_value(data_dir, Config), {ok, CWD} = file:get_cwd(), NewEjPath = filename:join([CWD, "ejabberd-0.0.1"]), TopDir = find_top_dir(DataDir), ok = file:make_symlink(TopDir, NewEjPath), code:replace_path(ejabberd, NewEjPath); _ -> ok end. %% Read environment variable CT_DB=mysql to limit the backends to test. %% You can thus limit the backend you want to test with: %% CT_BACKENDS=mysql rebar ct suites=ejabberd get_config_backends() -> EnvBackends = case os:getenv("CT_BACKENDS") of false -> ?BACKENDS; String -> Backends0 = string:tokens(String, ","), lists:map( fun(Backend) -> list_to_atom(string:strip(Backend, both, $ )) end, Backends0) end, application:load(ejabberd), EnabledBackends = application:get_env(ejabberd, enabled_backends, EnvBackends), misc:intersection(EnvBackends, [mnesia, ldap, extauth|EnabledBackends]). process_config_tpl(Content, []) -> Content; process_config_tpl(Content, [{Name, DefaultValue} | Rest]) -> Val = case ct:get_config(Name, DefaultValue) of V when is_integer(V) -> integer_to_binary(V); V when is_atom(V) -> atom_to_binary(V, latin1); V -> iolist_to_binary(V) end, NewContent = binary:replace(Content, <<"@@",(atom_to_binary(Name,latin1))/binary, "@@">>, Val, [global]), process_config_tpl(NewContent, Rest). stream_header(Config) -> To = case ?config(server, Config) of <<"">> -> undefined; Server -> jid:make(Server) end, From = case ?config(stream_from, Config) of <<"">> -> undefined; Frm -> jid:make(Frm) end, #stream_start{to = To, from = From, lang = ?config(lang, Config), version = ?config(stream_version, Config), xmlns = ?config(xmlns, Config), db_xmlns = ?config(db_xmlns, Config), stream_xmlns = ?config(ns_stream, Config)}. connect(Config) -> NewConfig = init_stream(Config), case ?config(type, NewConfig) of client -> process_stream_features(NewConfig); server -> process_stream_features(NewConfig); component -> NewConfig end. tcp_connect(Config) -> case ?config(receiver, Config) of undefined -> Owner = self(), NS = case ?config(type, Config) of client -> ?NS_CLIENT; server -> ?NS_SERVER; component -> ?NS_COMPONENT end, Server = ?config(server_host, Config), Port = ?config(server_port, Config), ReceiverPid = spawn(fun() -> start_receiver(NS, Owner, Server, Port) end), set_opt(receiver, ReceiverPid, Config); _ -> Config end. init_stream(Config) -> Version = ?config(stream_version, Config), NewConfig = tcp_connect(Config), send(NewConfig, stream_header(NewConfig)), XMLNS = case ?config(type, Config) of client -> ?NS_CLIENT; component -> ?NS_COMPONENT; server -> ?NS_SERVER end, receive #stream_start{id = ID, xmlns = XMLNS, version = Version} -> set_opt(stream_id, ID, NewConfig) end. process_stream_features(Config) -> receive #stream_features{sub_els = Fs} -> Mechs = lists:flatmap( fun(#sasl_mechanisms{list = Ms}) -> Ms; (_) -> [] end, Fs), lists:foldl( fun(#feature_register{}, Acc) -> set_opt(register, true, Acc); (#starttls{}, Acc) -> set_opt(starttls, true, Acc); (#legacy_auth_feature{}, Acc) -> set_opt(legacy_auth, true, Acc); (#compression{methods = Ms}, Acc) -> set_opt(compression, Ms, Acc); (_, Acc) -> Acc end, set_opt(mechs, Mechs, Config), Fs) end. disconnect(Config) -> ct:comment("Disconnecting"), try send_text(Config, ?STREAM_TRAILER) catch exit:normal -> ok end, receive {xmlstreamend, <<"stream:stream">>} -> ok end, flush(Config), ok = recv_call(Config, close), ct:comment("Disconnected"), set_opt(receiver, undefined, Config). close_socket(Config) -> ok = recv_call(Config, close), Config. starttls(Config) -> starttls(Config, false). starttls(Config, ShouldFail) -> send(Config, #starttls{}), receive #starttls_proceed{} when ShouldFail -> ct:fail(starttls_should_have_failed); #starttls_failure{} when ShouldFail -> Config; #starttls_failure{} -> ct:fail(starttls_failed); #starttls_proceed{} -> ok = recv_call(Config, {starttls, ?config(certfile, Config)}), Config end. zlib(Config) -> send(Config, #compress{methods = [<<"zlib">>]}), receive #compressed{} -> ok end, ok = recv_call(Config, compress), process_stream_features(init_stream(Config)). auth(Config) -> auth(Config, false). auth(Config, ShouldFail) -> Type = ?config(type, Config), IsAnonymous = ?config(anonymous, Config), Mechs = ?config(mechs, Config), HaveMD5 = lists:member(<<"DIGEST-MD5">>, Mechs), HavePLAIN = lists:member(<<"PLAIN">>, Mechs), HaveExternal = lists:member(<<"EXTERNAL">>, Mechs), HaveAnonymous = lists:member(<<"ANONYMOUS">>, Mechs), if HaveAnonymous and IsAnonymous -> auth_SASL(<<"ANONYMOUS">>, Config, ShouldFail); HavePLAIN -> auth_SASL(<<"PLAIN">>, Config, ShouldFail); HaveMD5 -> auth_SASL(<<"DIGEST-MD5">>, Config, ShouldFail); HaveExternal -> auth_SASL(<<"EXTERNAL">>, Config, ShouldFail); Type == client -> auth_legacy(Config, false, ShouldFail); Type == component -> auth_component(Config, ShouldFail); true -> ct:fail(no_known_sasl_mechanism_available) end. bind(Config) -> U = ?config(user, Config), S = ?config(server, Config), R = ?config(resource, Config), case ?config(type, Config) of client -> #iq{type = result, sub_els = [#bind{jid = JID}]} = send_recv( Config, #iq{type = set, sub_els = [#bind{resource = R}]}), case ?config(anonymous, Config) of false -> {U, S, R} = jid:tolower(JID), Config; true -> {User, S, Resource} = jid:tolower(JID), set_opt(user, User, set_opt(resource, Resource, Config)) end; component -> Config end. open_session(Config) -> open_session(Config, false). open_session(Config, Force) -> if Force -> #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, sub_els = [#xmpp_session{}]}); true -> ok end, Config. auth_legacy(Config, IsDigest) -> auth_legacy(Config, IsDigest, false). auth_legacy(Config, IsDigest, ShouldFail) -> ServerJID = server_jid(Config), U = ?config(user, Config), R = ?config(resource, Config), P = ?config(password, Config), #iq{type = result, from = ServerJID, sub_els = [#legacy_auth{username = <<"">>, password = <<"">>, resource = <<"">>} = Auth]} = send_recv(Config, #iq{to = ServerJID, type = get, sub_els = [#legacy_auth{}]}), Res = case Auth#legacy_auth.digest of <<"">> when IsDigest -> StreamID = ?config(stream_id, Config), D = p1_sha:sha(<<StreamID/binary, P/binary>>), send_recv(Config, #iq{to = ServerJID, type = set, sub_els = [#legacy_auth{username = U, resource = R, digest = D}]}); _ when not IsDigest -> send_recv(Config, #iq{to = ServerJID, type = set, sub_els = [#legacy_auth{username = U, resource = R, password = P}]}) end, case Res of #iq{from = ServerJID, type = result, sub_els = []} -> if ShouldFail -> ct:fail(legacy_auth_should_have_failed); true -> Config end; #iq{from = ServerJID, type = error} -> if ShouldFail -> Config; true -> ct:fail(legacy_auth_failed) end end. auth_component(Config, ShouldFail) -> StreamID = ?config(stream_id, Config), Password = ?config(password, Config), Digest = p1_sha:sha(<<StreamID/binary, Password/binary>>), send(Config, #handshake{data = Digest}), receive #handshake{} when ShouldFail -> ct:fail(component_auth_should_have_failed); #handshake{} -> Config; #stream_error{reason = 'not-authorized'} when ShouldFail -> Config; #stream_error{reason = 'not-authorized'} -> ct:fail(component_auth_failed) end. auth_SASL(Mech, Config) -> auth_SASL(Mech, Config, false). auth_SASL(Mech, Config, ShouldFail) -> Creds = {?config(user, Config), ?config(server, Config), ?config(password, Config)}, auth_SASL(Mech, Config, ShouldFail, Creds). auth_SASL(Mech, Config, ShouldFail, Creds) -> {Response, SASL} = sasl_new(Mech, Creds), send(Config, #sasl_auth{mechanism = Mech, text = Response}), wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail). wait_auth_SASL_result(Config, ShouldFail) -> receive #sasl_success{} when ShouldFail -> ct:fail(sasl_auth_should_have_failed); #sasl_success{} -> ok = recv_call(Config, reset_stream), send(Config, stream_header(Config)), Type = ?config(type, Config), NS = if Type == client -> ?NS_CLIENT; Type == server -> ?NS_SERVER end, Config2 = receive #stream_start{id = ID, xmlns = NS, version = {1,0}} -> set_opt(stream_id, ID, Config) end, receive #stream_features{sub_els = Fs} -> if Type == client -> #xmpp_session{optional = true} = lists:keyfind(xmpp_session, 1, Fs); true -> ok end, lists:foldl( fun(#feature_sm{}, ConfigAcc) -> set_opt(sm, true, ConfigAcc); (#feature_csi{}, ConfigAcc) -> set_opt(csi, true, ConfigAcc); (#rosterver_feature{}, ConfigAcc) -> set_opt(rosterver, true, ConfigAcc); (#compression{methods = Ms}, ConfigAcc) -> set_opt(compression, Ms, ConfigAcc); (_, ConfigAcc) -> ConfigAcc end, Config2, Fs) end; #sasl_challenge{text = ClientIn} -> {Response, SASL} = (?config(sasl, Config))(ClientIn), send(Config, #sasl_response{text = Response}), wait_auth_SASL_result(set_opt(sasl, SASL, Config), ShouldFail); #sasl_failure{} when ShouldFail -> Config; #sasl_failure{} -> ct:fail(sasl_auth_failed) end. re_register(Config) -> User = ?config(user, Config), Server = ?config(server, Config), Pass = ?config(password, Config), ok = ejabberd_auth:try_register(User, Server, Pass). match_failure(Received, [Match]) when is_list(Match)-> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~s", [Received, Match]); match_failure(Received, Matches) -> ct:fail("Received input:~n~n~p~n~ndon't match expected patterns:~n~n~p", [Received, Matches]). recv(_Config) -> receive {fail, El, Why} -> ct:fail("recv failed: ~p->~n~s", [El, xmpp:format_error(Why)]); Event -> Event end. recv_iq(_Config) -> receive #iq{} = IQ -> IQ end. recv_presence(_Config) -> receive #presence{} = Pres -> Pres end. recv_message(_Config) -> receive #message{} = Msg -> Msg end. decode_stream_element(NS, El) -> decode(El, NS, []). format_element(El) -> case erlang:function_exported(ct, log, 5) of true -> ejabberd_web_admin:pretty_print_xml(El); false -> io_lib:format("~p~n", [El]) end. decode(El, NS, Opts) -> try Pkt = xmpp:decode(El, NS, Opts), ct:pal("RECV:~n~s~n~s", [format_element(El), xmpp:pp(Pkt)]), Pkt catch _:{xmpp_codec, Why} -> ct:pal("recv failed: ~p->~n~s", [El, xmpp:format_error(Why)]), erlang:error({xmpp_codec, Why}) end. send_text(Config, Text) -> recv_call(Config, {send_text, Text}). send(State, Pkt) -> {NewID, NewPkt} = case Pkt of #message{id = I} -> ID = id(I), {ID, Pkt#message{id = ID}}; #presence{id = I} -> ID = id(I), {ID, Pkt#presence{id = ID}}; #iq{id = I} -> ID = id(I), {ID, Pkt#iq{id = ID}}; _ -> {undefined, Pkt} end, El = xmpp:encode(NewPkt), ct:pal("SENT:~n~s~n~s", [format_element(El), xmpp:pp(NewPkt)]), Data = case NewPkt of #stream_start{} -> fxml:element_to_header(El); _ -> fxml:element_to_binary(El) end, ok = send_text(State, Data), NewID. send_recv(State, #message{} = Msg) -> ID = send(State, Msg), receive #message{id = ID} = Result -> Result end; send_recv(State, #presence{} = Pres) -> ID = send(State, Pres), receive #presence{id = ID} = Result -> Result end; send_recv(State, #iq{} = IQ) -> ID = send(State, IQ), receive #iq{id = ID} = Result -> Result end. sasl_new(<<"PLAIN">>, {User, Server, Password}) -> {<<User/binary, $@, Server/binary, 0, User/binary, 0, Password/binary>>, fun (_) -> {error, <<"Invalid SASL challenge">>} end}; sasl_new(<<"EXTERNAL">>, {User, Server, _Password}) -> {jid:encode(jid:make(User, Server)), fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; sasl_new(<<"ANONYMOUS">>, _) -> {<<"">>, fun(_) -> ct:fail(sasl_challenge_is_not_expected) end}; sasl_new(<<"DIGEST-MD5">>, {User, Server, Password}) -> {<<"">>, fun (ServerIn) -> case xmpp_sasl_digest:parse(ServerIn) of bad -> {error, <<"Invalid SASL challenge">>}; KeyVals -> Nonce = fxml:get_attr_s(<<"nonce">>, KeyVals), CNonce = id(), Realm = proplists:get_value(<<"realm">>, KeyVals, Server), DigestURI = <<"xmpp/", Realm/binary>>, NC = <<"00000001">>, QOP = <<"auth">>, AuthzId = <<"">>, MyResponse = response(User, Password, Nonce, AuthzId, Realm, CNonce, DigestURI, NC, QOP, <<"AUTHENTICATE">>), SUser = << <<(case Char of $" -> <<"\\\"">>; $\\ -> <<"\\\\">>; _ -> <<Char>> end)/binary>> || <<Char>> <= User >>, Resp = <<"username=\"", SUser/binary, "\",realm=\"", Realm/binary, "\",nonce=\"", Nonce/binary, "\",cnonce=\"", CNonce/binary, "\",nc=", NC/binary, ",qop=", QOP/binary, ",digest-uri=\"", DigestURI/binary, "\",response=\"", MyResponse/binary, "\"">>, {Resp, fun (ServerIn2) -> case xmpp_sasl_digest:parse(ServerIn2) of bad -> {error, <<"Invalid SASL challenge">>}; _KeyVals2 -> {<<"">>, fun (_) -> {error, <<"Invalid SASL challenge">>} end} end end} end end}. hex(S) -> p1_sha:to_hexlist(S). response(User, Passwd, Nonce, AuthzId, Realm, CNonce, DigestURI, NC, QOP, A2Prefix) -> A1 = case AuthzId of <<"">> -> <<((erlang:md5(<<User/binary, ":", Realm/binary, ":", Passwd/binary>>)))/binary, ":", Nonce/binary, ":", CNonce/binary>>; _ -> <<((erlang:md5(<<User/binary, ":", Realm/binary, ":", Passwd/binary>>)))/binary, ":", Nonce/binary, ":", CNonce/binary, ":", AuthzId/binary>> end, A2 = case QOP of <<"auth">> -> <<A2Prefix/binary, ":", DigestURI/binary>>; _ -> <<A2Prefix/binary, ":", DigestURI/binary, ":00000000000000000000000000000000">> end, T = <<(hex((erlang:md5(A1))))/binary, ":", Nonce/binary, ":", NC/binary, ":", CNonce/binary, ":", QOP/binary, ":", (hex((erlang:md5(A2))))/binary>>, hex((erlang:md5(T))). my_jid(Config) -> jid:make(?config(user, Config), ?config(server, Config), ?config(resource, Config)). server_jid(Config) -> jid:make(<<>>, ?config(server, Config), <<>>). pubsub_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"pubsub.", Server/binary>>, <<>>). proxy_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"proxy.", Server/binary>>, <<>>). upload_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"upload.", Server/binary>>, <<>>). muc_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"conference.", Server/binary>>, <<>>). muc_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"test">>, <<"conference.", Server/binary>>, <<>>). my_muc_jid(Config) -> Nick = ?config(nick, Config), RoomJID = muc_room_jid(Config), jid:replace_resource(RoomJID, Nick). peer_muc_jid(Config) -> PeerNick = ?config(peer_nick, Config), RoomJID = muc_room_jid(Config), jid:replace_resource(RoomJID, PeerNick). alt_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"alt">>, <<"conference.", Server/binary>>, <<>>). mix_jid(Config) -> Server = ?config(server, Config), jid:make(<<>>, <<"mix.", Server/binary>>, <<>>). mix_room_jid(Config) -> Server = ?config(server, Config), jid:make(<<"test">>, <<"mix.", Server/binary>>, <<>>). id() -> id(<<>>). id(<<>>) -> p1_rand:get_string(); id(ID) -> ID. get_features(Config) -> get_features(Config, server_jid(Config)). get_features(Config, To) -> ct:comment("Getting features of ~s", [jid:encode(To)]), #iq{type = result, sub_els = [#disco_info{features = Features}]} = send_recv(Config, #iq{type = get, sub_els = [#disco_info{}], to = To}), Features. is_feature_advertised(Config, Feature) -> is_feature_advertised(Config, Feature, server_jid(Config)). is_feature_advertised(Config, Feature, To) -> Features = get_features(Config, To), lists:member(Feature, Features). set_opt(Opt, Val, Config) -> [{Opt, Val}|lists:keydelete(Opt, 1, Config)]. wait_for_master(Config) -> put_event(Config, peer_ready), case get_event(Config) of peer_ready -> ok; Other -> suite:match_failure(Other, peer_ready) end. wait_for_slave(Config) -> put_event(Config, peer_ready), case get_event(Config) of peer_ready -> ok; Other -> suite:match_failure(Other, peer_ready) end. make_iq_result(#iq{from = From} = IQ) -> IQ#iq{type = result, to = From, from = undefined, sub_els = []}. self_presence(Config, Type) -> MyJID = my_jid(Config), ct:comment("Sending self-presence"), #presence{type = Type, from = MyJID} = send_recv(Config, #presence{type = Type}). set_roster(Config, Subscription, Groups) -> MyJID = my_jid(Config), {U, S, _} = jid:tolower(MyJID), PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), PeerLJID = jid:tolower(PeerBareJID), ct:comment("Adding ~s to roster with subscription '~s' in groups ~p", [jid:encode(PeerBareJID), Subscription, Groups]), {atomic, _} = mod_roster:set_roster(#roster{usj = {U, S, PeerLJID}, us = {U, S}, jid = PeerLJID, subscription = Subscription, groups = Groups}), Config. del_roster(Config) -> del_roster(Config, ?config(peer, Config)). del_roster(Config, PeerJID) -> MyJID = my_jid(Config), {U, S, _} = jid:tolower(MyJID), PeerBareJID = jid:remove_resource(PeerJID), PeerLJID = jid:tolower(PeerBareJID), ct:comment("Removing ~s from roster", [jid:encode(PeerBareJID)]), {atomic, _} = mod_roster:del_roster(U, S, PeerLJID), Config. get_roster(Config) -> {LUser, LServer, _} = jid:tolower(my_jid(Config)), mod_roster:get_roster(LUser, LServer). recv_call(Config, Msg) -> Receiver = ?config(receiver, Config), Ref = make_ref(), Receiver ! {Ref, Msg}, receive {Ref, Reply} -> Reply end. start_receiver(NS, Owner, Server, Port) -> MRef = erlang:monitor(process, Owner), {ok, Socket} = xmpp_socket:connect( Server, Port, [binary, {packet, 0}, {active, false}], infinity), receiver(NS, Owner, Socket, MRef). receiver(NS, Owner, Socket, MRef) -> receive {Ref, reset_stream} -> Socket1 = xmpp_socket:reset_stream(Socket), Owner ! {Ref, ok}, receiver(NS, Owner, Socket1, MRef); {Ref, {starttls, Certfile}} -> {ok, TLSSocket} = xmpp_socket:starttls( Socket, [{certfile, Certfile}, connect]), Owner ! {Ref, ok}, receiver(NS, Owner, TLSSocket, MRef); {Ref, compress} -> {ok, ZlibSocket} = xmpp_socket:compress(Socket), Owner ! {Ref, ok}, receiver(NS, Owner, ZlibSocket, MRef); {Ref, {send_text, Text}} -> Ret = xmpp_socket:send(Socket, Text), Owner ! {Ref, Ret}, receiver(NS, Owner, Socket, MRef); {Ref, close} -> xmpp_socket:close(Socket), Owner ! {Ref, ok}, receiver(NS, Owner, Socket, MRef); {'$gen_event', {xmlstreamelement, El}} -> Owner ! decode_stream_element(NS, El), receiver(NS, Owner, Socket, MRef); {'$gen_event', {xmlstreamstart, Name, Attrs}} -> Owner ! decode(#xmlel{name = Name, attrs = Attrs}, <<>>, []), receiver(NS, Owner, Socket, MRef); {'$gen_event', Event} -> Owner ! Event, receiver(NS, Owner, Socket, MRef); {'DOWN', MRef, process, Owner, _} -> ok; {tcp, _, Data} -> case xmpp_socket:recv(Socket, Data) of {ok, Socket1} -> receiver(NS, Owner, Socket1, MRef); {error, _} -> Owner ! closed, receiver(NS, Owner, Socket, MRef) end; {tcp_error, _, _} -> Owner ! closed, receiver(NS, Owner, Socket, MRef); {tcp_closed, _} -> Owner ! closed, receiver(NS, Owner, Socket, MRef) end. %%%=================================================================== %%% Clients puts and gets events via this relay. %%%=================================================================== start_event_relay() -> spawn(fun event_relay/0). stop_event_relay(Config) -> Pid = ?config(event_relay, Config), exit(Pid, normal). event_relay() -> event_relay([], []). event_relay(Events, Subscribers) -> receive {subscribe, From} -> erlang:monitor(process, From), From ! {ok, self()}, lists:foreach( fun(Event) -> From ! {event, Event, self()} end, Events), event_relay(Events, [From|Subscribers]); {put, Event, From} -> From ! {ok, self()}, lists:foreach( fun(Pid) when Pid /= From -> Pid ! {event, Event, self()}; (_) -> ok end, Subscribers), event_relay([Event|Events], Subscribers); {'DOWN', _MRef, process, Pid, _Info} -> case lists:member(Pid, Subscribers) of true -> NewSubscribers = lists:delete(Pid, Subscribers), lists:foreach( fun(Subscriber) -> Subscriber ! {event, peer_down, self()} end, NewSubscribers), event_relay(Events, NewSubscribers); false -> event_relay(Events, Subscribers) end end. subscribe_to_events(Config) -> Relay = ?config(event_relay, Config), Relay ! {subscribe, self()}, receive {ok, Relay} -> ok end. put_event(Config, Event) -> Relay = ?config(event_relay, Config), Relay ! {put, Event, self()}, receive {ok, Relay} -> ok end. get_event(Config) -> Relay = ?config(event_relay, Config), receive {event, Event, Relay} -> Event end. flush(Config) -> receive {event, peer_down, _} -> flush(Config); closed -> flush(Config); Msg -> ct:fail({unexpected_msg, Msg}) after 0 -> ok end. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/proxy65_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000011556�14154362354�020103� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(proxy65_tests). %% API -compile(export_all). -import(suite, [disconnect/1, is_feature_advertised/3, proxy_jid/1, my_jid/1, wait_for_slave/1, wait_for_master/1, send_recv/2, put_event/2, get_event/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {proxy65_single, [sequence], [single_test(feature_enabled), single_test(service_vcard)]}. feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_BYTESTREAMS, proxy_jid(Config)), disconnect(Config). service_vcard(Config) -> JID = proxy_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(JID)]), VCard = mod_proxy65_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = send_recv(Config, #iq{type = get, to = JID, sub_els = [#vcard_temp{}]}), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {proxy65_master_slave, [sequence], [master_slave_test(all)]}. all_master(Config) -> Proxy = proxy_jid(Config), MyJID = my_jid(Config), Peer = ?config(slave, Config), wait_for_slave(Config), #presence{} = send_recv(Config, #presence{}), #iq{type = result, sub_els = [#bytestreams{hosts = [StreamHost]}]} = send_recv( Config, #iq{type = get, sub_els = [#bytestreams{}], to = Proxy}), SID = p1_rand:get_string(), Data = p1_rand:bytes(1024), put_event(Config, {StreamHost, SID, Data}), Socks5 = socks5_connect(StreamHost, {SID, MyJID, Peer}), wait_for_slave(Config), #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, to = Proxy, sub_els = [#bytestreams{activate = Peer, sid = SID}]}), socks5_send(Socks5, Data), disconnect(Config). all_slave(Config) -> MyJID = my_jid(Config), Peer = ?config(master, Config), #presence{} = send_recv(Config, #presence{}), wait_for_master(Config), {StreamHost, SID, Data} = get_event(Config), Socks5 = socks5_connect(StreamHost, {SID, Peer, MyJID}), wait_for_master(Config), socks5_recv(Socks5, Data), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("proxy65_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("proxy65_" ++ atom_to_list(T)), [parallel], [list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_master"), list_to_atom("proxy65_" ++ atom_to_list(T) ++ "_slave")]}. socks5_connect(#streamhost{host = Host, port = Port}, {SID, JID1, JID2}) -> Hash = p1_sha:sha([SID, jid:encode(JID1), jid:encode(JID2)]), {ok, Sock} = gen_tcp:connect(binary_to_list(Host), Port, [binary, {active, false}]), Init = <<?VERSION_5, 1, ?AUTH_ANONYMOUS>>, InitAck = <<?VERSION_5, ?AUTH_ANONYMOUS>>, Req = <<?VERSION_5, ?CMD_CONNECT, 0, ?ATYP_DOMAINNAME, 40, Hash:40/binary, 0, 0>>, Resp = <<?VERSION_5, ?SUCCESS, 0, ?ATYP_DOMAINNAME, 40, Hash:40/binary, 0, 0>>, gen_tcp:send(Sock, Init), {ok, InitAck} = gen_tcp:recv(Sock, size(InitAck)), gen_tcp:send(Sock, Req), {ok, Resp} = gen_tcp:recv(Sock, size(Resp)), Sock. socks5_send(Sock, Data) -> ok = gen_tcp:send(Sock, Data). socks5_recv(Sock, Data) -> {ok, Data} = gen_tcp:recv(Sock, size(Data)). ��������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/example_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000004507�14154362354�020200� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 16 Nov 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(example_tests). %% API -compile(export_all). -import(suite, []). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {example_single, [sequence], [single_test(foo)]}. foo(Config) -> Config. %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {example_master_slave, [sequence], [master_slave_test(foo)]}. foo_master(Config) -> Config. foo_slave(Config) -> Config. %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("example_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("example_" ++ atom_to_list(T)), [parallel], [list_to_atom("example_" ++ atom_to_list(T) ++ "_master"), list_to_atom("example_" ++ atom_to_list(T) ++ "_slave")]}. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/jidprep_tests.erl���������������������������������������������������������������0000644�0002322�0002322�00000004547�14154362354�020206� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Holger Weiss <holger@zedat.fu-berlin.de> %%% Created : 11 Sep 2019 by Holger Weiss <holger@zedat.fu-berlin.de> %%% %%% %%% ejabberd, Copyright (C) 2019-2021 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(jidprep_tests). %% API -compile(export_all). -import(suite, [send_recv/2, disconnect/1, is_feature_advertised/2, server_jid/1]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single user tests %%%=================================================================== single_cases() -> {jidprep_single, [sequence], [single_test(feature_enabled), single_test(normalize_jid)]}. feature_enabled(Config) -> true = is_feature_advertised(Config, ?NS_JIDPREP_0), disconnect(Config). normalize_jid(Config) -> ServerJID = server_jid(Config), OrigJID = jid:decode(<<"Romeo@Example.COM/Orchard">>), NormJID = jid:decode(<<"romeo@example.com/Orchard">>), Request = #jidprep{jid = OrigJID}, #iq{type = result, sub_els = [#jidprep{jid = NormJID}]} = send_recv(Config, #iq{type = get, to = ServerJID, sub_els = [Request]}), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("jidprep_" ++ atom_to_list(T)). ���������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/test/muc_tests.erl�������������������������������������������������������������������0000644�0002322�0002322�00000221546�14154362354�017335� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%%%------------------------------------------------------------------- %%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 15 Oct 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> %%% %%% %%% ejabberd, Copyright (C) 2002-2021 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(muc_tests). %% API -compile(export_all). -import(suite, [recv_presence/1, send_recv/2, my_jid/1, muc_room_jid/1, send/2, recv_message/1, recv_iq/1, muc_jid/1, alt_room_jid/1, wait_for_slave/1, wait_for_master/1, disconnect/1, put_event/2, get_event/1, peer_muc_jid/1, my_muc_jid/1, get_features/2, set_opt/3]). -include("suite.hrl"). %%%=================================================================== %%% API %%%=================================================================== %%%=================================================================== %%% Single tests %%%=================================================================== single_cases() -> {muc_single, [sequence], [single_test(service_presence_error), single_test(service_message_error), single_test(service_unknown_ns_iq_error), single_test(service_iq_set_error), single_test(service_improper_iq_error), single_test(service_features), single_test(service_disco_info_node_error), single_test(service_disco_items), single_test(service_unique), single_test(service_vcard), single_test(configure_non_existent), single_test(cancel_configure_non_existent), single_test(service_subscriptions), single_test(set_room_affiliation)]}. service_presence_error(Config) -> Service = muc_jid(Config), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), lists:foreach( fun(To) -> send(Config, #presence{type = error, to = To}), lists:foreach( fun(Type) -> #presence{type = error} = Err = send_recv(Config, #presence{type = Type, to = To}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err) end, [available, unavailable]) end, [Service, ServiceResource]), disconnect(Config). service_message_error(Config) -> Service = muc_jid(Config), send(Config, #message{type = error, to = Service}), lists:foreach( fun(Type) -> #message{type = error} = Err1 = send_recv(Config, #message{type = Type, to = Service}), #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err1) end, [chat, normal, headline, groupchat]), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), send(Config, #message{type = error, to = ServiceResource}), lists:foreach( fun(Type) -> #message{type = error} = Err2 = send_recv(Config, #message{type = Type, to = ServiceResource}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err2) end, [chat, normal, headline, groupchat]), disconnect(Config). service_unknown_ns_iq_error(Config) -> Service = muc_jid(Config), ServiceResource = jid:replace_resource(Service, p1_rand:get_string()), lists:foreach( fun(To) -> send(Config, #iq{type = result, to = To}), send(Config, #iq{type = error, to = To}), lists:foreach( fun(Type) -> #iq{type = error} = Err1 = send_recv(Config, #iq{type = Type, to = To, sub_els = [#presence{}]}), #stanza_error{reason = 'service-unavailable'} = xmpp:get_error(Err1) end, [set, get]) end, [Service, ServiceResource]), disconnect(Config). service_iq_set_error(Config) -> Service = muc_jid(Config), lists:foreach( fun(SubEl) -> send(Config, #iq{type = result, to = Service, sub_els = [SubEl]}), #iq{type = error} = Err2 = send_recv(Config, #iq{type = set, to = Service, sub_els = [SubEl]}), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2) end, [#disco_items{}, #disco_info{}, #vcard_temp{}, #muc_unique{}, #muc_subscriptions{}]), disconnect(Config). service_improper_iq_error(Config) -> Service = muc_jid(Config), lists:foreach( fun(SubEl) -> send(Config, #iq{type = result, to = Service, sub_els = [SubEl]}), lists:foreach( fun(Type) -> #iq{type = error} = Err3 = send_recv(Config, #iq{type = Type, to = Service, sub_els = [SubEl]}), #stanza_error{reason = Reason} = xmpp:get_error(Err3), true = Reason /= 'internal-server-error' end, [set, get]) end, [#disco_item{jid = Service}, #identity{category = <<"category">>, type = <<"type">>}, #vcard_email{}, #muc_subscribe{nick = ?config(nick, Config)}]), disconnect(Config). service_features(Config) -> ServerHost = ?config(server_host, Config), MUC = muc_jid(Config), Features = sets:from_list(get_features(Config, MUC)), MAMFeatures = case gen_mod:is_loaded(ServerHost, mod_mam) of true -> [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1]; false -> [] end, RequiredFeatures = sets:from_list( [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_REGISTER, ?NS_MUC, ?NS_VCARD, ?NS_MUCSUB, ?NS_MUC_UNIQUE | MAMFeatures]), ct:comment("Checking if all needed disco features are set"), true = sets:is_subset(RequiredFeatures, Features), disconnect(Config). service_disco_info_node_error(Config) -> MUC = muc_jid(Config), Node = p1_rand:get_string(), #iq{type = error} = Err = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#disco_info{node = Node}]}), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(Err), disconnect(Config). service_disco_items(Config) -> #jid{server = Service} = muc_jid(Config), Rooms = lists:sort( lists:map( fun(I) -> RoomName = integer_to_binary(I), jid:make(RoomName, Service) end, lists:seq(1, 5))), lists:foreach( fun(Room) -> ok = join_new(Config, Room) end, Rooms), Items = disco_items(Config), Rooms = [J || #disco_item{jid = J} <- Items], lists:foreach( fun(Room) -> ok = leave(Config, Room) end, Rooms), [] = disco_items(Config), disconnect(Config). service_vcard(Config) -> MUC = muc_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(MUC)]), VCard = mod_muc_opt:vcard(?config(server, Config)), #iq{type = result, sub_els = [VCard]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#vcard_temp{}]}), disconnect(Config). service_unique(Config) -> MUC = muc_jid(Config), ct:comment("Requesting muc unique from ~s", [jid:encode(MUC)]), #iq{type = result, sub_els = [#muc_unique{name = Name}]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_unique{}]}), ct:comment("Checking if unique name is set in the response"), <<_, _/binary>> = Name, disconnect(Config). configure_non_existent(Config) -> [_|_] = get_config(Config), disconnect(Config). cancel_configure_non_existent(Config) -> Room = muc_room_jid(Config), #iq{type = result, sub_els = []} = send_recv(Config, #iq{to = Room, type = set, sub_els = [#muc_owner{config = #xdata{type = cancel}}]}), disconnect(Config). service_subscriptions(Config) -> MUC = #jid{server = Service} = muc_jid(Config), Rooms = lists:sort( lists:map( fun(I) -> RoomName = integer_to_binary(I), jid:make(RoomName, Service) end, lists:seq(1, 5))), lists:foreach( fun(Room) -> ok = join_new(Config, Room), [104] = set_config(Config, [{allow_subscription, true}], Room), [] = subscribe(Config, [], Room) end, Rooms), #iq{type = result, sub_els = [#muc_subscriptions{list = JIDs}]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#muc_subscriptions{}]}), Rooms = lists:sort([J || #muc_subscription{jid = J, events = []} <- JIDs]), lists:foreach( fun(Room) -> ok = unsubscribe(Config, Room), ok = leave(Config, Room) end, Rooms), disconnect(Config). set_room_affiliation(Config) -> #jid{server = RoomService} = muc_jid(Config), RoomName = <<"set_room_affiliation">>, RoomJID = jid:make(RoomName, RoomService), MyJID = my_jid(Config), PeerJID = jid:remove_resource(?config(slave, Config)), ct:pal("joining room ~p", [RoomJID]), ok = join_new(Config, RoomJID), ct:pal("setting affiliation in room ~p to 'member' for ~p", [RoomJID, PeerJID]), ServerHost = ?config(server_host, Config), WebPort = ct:get_config(web_port, 5280), RequestURL = "http://" ++ ServerHost ++ ":" ++ integer_to_list(WebPort) ++ "/api/set_room_affiliation", Headers = [{"X-Admin", "true"}], ContentType = "application/json", Body = jiffy:encode(#{name => RoomName, service => RoomService, jid => jid:encode(PeerJID), affiliation => member}), {ok, {{_, 200, _}, _, _}} = httpc:request(post, {RequestURL, Headers, ContentType, Body}, [], []), #message{id = _, from = RoomJID, to = MyJID, sub_els = [ #muc_user{items = [ #muc_item{affiliation = member, role = none, jid = PeerJID}]}]} = recv_message(Config), ok = leave(Config, RoomJID), disconnect(Config). %%%=================================================================== %%% Master-slave tests %%%=================================================================== master_slave_cases() -> {muc_master_slave, [sequence], [master_slave_test(register), master_slave_test(groupchat_msg), master_slave_test(private_msg), master_slave_test(set_subject), master_slave_test(history), master_slave_test(invite), master_slave_test(invite_members_only), master_slave_test(invite_password_protected), master_slave_test(voice_request), master_slave_test(change_role), master_slave_test(kick), master_slave_test(change_affiliation), master_slave_test(destroy), master_slave_test(vcard), master_slave_test(nick_change), master_slave_test(config_title_desc), master_slave_test(config_public_list), master_slave_test(config_password), master_slave_test(config_whois), master_slave_test(config_members_only), master_slave_test(config_moderated), master_slave_test(config_private_messages), master_slave_test(config_query), master_slave_test(config_allow_invites), master_slave_test(config_visitor_status), master_slave_test(config_allow_voice_requests), master_slave_test(config_voice_request_interval), master_slave_test(config_visitor_nickchange), master_slave_test(join_conflict)]}. join_conflict_master(Config) -> ok = join_new(Config), put_event(Config, join), ct:comment("Waiting for 'leave' command from the slave"), leave = get_event(Config), ok = leave(Config), disconnect(Config). join_conflict_slave(Config) -> NewConfig = set_opt(nick, ?config(peer_nick, Config), Config), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), ct:comment("Fail trying to join the room with conflicting nick"), #stanza_error{reason = 'conflict'} = join(NewConfig), put_event(Config, leave), disconnect(NewConfig). groupchat_msg_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ok = master_join(Config), lists:foreach( fun(I) -> Body = xmpp:mk_text(integer_to_binary(I)), send(Config, #message{type = groupchat, to = Room, body = Body}), #message{type = groupchat, from = MyNickJID, body = Body} = recv_message(Config) end, lists:seq(1, 5)), #muc_user{items = [#muc_item{jid = PeerJID, role = none, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). groupchat_msg_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), {[], _, _} = slave_join(Config), lists:foreach( fun(I) -> Body = xmpp:mk_text(integer_to_binary(I)), #message{type = groupchat, from = PeerNickJID, body = Body} = recv_message(Config) end, lists:seq(1, 5)), ok = leave(Config), disconnect(Config). private_msg_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = master_join(Config), lists:foreach( fun(I) -> Body = xmpp:mk_text(integer_to_binary(I)), send(Config, #message{type = chat, to = PeerNickJID, body = Body}) end, lists:seq(1, 5)), #muc_user{items = [#muc_item{jid = PeerJID, role = none, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Fail trying to send a private message to non-existing occupant"), send(Config, #message{type = chat, to = PeerNickJID}), #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), #stanza_error{reason = 'item-not-found'} = xmpp:get_error(ErrMsg), ok = leave(Config), disconnect(Config). private_msg_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), {[], _, _} = slave_join(Config), lists:foreach( fun(I) -> Body = xmpp:mk_text(integer_to_binary(I)), #message{type = chat, from = PeerNickJID, body = Body} = recv_message(Config) end, lists:seq(1, 5)), ok = leave(Config), disconnect(Config). set_subject_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), Subject1 = xmpp:mk_text(?config(room_subject, Config)), Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), ok = master_join(Config), ct:comment("Setting 1st subject"), send(Config, #message{type = groupchat, to = Room, subject = Subject1}), #message{type = groupchat, from = MyNickJID, subject = Subject1} = recv_message(Config), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Setting 2nd subject"), send(Config, #message{type = groupchat, to = Room, subject = Subject2}), #message{type = groupchat, from = MyNickJID, subject = Subject2} = recv_message(Config), ct:comment("Asking the slave to join"), put_event(Config, join), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving 1st subject set by the slave"), #message{type = groupchat, from = PeerNickJID, subject = Subject1} = recv_message(Config), ct:comment("Disallow subject change"), [104] = set_config(Config, [{changesubject, false}]), ct:comment("Waiting for the slave to leave"), #muc_user{items = [#muc_item{jid = PeerJID, role = none, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). set_subject_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), PeerNick = ?config(master_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), Subject1 = xmpp:mk_text(?config(room_subject, Config)), Subject2 = xmpp:mk_text(<<"new-", (?config(room_subject, Config))/binary>>), {[], _, _} = slave_join(Config), ct:comment("Receiving 1st subject set by the master"), #message{type = groupchat, from = PeerNickJID, subject = Subject1} = recv_message(Config), ok = leave(Config), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {[], SubjMsg2, _} = join(Config), ct:comment("Checking if the master has set 2nd subject during our absence"), #message{type = groupchat, from = PeerNickJID, subject = Subject2} = SubjMsg2, ct:comment("Setting 1st subject"), send(Config, #message{to = Room, type = groupchat, subject = Subject1}), #message{type = groupchat, from = MyNickJID, subject = Subject1} = recv_message(Config), ct:comment("Waiting for the master to disallow subject change"), [104] = recv_config_change_message(Config), ct:comment("Fail trying to change the subject"), send(Config, #message{to = Room, type = groupchat, subject = Subject2}), #message{from = Room, type = error} = ErrMsg = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), ok = leave(Config), disconnect(Config). history_master(Config) -> Room = muc_room_jid(Config), ServerHost = ?config(server_host, Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), PeerNickJID = peer_muc_jid(Config), Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), ok = join_new(Config), ct:comment("Putting ~p+1 messages in the history", [Size]), %% Only Size messages will be stored lists:foreach( fun(I) -> Body = xmpp:mk_text(integer_to_binary(I)), send(Config, #message{to = Room, type = groupchat, body = Body}), #message{type = groupchat, from = MyNickJID, body = Body} = recv_message(Config) end, lists:seq(0, Size)), put_event(Config, join), lists:foreach( fun(Type) -> recv_muc_presence(Config, PeerNickJID, Type) end, [available, unavailable, available, unavailable, available, unavailable, available, unavailable]), ok = leave(Config), disconnect(Config). history_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(peer_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ServerHost = ?config(server_host, Config), Size = mod_muc_opt:history_size(iolist_to_binary(ServerHost)), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {History, _, _} = join(Config), ct:comment("Checking ordering of history events"), BodyList = [binary_to_integer(xmpp:get_text(Body)) || #message{type = groupchat, from = From, body = Body} <- History, From == PeerNickJID], BodyList = lists:seq(1, Size), ok = leave(Config), %% If the client wishes to receive no history, it MUST set the 'maxchars' %% attribute to a value of "0" (zero) %% (http://xmpp.org/extensions/xep-0045.html#enter-managehistory) ct:comment("Checking if maxchars=0 yields to no history"), {[], _, _} = join(Config, #muc{history = #muc_history{maxchars = 0}}), ok = leave(Config), ct:comment("Receiving only 10 last stanzas"), {History10, _, _} = join(Config, #muc{history = #muc_history{maxstanzas = 10}}), BodyList10 = [binary_to_integer(xmpp:get_text(Body)) || #message{type = groupchat, from = From, body = Body} <- History10, From == PeerNickJID], BodyList10 = lists:nthtail(Size-10, lists:seq(1, Size)), ok = leave(Config), #delay{stamp = TS} = xmpp:get_subtag(hd(History), #delay{}), ct:comment("Receiving all history without the very first element"), {HistoryWithoutFirst, _, _} = join(Config, #muc{history = #muc_history{since = TS}}), BodyListWithoutFirst = [binary_to_integer(xmpp:get_text(Body)) || #message{type = groupchat, from = From, body = Body} <- HistoryWithoutFirst, From == PeerNickJID], BodyListWithoutFirst = lists:nthtail(1, lists:seq(1, Size)), ok = leave(Config), disconnect(Config). invite_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), ok = join_new(Config), wait_for_slave(Config), %% Inviting the peer send(Config, #message{to = Room, type = normal, sub_els = [#muc_user{ invites = [#muc_invite{to = PeerJID}]}]}), #message{from = Room} = DeclineMsg = recv_message(Config), #muc_user{decline = #muc_decline{from = PeerJID}} = xmpp:get_subtag(DeclineMsg, #muc_user{}), ok = leave(Config), disconnect(Config). invite_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), PeerJID = ?config(master, Config), #message{from = Room, type = normal} = Msg = recv_message(Config), #muc_user{invites = [#muc_invite{from = PeerJID}]} = xmpp:get_subtag(Msg, #muc_user{}), %% Decline invitation send(Config, #message{to = Room, sub_els = [#muc_user{ decline = #muc_decline{to = PeerJID}}]}), disconnect(Config). invite_members_only_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), ok = join_new(Config), %% Setting the room to members-only [_|_] = set_config(Config, [{membersonly, true}]), wait_for_slave(Config), %% Inviting the peer send(Config, #message{to = Room, type = normal, sub_els = [#muc_user{ invites = [#muc_invite{to = PeerJID}]}]}), #message{from = Room, type = normal} = AffMsg = recv_message(Config), #muc_user{items = [#muc_item{jid = PeerJID, affiliation = member}]} = xmpp:get_subtag(AffMsg, #muc_user{}), ok = leave(Config), disconnect(Config). invite_members_only_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), %% Receiving invitation #message{from = Room, type = normal} = recv_message(Config), disconnect(Config). invite_password_protected_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), Password = p1_rand:get_string(), ok = join_new(Config), [104] = set_config(Config, [{passwordprotectedroom, true}, {roomsecret, Password}]), put_event(Config, Password), %% Inviting the peer send(Config, #message{to = Room, type = normal, sub_els = [#muc_user{ invites = [#muc_invite{to = PeerJID}]}]}), ok = leave(Config), disconnect(Config). invite_password_protected_slave(Config) -> Room = muc_room_jid(Config), Password = get_event(Config), %% Receiving invitation #message{from = Room, type = normal} = Msg = recv_message(Config), #muc_user{password = Password} = xmpp:get_subtag(Msg, #muc_user{}), disconnect(Config). voice_request_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), [104] = set_config(Config, [{members_by_default, false}]), wait_for_slave(Config), #muc_user{ items = [#muc_item{role = visitor, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving voice request"), #message{from = Room, type = normal} = VoiceReq = recv_message(Config), #xdata{type = form, fields = Fs} = xmpp:get_subtag(VoiceReq, #xdata{}), [{jid, PeerJID}, {request_allow, false}, {role, participant}, {roomnick, PeerNick}] = lists:sort(muc_request:decode(Fs)), ct:comment("Approving voice request"), ApprovalFs = muc_request:encode([{jid, PeerJID}, {role, participant}, {roomnick, PeerNick}, {request_allow, true}]), send(Config, #message{to = Room, sub_els = [#xdata{type = submit, fields = ApprovalFs}]}), #muc_user{ items = [#muc_item{role = participant, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). voice_request_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), wait_for_master(Config), {[], _, _} = join(Config, visitor), ct:comment("Requesting voice"), Fs = muc_request:encode([{role, participant}]), X = #xdata{type = submit, fields = Fs}, send(Config, #message{to = Room, sub_els = [X]}), ct:comment("Waiting to become a participant"), #muc_user{ items = [#muc_item{role = participant, jid = MyJID, affiliation = none}]} = recv_muc_presence(Config, MyNickJID, available), ok = leave(Config), disconnect(Config). change_role_master(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), MyNick = ?config(nick, Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), #muc_user{items = [#muc_item{role = participant, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), lists:foreach( fun(Role) -> ct:comment("Checking if the slave is not in the roles list"), case get_role(Config, Role) of [#muc_item{jid = MyJID, affiliation = owner, role = moderator, nick = MyNick}] when Role == moderator -> ok; [] -> ok end, Reason = p1_rand:get_string(), put_event(Config, {Role, Reason}), ok = set_role(Config, Role, Reason), ct:comment("Receiving role change to ~s", [Role]), #muc_user{ items = [#muc_item{role = Role, affiliation = none, reason = Reason}]} = recv_muc_presence(Config, PeerNickJID, available), [#muc_item{role = Role, affiliation = none, nick = PeerNick}|_] = get_role(Config, Role) end, [visitor, participant, moderator]), put_event(Config, disconnect), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). change_role_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), change_role_slave(Config, get_event(Config)). change_role_slave(Config, {Role, Reason}) -> Room = muc_room_jid(Config), MyNick = ?config(slave_nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Receiving role change to ~s", [Role]), #muc_user{status_codes = Codes, items = [#muc_item{role = Role, affiliation = none, reason = Reason}]} = recv_muc_presence(Config, MyNickJID, available), true = lists:member(110, Codes), change_role_slave(Config, get_event(Config)); change_role_slave(Config, disconnect) -> ok = leave(Config), disconnect(Config). change_affiliation_master(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), MyBareJID = jid:remove_resource(MyJID), MyNick = ?config(nick, Config), PeerJID = ?config(slave, Config), PeerBareJID = jid:remove_resource(PeerJID), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), #muc_user{items = [#muc_item{role = participant, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), lists:foreach( fun({Aff, Role, Status}) -> ct:comment("Checking if slave is not in affiliation list"), case get_affiliation(Config, Aff) of [#muc_item{jid = MyBareJID, affiliation = owner}] when Aff == owner -> ok; [] -> ok end, Reason = p1_rand:get_string(), put_event(Config, {Aff, Role, Status, Reason}), ok = set_affiliation(Config, Aff, Reason), ct:comment("Receiving affiliation change to ~s", [Aff]), #muc_user{ items = [#muc_item{role = Role, affiliation = Aff, actor = Actor, reason = Reason}]} = recv_muc_presence(Config, PeerNickJID, Status), if Aff == outcast -> ct:comment("Checking if actor is set"), #muc_actor{nick = MyNick} = Actor; true -> ok end, Affs = get_affiliation(Config, Aff), ct:comment("Checking if the affiliation was correctly set"), case lists:keyfind(PeerBareJID, #muc_item.jid, Affs) of false when Aff == none -> ok; #muc_item{affiliation = Aff} -> ok end end, [{member, participant, available}, {none, visitor, available}, {admin, moderator, available}, {owner, moderator, available}, {outcast, none, unavailable}]), ok = leave(Config), disconnect(Config). change_affiliation_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), change_affiliation_slave(Config, get_event(Config)). change_affiliation_slave(Config, {Aff, Role, Status, Reason}) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Receiving affiliation change to ~s", [Aff]), if Aff == outcast -> #presence{from = Room, type = unavailable} = recv_presence(Config); true -> ok end, #muc_user{status_codes = Codes, items = [#muc_item{role = Role, actor = Actor, affiliation = Aff, reason = Reason}]} = recv_muc_presence(Config, MyNickJID, Status), true = lists:member(110, Codes), if Aff == outcast -> ct:comment("Checking for status code '301' (banned)"), true = lists:member(301, Codes), ct:comment("Checking if actor is set"), #muc_actor{nick = PeerNick} = Actor, disconnect(Config); true -> change_affiliation_slave(Config, get_event(Config)) end. kick_master(Config) -> Room = muc_room_jid(Config), MyNick = ?config(nick, Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), Reason = <<"Testing">>, ok = join_new(Config), ct:comment("Waiting for the slave to join"), wait_for_slave(Config), #muc_user{items = [#muc_item{role = participant, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), [#muc_item{role = participant, affiliation = none, nick = PeerNick}|_] = get_role(Config, participant), ct:comment("Kicking slave"), ok = set_role(Config, none, Reason), ct:comment("Receiving role change to 'none'"), #muc_user{ status_codes = Codes, items = [#muc_item{role = none, affiliation = none, actor = #muc_actor{nick = MyNick}, reason = Reason}]} = recv_muc_presence(Config, PeerNickJID, unavailable), [] = get_role(Config, participant), ct:comment("Checking if the code is '307' (kicked)"), true = lists:member(307, Codes), ok = leave(Config), disconnect(Config). kick_slave(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(master_nick, Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), Reason = <<"Testing">>, wait_for_master(Config), {[], _, _} = join(Config), ct:comment("Receiving role change to 'none'"), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = Codes, items = [#muc_item{role = none, affiliation = none, actor = #muc_actor{nick = PeerNick}, reason = Reason}]} = recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if codes '110' (self-presence) " "and '307' (kicked) are present"), true = lists:member(110, Codes), true = lists:member(307, Codes), disconnect(Config). destroy_master(Config) -> Reason = <<"Testing">>, Room = muc_room_jid(Config), AltRoom = alt_room_jid(Config), PeerJID = ?config(peer, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ok = join_new(Config), ct:comment("Waiting for slave to join"), wait_for_slave(Config), #muc_user{items = [#muc_item{role = participant, jid = PeerJID, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), wait_for_slave(Config), ok = destroy(Config, Reason), ct:comment("Receiving destruction presence"), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{items = [#muc_item{role = none, affiliation = none}], destroy = #muc_destroy{jid = AltRoom, reason = Reason}} = recv_muc_presence(Config, MyNickJID, unavailable), disconnect(Config). destroy_slave(Config) -> Reason = <<"Testing">>, Room = muc_room_jid(Config), AltRoom = alt_room_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), wait_for_master(Config), {[], _, _} = join(Config), #stanza_error{reason = 'forbidden'} = destroy(Config, Reason), wait_for_master(Config), ct:comment("Receiving destruction presence"), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{items = [#muc_item{role = none, affiliation = none}], destroy = #muc_destroy{jid = AltRoom, reason = Reason}} = recv_muc_presence(Config, MyNickJID, unavailable), disconnect(Config). vcard_master(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), FN = p1_rand:get_string(), VCard = #vcard_temp{fn = FN}, ok = join_new(Config), ct:comment("Waiting for slave to join"), wait_for_slave(Config), #muc_user{items = [#muc_item{role = participant, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), #stanza_error{reason = 'item-not-found'} = get_vcard(Config), ok = set_vcard(Config, VCard), VCard = get_vcard(Config), put_event(Config, VCard), recv_muc_presence(Config, PeerNickJID, unavailable), leave = get_event(Config), ok = leave(Config), disconnect(Config). vcard_slave(Config) -> wait_for_master(Config), {[], _, _} = join(Config), [104] = recv_config_change_message(Config), VCard = get_event(Config), VCard = get_vcard(Config), #stanza_error{reason = 'forbidden'} = set_vcard(Config, VCard), ok = leave(Config), VCard = get_vcard(Config), put_event(Config, leave), disconnect(Config). nick_change_master(Config) -> NewNick = p1_rand:get_string(), PeerJID = ?config(peer, Config), PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), put_event(Config, {new_nick, NewNick}), ct:comment("Waiting for nickchange presence from the slave"), #muc_user{status_codes = Codes, items = [#muc_item{jid = PeerJID, nick = NewNick}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '303' (nick change) is set"), true = lists:member(303, Codes), ct:comment("Waiting for updated presence from the slave"), PeerNewNickJID = jid:replace_resource(PeerNickJID, NewNick), recv_muc_presence(Config, PeerNewNickJID, available), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNewNickJID, unavailable), ok = leave(Config), disconnect(Config). nick_change_slave(Config) -> MyJID = my_jid(Config), MyNickJID = my_muc_jid(Config), {[], _, _} = slave_join(Config), {new_nick, NewNick} = get_event(Config), MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), ct:comment("Sending new presence"), send(Config, #presence{to = MyNewNickJID}), ct:comment("Receiving nickchange self-presence"), #muc_user{status_codes = Codes1, items = [#muc_item{role = participant, jid = MyJID, nick = NewNick}]} = recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if codes '110' (self-presence) and " "'303' (nickchange) are present"), lists:member(110, Codes1), lists:member(303, Codes1), ct:comment("Receiving self-presence update"), #muc_user{status_codes = Codes2, items = [#muc_item{jid = MyJID, role = participant}]} = recv_muc_presence(Config, MyNewNickJID, available), ct:comment("Checking if code '110' (self-presence) is set"), lists:member(110, Codes2), NewConfig = set_opt(nick, NewNick, Config), ok = leave(NewConfig), disconnect(NewConfig). config_title_desc_master(Config) -> Title = p1_rand:get_string(), Desc = p1_rand:get_string(), Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = master_join(Config), [104] = set_config(Config, [{roomname, Title}, {roomdesc, Desc}]), RoomCfg = get_config(Config), Title = proplists:get_value(roomname, RoomCfg), Desc = proplists:get_value(roomdesc, RoomCfg), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_title_desc_slave(Config) -> {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ok = leave(Config), disconnect(Config). config_public_list_master(Config) -> Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), wait_for_slave(Config), recv_muc_presence(Config, PeerNickJID, available), lists:member(<<"muc_public">>, get_features(Config, Room)), [104] = set_config(Config, [{public_list, false}, {publicroom, false}]), recv_muc_presence(Config, PeerNickJID, unavailable), lists:member(<<"muc_hidden">>, get_features(Config, Room)), wait_for_slave(Config), ok = leave(Config), disconnect(Config). config_public_list_slave(Config) -> Room = muc_room_jid(Config), wait_for_master(Config), PeerNick = ?config(peer_nick, Config), PeerNickJID = peer_muc_jid(Config), [#disco_item{jid = Room}] = disco_items(Config), [#disco_item{jid = PeerNickJID, name = PeerNick}] = disco_room_items(Config), {[], _, _} = join(Config), [104] = recv_config_change_message(Config), ok = leave(Config), [] = disco_items(Config), [] = disco_room_items(Config), wait_for_master(Config), disconnect(Config). config_password_master(Config) -> Password = p1_rand:get_string(), Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), lists:member(<<"muc_unsecured">>, get_features(Config, Room)), [104] = set_config(Config, [{passwordprotectedroom, true}, {roomsecret, Password}]), lists:member(<<"muc_passwordprotected">>, get_features(Config, Room)), put_event(Config, Password), recv_muc_presence(Config, PeerNickJID, available), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_password_slave(Config) -> Password = get_event(Config), #stanza_error{reason = 'not-authorized'} = join(Config), #stanza_error{reason = 'not-authorized'} = join(Config, #muc{password = p1_rand:get_string()}), {[], _, _} = join(Config, #muc{password = Password}), ok = leave(Config), disconnect(Config). config_whois_master(Config) -> Room = muc_room_jid(Config), PeerNickJID = peer_muc_jid(Config), MyNickJID = my_muc_jid(Config), ok = master_join(Config), lists:member(<<"muc_semianonymous">>, get_features(Config, Room)), [172] = set_config(Config, [{whois, anyone}]), lists:member(<<"muc_nonanonymous">>, get_features(Config, Room)), recv_muc_presence(Config, PeerNickJID, unavailable), recv_muc_presence(Config, PeerNickJID, available), send(Config, #presence{to = Room}), recv_muc_presence(Config, MyNickJID, available), [173] = set_config(Config, [{whois, moderators}]), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_whois_slave(Config) -> PeerJID = ?config(peer, Config), PeerNickJID = peer_muc_jid(Config), {[], _, _} = slave_join(Config), ct:comment("Checking if the room becomes non-anonymous (code '172')"), [172] = recv_config_change_message(Config), ct:comment("Re-joining in order to check status codes"), ok = leave(Config), {[], _, Codes} = join(Config), ct:comment("Checking if code '100' (non-anonymous) present"), true = lists:member(100, Codes), ct:comment("Receiving presence from peer with JID exposed"), #muc_user{items = [#muc_item{jid = PeerJID}]} = recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for the room to become anonymous again (code '173')"), [173] = recv_config_change_message(Config), ok = leave(Config), disconnect(Config). config_members_only_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), PeerBareJID = jid:remove_resource(PeerJID), PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), lists:member(<<"muc_open">>, get_features(Config, Room)), [104] = set_config(Config, [{membersonly, true}]), #muc_user{status_codes = Codes, items = [#muc_item{jid = PeerJID, affiliation = none, role = none}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '322' (non-member) is set"), true = lists:member(322, Codes), lists:member(<<"muc_membersonly">>, get_features(Config, Room)), ct:comment("Waiting for slave to fail joining the room"), set_member = get_event(Config), ok = set_affiliation(Config, member, p1_rand:get_string()), #message{from = Room, type = normal} = Msg = recv_message(Config), #muc_user{items = [#muc_item{jid = PeerBareJID, affiliation = member}]} = xmpp:get_subtag(Msg, #muc_user{}), ct:comment("Asking peer to join"), put_event(Config, join), ct:comment("Waiting for peer to join"), recv_muc_presence(Config, PeerNickJID, available), ok = set_affiliation(Config, none, p1_rand:get_string()), ct:comment("Waiting for peer to be kicked"), #muc_user{status_codes = NewCodes, items = [#muc_item{affiliation = none, role = none}]} = recv_muc_presence(Config, PeerNickJID, unavailable), ct:comment("Checking if code '321' (became non-member in " "members-only room) is set"), true = lists:member(321, NewCodes), ok = leave(Config), disconnect(Config). config_members_only_slave(Config) -> Room = muc_room_jid(Config), MyJID = my_jid(Config), MyNickJID = my_muc_jid(Config), {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ct:comment("Getting kicked because the room has become members-only"), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = Codes, items = [#muc_item{jid = MyJID, role = none, affiliation = none}]} = recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if the code '110' (self-presence) " "and '322' (non-member) is set"), true = lists:member(110, Codes), true = lists:member(322, Codes), ct:comment("Fail trying to join members-only room"), #stanza_error{reason = 'registration-required'} = join(Config), ct:comment("Asking the peer to set us member"), put_event(Config, set_member), ct:comment("Waiting for the peer to ask for join"), join = get_event(Config), {[], _, _} = join(Config, participant, member), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{status_codes = NewCodes, items = [#muc_item{jid = MyJID, role = none, affiliation = none}]} = recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if the code '110' (self-presence) " "and '321' (became non-member in members-only room) is set"), true = lists:member(110, NewCodes), true = lists:member(321, NewCodes), disconnect(Config). config_moderated_master(Config) -> Room = muc_room_jid(Config), PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), lists:member(<<"muc_moderated">>, get_features(Config, Room)), ok = set_role(Config, visitor, p1_rand:get_string()), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, PeerNickJID, available), set_unmoderated = get_event(Config), [104] = set_config(Config, [{moderatedroom, false}]), #message{from = PeerNickJID, type = groupchat} = recv_message(Config), recv_muc_presence(Config, PeerNickJID, unavailable), lists:member(<<"muc_unmoderated">>, get_features(Config, Room)), ok = leave(Config), disconnect(Config). config_moderated_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), {[], _, _} = slave_join(Config), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, MyNickJID, available), send(Config, #message{to = Room, type = groupchat}), ErrMsg = #message{from = Room, type = error} = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), put_event(Config, set_unmoderated), [104] = recv_config_change_message(Config), send(Config, #message{to = Room, type = groupchat}), #message{from = MyNickJID, type = groupchat} = recv_message(Config), ok = leave(Config), disconnect(Config). config_private_messages_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), ct:comment("Waiting for a private message from the slave"), #message{from = PeerNickJID, type = chat} = recv_message(Config), ok = set_role(Config, visitor, <<>>), ct:comment("Waiting for the peer to become a visitor"), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for a private message from the slave"), #message{from = PeerNickJID, type = chat} = recv_message(Config), [104] = set_config(Config, [{allow_private_messages_from_visitors, moderators}]), ct:comment("Waiting for a private message from the slave"), #message{from = PeerNickJID, type = chat} = recv_message(Config), [104] = set_config(Config, [{allow_private_messages_from_visitors, nobody}]), wait_for_slave(Config), [104] = set_config(Config, [{allow_private_messages_from_visitors, anyone}, {allow_private_messages, false}]), ct:comment("Fail trying to send a private message"), send(Config, #message{to = PeerNickJID, type = chat}), #message{from = PeerNickJID, type = error} = ErrMsg = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg), ok = set_role(Config, participant, <<>>), ct:comment("Waiting for the peer to become a participant"), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Waiting for the peer to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_private_messages_slave(Config) -> MyNickJID = my_muc_jid(Config), PeerNickJID = peer_muc_jid(Config), {[], _, _} = slave_join(Config), ct:comment("Sending a private message"), send(Config, #message{to = PeerNickJID, type = chat}), ct:comment("Waiting to become a visitor"), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, MyNickJID, available), ct:comment("Sending a private message"), send(Config, #message{to = PeerNickJID, type = chat}), [104] = recv_config_change_message(Config), ct:comment("Sending a private message"), send(Config, #message{to = PeerNickJID, type = chat}), [104] = recv_config_change_message(Config), ct:comment("Fail trying to send a private message"), send(Config, #message{to = PeerNickJID, type = chat}), #message{from = PeerNickJID, type = error} = ErrMsg1 = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg1), wait_for_master(Config), [104] = recv_config_change_message(Config), ct:comment("Waiting to become a participant again"), #muc_user{items = [#muc_item{role = participant}]} = recv_muc_presence(Config, MyNickJID, available), ct:comment("Fail trying to send a private message"), send(Config, #message{to = PeerNickJID, type = chat}), #message{from = PeerNickJID, type = error} = ErrMsg2 = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(ErrMsg2), ok = leave(Config), disconnect(Config). config_query_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), wait_for_slave(Config), recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving IQ query from the slave"), #iq{type = get, from = PeerNickJID, id = I, sub_els = [#ping{}]} = recv_iq(Config), send(Config, #iq{type = result, to = PeerNickJID, id = I}), [104] = set_config(Config, [{allow_query_users, false}]), ct:comment("Fail trying to send IQ"), #iq{type = error, from = PeerNickJID} = Err = send_recv(Config, #iq{type = get, to = PeerNickJID, sub_els = [#ping{}]}), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_query_slave(Config) -> PeerNickJID = peer_muc_jid(Config), wait_for_master(Config), ct:comment("Checking if IQ queries are denied from non-occupants"), #iq{type = error, from = PeerNickJID} = Err1 = send_recv(Config, #iq{type = get, to = PeerNickJID, sub_els = [#ping{}]}), #stanza_error{reason = 'not-acceptable'} = xmpp:get_error(Err1), {[], _, _} = join(Config), ct:comment("Sending IQ to the master"), #iq{type = result, from = PeerNickJID, sub_els = []} = send_recv(Config, #iq{to = PeerNickJID, type = get, sub_els = [#ping{}]}), [104] = recv_config_change_message(Config), ct:comment("Fail trying to send IQ"), #iq{type = error, from = PeerNickJID} = Err2 = send_recv(Config, #iq{type = get, to = PeerNickJID, sub_els = [#ping{}]}), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err2), ok = leave(Config), disconnect(Config). config_allow_invites_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), PeerNickJID = peer_muc_jid(Config), ok = master_join(Config), [104] = set_config(Config, [{allowinvites, true}]), ct:comment("Receiving an invitation from the slave"), #message{from = Room, type = normal} = recv_message(Config), [104] = set_config(Config, [{allowinvites, false}]), send_invitation = get_event(Config), ct:comment("Sending an invitation"), send(Config, #message{to = Room, type = normal, sub_els = [#muc_user{ invites = [#muc_invite{to = PeerJID}]}]}), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_allow_invites_slave(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), InviteMsg = #message{to = Room, type = normal, sub_els = [#muc_user{ invites = [#muc_invite{to = PeerJID}]}]}, {[], _, _} = slave_join(Config), [104] = recv_config_change_message(Config), ct:comment("Sending an invitation"), send(Config, InviteMsg), [104] = recv_config_change_message(Config), ct:comment("Fail sending an invitation"), send(Config, InviteMsg), #message{from = Room, type = error} = Err = recv_message(Config), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), ct:comment("Checking if the master is still able to send invitations"), put_event(Config, send_invitation), #message{from = Room, type = normal} = recv_message(Config), ok = leave(Config), disconnect(Config). config_visitor_status_master(Config) -> PeerNickJID = peer_muc_jid(Config), Status = xmpp:mk_text(p1_rand:get_string()), ok = join_new(Config), [104] = set_config(Config, [{members_by_default, false}]), ct:comment("Asking the slave to join as a visitor"), put_event(Config, {join, Status}), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, PeerNickJID, available), ct:comment("Receiving status change from the visitor"), #presence{from = PeerNickJID, status = Status} = recv_presence(Config), [104] = set_config(Config, [{allow_visitor_status, false}]), ct:comment("Receiving status change with <status/> stripped"), #presence{from = PeerNickJID, status = []} = recv_presence(Config), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_visitor_status_slave(Config) -> Room = muc_room_jid(Config), MyNickJID = my_muc_jid(Config), ct:comment("Waiting for 'join' command from the master"), {join, Status} = get_event(Config), {[], _, _} = join(Config, visitor, none), ct:comment("Sending status change"), send(Config, #presence{to = Room, status = Status}), #presence{from = MyNickJID, status = Status} = recv_presence(Config), [104] = recv_config_change_message(Config), ct:comment("Sending status change again"), send(Config, #presence{to = Room, status = Status}), #presence{from = MyNickJID, status = []} = recv_presence(Config), ok = leave(Config), disconnect(Config). config_allow_voice_requests_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), [104] = set_config(Config, [{members_by_default, false}]), ct:comment("Asking the slave to join as a visitor"), put_event(Config, join), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{allow_voice_requests, false}]), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_allow_voice_requests_slave(Config) -> Room = muc_room_jid(Config), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {[], _, _} = join(Config, visitor), [104] = recv_config_change_message(Config), ct:comment("Fail sending voice request"), Fs = muc_request:encode([{role, participant}]), X = #xdata{type = submit, fields = Fs}, send(Config, #message{to = Room, sub_els = [X]}), #message{from = Room, type = error} = Err = recv_message(Config), #stanza_error{reason = 'forbidden'} = xmpp:get_error(Err), ok = leave(Config), disconnect(Config). config_voice_request_interval_master(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(peer, Config), PeerNick = ?config(peer_nick, Config), PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), [104] = set_config(Config, [{members_by_default, false}]), ct:comment("Asking the slave to join as a visitor"), put_event(Config, join), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{voice_request_min_interval, 5}]), ct:comment("Receiving a voice request from slave"), #message{from = Room, type = normal} = recv_message(Config), ct:comment("Deny voice request at first"), Fs = muc_request:encode([{jid, PeerJID}, {role, participant}, {roomnick, PeerNick}, {request_allow, false}]), send(Config, #message{to = Room, sub_els = [#xdata{type = submit, fields = Fs}]}), put_event(Config, denied), ct:comment("Waiting for repeated voice request from the slave"), #message{from = Room, type = normal} = recv_message(Config), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_voice_request_interval_slave(Config) -> Room = muc_room_jid(Config), Fs = muc_request:encode([{role, participant}]), X = #xdata{type = submit, fields = Fs}, ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {[], _, _} = join(Config, visitor), [104] = recv_config_change_message(Config), ct:comment("Sending voice request"), send(Config, #message{to = Room, sub_els = [X]}), ct:comment("Waiting for the master to deny our voice request"), denied = get_event(Config), ct:comment("Requesting voice again"), send(Config, #message{to = Room, sub_els = [X]}), ct:comment("Receving voice request error because we're sending to fast"), #message{from = Room, type = error} = Err = recv_message(Config), #stanza_error{reason = 'resource-constraint'} = xmpp:get_error(Err), ct:comment("Waiting for 5 seconds"), timer:sleep(timer:seconds(5)), ct:comment("Repeating again"), send(Config, #message{to = Room, sub_els = [X]}), ok = leave(Config), disconnect(Config). config_visitor_nickchange_master(Config) -> PeerNickJID = peer_muc_jid(Config), ok = join_new(Config), [104] = set_config(Config, [{members_by_default, false}]), ct:comment("Asking the slave to join as a visitor"), put_event(Config, join), ct:comment("Waiting for the slave to join"), #muc_user{items = [#muc_item{role = visitor}]} = recv_muc_presence(Config, PeerNickJID, available), [104] = set_config(Config, [{allow_visitor_nickchange, false}]), ct:comment("Waiting for the slave to leave"), recv_muc_presence(Config, PeerNickJID, unavailable), ok = leave(Config), disconnect(Config). config_visitor_nickchange_slave(Config) -> NewNick = p1_rand:get_string(), MyNickJID = my_muc_jid(Config), MyNewNickJID = jid:replace_resource(MyNickJID, NewNick), ct:comment("Waiting for 'join' command from the master"), join = get_event(Config), {[], _, _} = join(Config, visitor), [104] = recv_config_change_message(Config), ct:comment("Fail trying to change nickname"), send(Config, #presence{to = MyNewNickJID}), #presence{from = MyNewNickJID, type = error} = Err = recv_presence(Config), #stanza_error{reason = 'not-allowed'} = xmpp:get_error(Err), ok = leave(Config), disconnect(Config). register_master(Config) -> MUC = muc_jid(Config), %% Register nick "master1" register_nick(Config, MUC, <<"">>, <<"master1">>), %% Unregister nick "master1" via jabber:register #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, to = MUC, sub_els = [#register{remove = true}]}), %% Register nick "master2" register_nick(Config, MUC, <<"">>, <<"master2">>), %% Now register nick "master" register_nick(Config, MUC, <<"master2">>, <<"master">>), %% Wait for slave to fail trying to register nick "master" wait_for_slave(Config), wait_for_slave(Config), %% Now register empty ("") nick, which means we're unregistering register_nick(Config, MUC, <<"master">>, <<"">>), disconnect(Config). register_slave(Config) -> MUC = muc_jid(Config), wait_for_master(Config), %% Trying to register occupied nick "master" Fs = muc_register:encode([{roomnick, <<"master">>}]), X = #xdata{type = submit, fields = Fs}, #iq{type = error} = send_recv(Config, #iq{type = set, to = MUC, sub_els = [#register{xdata = X}]}), wait_for_master(Config), disconnect(Config). %%%=================================================================== %%% Internal functions %%%=================================================================== single_test(T) -> list_to_atom("muc_" ++ atom_to_list(T)). master_slave_test(T) -> {list_to_atom("muc_" ++ atom_to_list(T)), [parallel], [list_to_atom("muc_" ++ atom_to_list(T) ++ "_master"), list_to_atom("muc_" ++ atom_to_list(T) ++ "_slave")]}. recv_muc_presence(Config, From, Type) -> Pres = #presence{from = From, type = Type} = recv_presence(Config), xmpp:get_subtag(Pres, #muc_user{}). join_new(Config) -> join_new(Config, muc_room_jid(Config)). join_new(Config, Room) -> MyJID = my_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), ct:comment("Joining new room ~p", [Room]), send(Config, #presence{to = MyNickJID, sub_els = [#muc{}]}), #presence{from = Room, type = available} = recv_presence(Config), %% As per XEP-0045 we MUST receive stanzas in the following order: %% 1. In-room presence from other occupants %% 2. In-room presence from the joining entity itself (so-called "self-presence") %% 3. Room history (if any) %% 4. The room subject %% 5. Live messages, presence updates, new user joins, etc. %% As this is the newly created room, we receive only the 2nd and 4th stanza. #muc_user{ status_codes = Codes, items = [#muc_item{role = moderator, jid = MyJID, affiliation = owner}]} = recv_muc_presence(Config, MyNickJID, available), ct:comment("Checking if codes '110' (self-presence) and " "'201' (new room) is set"), true = lists:member(110, Codes), true = lists:member(201, Codes), ct:comment("Receiving empty room subject"), #message{from = Room, type = groupchat, body = [], subject = [#text{data = <<>>}]} = recv_message(Config), case ?config(persistent_room, Config) of true -> [104] = set_config(Config, [{persistentroom, true}], Room), ok; false -> ok end. recv_history_and_subject(Config) -> ct:comment("Receiving room history and/or subject"), recv_history_and_subject(Config, []). recv_history_and_subject(Config, History) -> Room = muc_room_jid(Config), #message{type = groupchat, subject = Subj, body = Body, thread = Thread} = Msg = recv_message(Config), case xmpp:get_subtag(Msg, #delay{}) of #delay{from = Room} -> recv_history_and_subject(Config, [Msg|History]); false when Subj /= [], Body == [], Thread == undefined -> {lists:reverse(History), Msg} end. join(Config) -> join(Config, participant, none, #muc{}). join(Config, Role) when is_atom(Role) -> join(Config, Role, none, #muc{}); join(Config, #muc{} = SubEl) -> join(Config, participant, none, SubEl). join(Config, Role, Aff) when is_atom(Role), is_atom(Aff) -> join(Config, Role, Aff, #muc{}); join(Config, Role, #muc{} = SubEl) when is_atom(Role) -> join(Config, Role, none, SubEl). join(Config, Role, Aff, SubEl) -> ct:comment("Joining existing room as ~s/~s", [Aff, Role]), MyJID = my_jid(Config), Room = muc_room_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), PeerNick = ?config(peer_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), send(Config, #presence{to = MyNickJID, sub_els = [SubEl]}), case recv_presence(Config) of #presence{type = error, from = MyNickJID} = Err -> xmpp:get_subtag(Err, #stanza_error{}); #presence{from = Room, type = available} -> case recv_presence(Config) of #presence{type = available, from = PeerNickJID} = Pres -> #muc_user{items = [#muc_item{role = moderator, affiliation = owner}]} = xmpp:get_subtag(Pres, #muc_user{}), ct:comment("Receiving initial self-presence"), #muc_user{status_codes = Codes, items = [#muc_item{role = Role, jid = MyJID, affiliation = Aff}]} = recv_muc_presence(Config, MyNickJID, available), ct:comment("Checking if code '110' (self-presence) is set"), true = lists:member(110, Codes), {History, Subj} = recv_history_and_subject(Config), {History, Subj, Codes}; #presence{type = available, from = MyNickJID} = Pres -> #muc_user{status_codes = Codes, items = [#muc_item{role = Role, jid = MyJID, affiliation = Aff}]} = xmpp:get_subtag(Pres, #muc_user{}), ct:comment("Checking if code '110' (self-presence) is set"), true = lists:member(110, Codes), {History, Subj} = recv_history_and_subject(Config), {empty, History, Subj, Codes} end end. leave(Config) -> leave(Config, muc_room_jid(Config)). leave(Config, Room) -> MyJID = my_jid(Config), MyNick = ?config(nick, Config), MyNickJID = jid:replace_resource(Room, MyNick), Mode = ?config(mode, Config), IsPersistent = ?config(persistent_room, Config), if Mode /= slave, IsPersistent -> [104] = set_config(Config, [{persistentroom, false}], Room); true -> ok end, ct:comment("Leaving the room"), send(Config, #presence{to = MyNickJID, type = unavailable}), #presence{from = Room, type = unavailable} = recv_presence(Config), #muc_user{ status_codes = Codes, items = [#muc_item{role = none, jid = MyJID}]} = recv_muc_presence(Config, MyNickJID, unavailable), ct:comment("Checking if code '110' (self-presence) is set"), true = lists:member(110, Codes), ok. get_config(Config) -> ct:comment("Get room config"), Room = muc_room_jid(Config), case send_recv(Config, #iq{type = get, to = Room, sub_els = [#muc_owner{}]}) of #iq{type = result, sub_els = [#muc_owner{config = #xdata{type = form} = X}]} -> muc_roomconfig:decode(X#xdata.fields); #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. set_config(Config, RoomConfig) -> set_config(Config, RoomConfig, muc_room_jid(Config)). set_config(Config, RoomConfig, Room) -> ct:comment("Set room config: ~p", [RoomConfig]), Fs = case RoomConfig of [] -> []; _ -> muc_roomconfig:encode(RoomConfig) end, case send_recv(Config, #iq{type = set, to = Room, sub_els = [#muc_owner{config = #xdata{type = submit, fields = Fs}}]}) of #iq{type = result, sub_els = []} -> #presence{from = Room, type = available} = recv_presence(Config), #message{from = Room, type = groupchat} = Msg = recv_message(Config), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), lists:sort(Codes); #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. create_persistent(Config) -> [_|_] = get_config(Config), [] = set_config(Config, [{persistentroom, true}], false), ok. destroy(Config) -> destroy(Config, <<>>). destroy(Config, Reason) -> Room = muc_room_jid(Config), AltRoom = alt_room_jid(Config), ct:comment("Destroying a room"), case send_recv(Config, #iq{type = set, to = Room, sub_els = [#muc_owner{destroy = #muc_destroy{ reason = Reason, jid = AltRoom}}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. disco_items(Config) -> MUC = muc_jid(Config), ct:comment("Performing disco#items request to ~s", [jid:encode(MUC)]), #iq{type = result, from = MUC, sub_els = [DiscoItems]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#disco_items{}]}), lists:keysort(#disco_item.jid, DiscoItems#disco_items.items). disco_room_items(Config) -> Room = muc_room_jid(Config), #iq{type = result, from = Room, sub_els = [DiscoItems]} = send_recv(Config, #iq{type = get, to = Room, sub_els = [#disco_items{}]}), DiscoItems#disco_items.items. get_affiliations(Config, Aff) -> Room = muc_room_jid(Config), case send_recv(Config, #iq{type = get, to = Room, sub_els = [#muc_admin{items = [#muc_item{affiliation = Aff}]}]}) of #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> Items; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. master_join(Config) -> Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerNick = ?config(slave_nick, Config), PeerNickJID = jid:replace_resource(Room, PeerNick), ok = join_new(Config), wait_for_slave(Config), #muc_user{items = [#muc_item{jid = PeerJID, role = participant, affiliation = none}]} = recv_muc_presence(Config, PeerNickJID, available), ok. slave_join(Config) -> wait_for_master(Config), join(Config). set_role(Config, Role, Reason) -> ct:comment("Changing role to ~s", [Role]), Room = muc_room_jid(Config), PeerNick = ?config(slave_nick, Config), case send_recv( Config, #iq{type = set, to = Room, sub_els = [#muc_admin{ items = [#muc_item{role = Role, reason = Reason, nick = PeerNick}]}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. get_role(Config, Role) -> ct:comment("Requesting list for role '~s'", [Role]), Room = muc_room_jid(Config), case send_recv( Config, #iq{type = get, to = Room, sub_els = [#muc_admin{ items = [#muc_item{role = Role}]}]}) of #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> lists:keysort(#muc_item.affiliation, Items); #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. set_affiliation(Config, Aff, Reason) -> ct:comment("Changing affiliation to ~s", [Aff]), Room = muc_room_jid(Config), PeerJID = ?config(slave, Config), PeerBareJID = jid:remove_resource(PeerJID), case send_recv( Config, #iq{type = set, to = Room, sub_els = [#muc_admin{ items = [#muc_item{affiliation = Aff, reason = Reason, jid = PeerBareJID}]}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. get_affiliation(Config, Aff) -> ct:comment("Requesting list for affiliation '~s'", [Aff]), Room = muc_room_jid(Config), case send_recv( Config, #iq{type = get, to = Room, sub_els = [#muc_admin{ items = [#muc_item{affiliation = Aff}]}]}) of #iq{type = result, sub_els = [#muc_admin{items = Items}]} -> Items; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. set_vcard(Config, VCard) -> Room = muc_room_jid(Config), ct:comment("Setting vCard for ~s", [jid:encode(Room)]), case send_recv(Config, #iq{type = set, to = Room, sub_els = [VCard]}) of #iq{type = result, sub_els = []} -> [104] = recv_config_change_message(Config), ok; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. get_vcard(Config) -> Room = muc_room_jid(Config), ct:comment("Retreiving vCard from ~s", [jid:encode(Room)]), case send_recv(Config, #iq{type = get, to = Room, sub_els = [#vcard_temp{}]}) of #iq{type = result, sub_els = [VCard]} -> VCard; #iq{type = error} = Err -> xmpp:get_subtag(Err, #stanza_error{}) end. recv_config_change_message(Config) -> ct:comment("Receiving configuration change notification message"), Room = muc_room_jid(Config), #presence{from = Room, type = available} = recv_presence(Config), #message{type = groupchat, from = Room} = Msg = recv_message(Config), #muc_user{status_codes = Codes} = xmpp:get_subtag(Msg, #muc_user{}), lists:sort(Codes). register_nick(Config, MUC, PrevNick, Nick) -> PrevRegistered = if PrevNick /= <<"">> -> true; true -> false end, NewRegistered = if Nick /= <<"">> -> true; true -> false end, ct:comment("Requesting registration form"), #iq{type = result, sub_els = [#register{registered = PrevRegistered, xdata = #xdata{type = form, fields = FsWithoutNick}}]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#register{}]}), ct:comment("Checking if previous nick is registered"), PrevNick = proplists:get_value( roomnick, muc_register:decode(FsWithoutNick)), X = #xdata{type = submit, fields = muc_register:encode([{roomnick, Nick}])}, ct:comment("Submitting registration form"), #iq{type = result, sub_els = []} = send_recv(Config, #iq{type = set, to = MUC, sub_els = [#register{xdata = X}]}), ct:comment("Checking if new nick was registered"), #iq{type = result, sub_els = [#register{registered = NewRegistered, xdata = #xdata{type = form, fields = FsWithNick}}]} = send_recv(Config, #iq{type = get, to = MUC, sub_els = [#register{}]}), Nick = proplists:get_value( roomnick, muc_register:decode(FsWithNick)). subscribe(Config, Events, Room) -> MyNick = ?config(nick, Config), case send_recv(Config, #iq{type = set, to = Room, sub_els = [#muc_subscribe{nick = MyNick, events = Events}]}) of #iq{type = result, sub_els = [#muc_subscribe{events = ResEvents}]} -> lists:sort(ResEvents); #iq{type = error} = Err -> xmpp:get_error(Err) end. unsubscribe(Config, Room) -> case send_recv(Config, #iq{type = set, to = Room, sub_els = [#muc_unsubscribe{}]}) of #iq{type = result, sub_els = []} -> ok; #iq{type = error} = Err -> xmpp:get_error(Err) end. ����������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/plugins/�����������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�015313� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/plugins/deps_erl_opts.erl������������������������������������������������������������0000644�0002322�0002322�00000001044�14154362354�020660� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-module(deps_erl_opts). -export([preprocess/2]). preprocess(Config, Dirs) -> ExtraOpts = rebar_config:get(Config, deps_erl_opts, []), Opts = rebar_config:get(Config, erl_opts, []), NewOpts = lists:foldl(fun(Opt, Acc) when is_tuple(Opt) -> lists:keystore(element(1, Opt), 1, Acc, Opt); (Opt, Acc) -> [Opt | lists:delete(Opt, Acc)] end, Opts, ExtraOpts), {ok, rebar_config:set(Config, erl_opts, NewOpts), []}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/plugins/override_deps_versions2.erl��������������������������������������������������0000644�0002322�0002322�00000007705�14154362354�022674� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-module(override_deps_versions2). -export([preprocess/2, 'pre_update-deps'/2, new_replace/1, new_replace/0]). preprocess(Config, _Dirs) -> update_deps(Config). update_deps(Config) -> LocalDeps = rebar_config:get_local(Config, deps, []), TopDeps = case rebar_config:get_xconf(Config, top_deps, []) of [] -> LocalDeps; Val -> Val end, Config2 = rebar_config:set_xconf(Config, top_deps, TopDeps), NewDeps = lists:map(fun({Name, _, _} = Dep) -> case lists:keyfind(Name, 1, TopDeps) of false -> Dep; TopDep -> TopDep end end, LocalDeps), %io:format("LD ~p~n", [LocalDeps]), %io:format("TD ~p~n", [TopDeps]), Config3 = rebar_config:set(Config2, deps, NewDeps), {ok, Config3, []}. 'pre_update-deps'(Config, _Dirs) -> {ok, Config2, _} = update_deps(Config), case code:is_loaded(old_rebar_config) of false -> {_, Beam, _} = code:get_object_code(rebar_config), NBeam = rename(Beam, old_rebar_config), code:load_binary(old_rebar_config, "blank", NBeam), replace_mod(Beam); _ -> ok end, {ok, Config2}. new_replace() -> old_rebar_config:new(). new_replace(Config) -> NC = old_rebar_config:new(Config), {ok, Conf, _} = update_deps(NC), Conf. replace_mod(Beam) -> {ok, {_, [{exports, Exports}]}} = beam_lib:chunks(Beam, [exports]), Funcs = lists:filtermap( fun({module_info, _}) -> false; ({Name, Arity}) -> Args = args(Arity), Call = case Name of new -> [erl_syntax:application( erl_syntax:abstract(override_deps_versions2), erl_syntax:abstract(new_replace), Args)]; _ -> [erl_syntax:application( erl_syntax:abstract(old_rebar_config), erl_syntax:abstract(Name), Args)] end, {true, erl_syntax:function(erl_syntax:abstract(Name), [erl_syntax:clause(Args, none, Call)])} end, Exports), Forms0 = ([erl_syntax:attribute(erl_syntax:abstract(module), [erl_syntax:abstract(rebar_config)])] ++ Funcs), Forms = [erl_syntax:revert(Form) || Form <- Forms0], %io:format("--------------------------------------------------~n" % "~s~n", % [[erl_pp:form(Form) || Form <- Forms]]), {ok, Mod, Bin} = compile:forms(Forms, [report, export_all]), code:purge(rebar_config), {module, Mod} = code:load_binary(rebar_config, "mock", Bin). args(0) -> []; args(N) -> [arg(N) | args(N-1)]. arg(N) -> erl_syntax:variable(list_to_atom("A"++integer_to_list(N))). rename(BeamBin0, Name) -> BeamBin = replace_in_atab(BeamBin0, Name), update_form_size(BeamBin). %% Replace the first atom of the atom table with the new name replace_in_atab(<<"Atom", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> replace_first_atom(<<"Atom">>, Cnk, CnkSz0, Rest, latin1, Name); replace_in_atab(<<"AtU8", CnkSz0:32, Cnk:CnkSz0/binary, Rest/binary>>, Name) -> replace_first_atom(<<"AtU8">>, Cnk, CnkSz0, Rest, unicode, Name); replace_in_atab(<<C, Rest/binary>>, Name) -> <<C, (replace_in_atab(Rest, Name))/binary>>. replace_first_atom(CnkName, Cnk, CnkSz0, Rest, Encoding, Name) -> <<NumAtoms:32, NameSz0:8, _Name0:NameSz0/binary, CnkRest/binary>> = Cnk, NumPad0 = num_pad_bytes(CnkSz0), <<_:NumPad0/unit:8, NextCnks/binary>> = Rest, NameBin = atom_to_binary(Name, Encoding), NameSz = byte_size(NameBin), CnkSz = CnkSz0 + NameSz - NameSz0, NumPad = num_pad_bytes(CnkSz), <<CnkName/binary, CnkSz:32, NumAtoms:32, NameSz:8, NameBin:NameSz/binary, CnkRest/binary, 0:NumPad/unit:8, NextCnks/binary>>. %% Calculate the number of padding bytes that have to be added for the %% BinSize to be an even multiple of ?beam_num_bytes_alignment. num_pad_bytes(BinSize) -> case 4 - (BinSize rem 4) of 4 -> 0; N -> N end. %% Update the size within the top-level form update_form_size(<<"FOR1", _OldSz:32, Rest/binary>> = Bin) -> Sz = size(Bin) - 8, <<"FOR1", Sz:32, Rest/binary>>. �����������������������������������������������������������ejabberd-21.12/plugins/override_opts.erl������������������������������������������������������������0000644�0002322�0002322�00000002527�14154362354�020711� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-module(override_opts). -export([preprocess/2]). override_opts(override, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> rebar_config:set(Conf, Opt, Value) end, Config, Opts); override_opts(add, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> V = rebar_config:get_local(Conf, Opt, []), rebar_config:set(Conf, Opt, V ++ Value) end, Config, Opts); override_opts(del, Config, Opts) -> lists:foldl(fun({Opt, Value}, Conf) -> V = rebar_config:get_local(Conf, Opt, []), rebar_config:set(Conf, Opt, V -- Value) end, Config, Opts). preprocess(Config, _Dirs) -> Overrides = rebar_config:get_local(Config, overrides, []), TopOverrides = case rebar_config:get_xconf(Config, top_overrides, []) of [] -> Overrides; Val -> Val end, Config2 = rebar_config:set_xconf(Config, top_overrides, TopOverrides), try Config3 = case rebar_app_utils:load_app_file(Config2, _Dirs) of {ok, C, AppName, _AppData} -> lists:foldl(fun({Type, AppName2, Opts}, Conf1) when AppName2 == AppName -> override_opts(Type, Conf1, Opts); ({Type, Opts}, Conf1a) -> override_opts(Type, Conf1a, Opts); (_, Conf2) -> Conf2 end, C, TopOverrides); _ -> Config2 end, {ok, Config3, []} catch error:badarg -> {ok, Config2, []} end. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/plugins/configure_deps.erl�����������������������������������������������������������0000644�0002322�0002322�00000000153�14154362354�021012� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������-module(configure_deps). -export(['configure-deps'/2]). 'configure-deps'(Config, Vals) -> {ok, Config}. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/.devcontainer/�����������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�016371� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/.devcontainer/Dockerfile�������������������������������������������������������������0000644�0002322�0002322�00000000207�14154362354�020362� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Update the VARIANT arg to pick an Elixir version: latest, 1.11.4, etc. ARG VARIANT=latest FROM ghcr.io/processone/elixir:${VARIANT} �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/.devcontainer/docker-compose.yml�����������������������������������������������������0000644�0002322�0002322�00000000171�14154362354�022025� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd: image: ejabberd/ecs ports: - 5222:5222 - 5223:5223 - 5269:5269 - 5280:5280 - 1883:1883 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/.devcontainer/devcontainer.json������������������������������������������������������0000644�0002322�0002322�00000002216�14154362354�021746� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "name": "ejabberd", // "dockerComposeFile": "docker-compose.yml", "build": { "dockerfile": "Dockerfile", "args": { "VARIANT": "latest" // 1.11.4 } }, "workspaceFolder": "/workspace", // Set *default* container specific settings.json values on container create. "settings": { "terminal.integrated.defaultProfile.linux": "/bin/zsh", }, // Add the IDs of extensions you want installed when the container is created. "extensions": ["pgourlain.erlang", "jakebecker.elixir-ls"], // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [5222, 5280, 5269], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "sh .devcontainer/post-create.sh", // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "portsAttributes": { "1883": { "label": "MQTT" }, "5222": { "label": "XMPP C2S" }, "5223": { "label": "Legacy XMPP C2S" }, "5269": { "label": "XMPP S2S" }, "5280": { "label": "ejabberd HTTP" }, "5443": { "label": "ejabberd HTTPS" } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/ejabberd.init.template���������������������������������������������������������������0000644�0002322�0002322�00000002510�14154362354�020065� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#! /bin/sh ### BEGIN INIT INFO # Provides: ejabberd # Required-Start: $remote_fs $network $named $time # Required-Stop: $remote_fs $network $named $time # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts ejabberd XMPP server # Description: Starts ejabberd XMPP server, an XMPP # compliant server written in Erlang. ### END INIT INFO # chkconfig: 2345 90 10 # description: ejabberd XMPP server set -o errexit DIR=@ctlscriptpath@ CTL="$DIR"/ejabberdctl USER=@installuser@ test -x "$CTL" || { echo "ERROR: ejabberd not found: $DIR" exit 1 } getent passwd "$USER" >/dev/null || { echo "ERROR: System user not found: $USER" exit 2 } export PATH="${PATH:+$PATH:}/usr/sbin:/sbin" case "$1" in start) test -x "$CTL" || exit 0 echo "Starting ejabberd..." su - $USER -c "$CTL start" su - $USER -c "$CTL started" echo "done." ;; stop) test -x "$CTL" || exit 0 echo "Stopping ejabberd..." su - $USER -c "$CTL stop" su - $USER -c "$CTL stopped" echo "done." ;; status) test -x "$CTL" || exit 0 echo "Getting ejabberd status..." su - $USER -c "$CTL status" ;; force-reload|restart) "$0" stop "$0" start ;; *) echo "Usage: $0 {start|stop|restart|force-reload|status}" exit 1 esac exit 0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/cover.spec���������������������������������������������������������������������������0000644�0002322�0002322�00000000162�14154362354�015623� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{level, details}. {incl_dirs, ["src", "ebin"]}. {excl_mods, [eldap, 'ELDAPv3']}. {export, "logs/all.coverdata"}. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/m4/����������������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�014152� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/m4/erlang-extra.m4�������������������������������������������������������������������0000644�0002322�0002322�00000004620�14154362354�017007� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dnl erlang-extra.m4 AC_DEFUN([ERLANG_SUBST_LIB_VER], [AC_ERLANG_CHECK_LIB([$1]) ERLANG_LIB_VER_SUBST="$ERLANG_LIB_VER_SUBST -e 's,[@]ERLANG_LIB_VER_$1[@],\$(ERLANG_LIB_VER_$1),g'" AC_SUBST([ERLANG_LIB_VER_SUBST]) ]) # ERLANG_SUBST_LIB_VER AC_DEFUN([ERLANG_VERSION_CHECK], [ AC_MSG_CHECKING([Erlang/OTP version]) cat > conftest.erl <<EOF -module(conftest). -export([[start/0]]). start() -> ERTS = erlang:system_info(version), RequiredMin = "$1", RequiredMax = "$2", Status = case {string:tokens(RequiredMin, " "), string:tokens(RequiredMax, " ")} of {[[MinStr | _]], [[MaxStr | _]]} -> case check(ERTS, {MinStr, MaxStr}) of less -> list_to_binary([[ERTS, " found, ", RequiredMin, " required"]]); greater -> list_to_binary([[ERTS, " found, ", RequiredMax, " or earlier required"]]); ok -> <<"ok">> end; _ -> list_to_binary([[ERTS, " found, ", RequiredMin, " required"]]) end, file:write_file("conftest.out", Status), halt(). check(CurStr, {MinStr, MaxStr}) -> Cur = parse(CurStr), Min = parse(MinStr), Max = parse(MaxStr), case {less_or_equal(Min, Cur), less_or_equal(Cur, Max)} of {false, true} -> less; {true, true} -> ok; {true, false} -> greater end. parse(Version) -> lists:map(fun(A) -> {Int,[[]]} = string:to_integer(A), Int end, string:tokens(Version, ".")). less_or_equal([[]], [[]]) -> true; less_or_equal([[]], _Any) -> true; less_or_equal(_Any, [[]]) -> false; less_or_equal([[Left| Rl]], [[Right| Rr]]) -> case {Left < Right, Left == Right} of {true, _} -> true; {false, false} -> false; {false, true} -> less_or_equal(Rl, Rr) end. EOF $ERLC conftest.erl || AC_MSG_ERROR(["Could not compile Erlang/OTP version check program using '$ERLC'"]) if ! $ERL -s conftest -noshell -o ! -f conftest.out ; then AC_MSG_ERROR(["Could not run Erlang/OTP version check program using '$ERL'"]) fi if test "x`cat conftest.out`" != "xok"; then AC_MSG_RESULT([failed]) X="`cat conftest.out`" if test "[$3]" == "warn"; then AC_MSG_WARN([$X]) else AC_MSG_FAILURE([$X]) fi else AC_MSG_RESULT([ok]) fi ]) dnl ERLANG_VERSION_CHECK ����������������������������������������������������������������������������������������������������������������ejabberd-21.12/m4/ax_lib_sqlite3.m4�����������������������������������������������������������������0000644�0002322�0002322�00000011775�14154362354�017331� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# =========================================================================== # http://www.gnu.org/software/autoconf-archive/ax_lib_sqlite3.html # =========================================================================== # # SYNOPSIS # # AX_LIB_SQLITE3([MINIMUM-VERSION]) # # DESCRIPTION # # Test for the SQLite 3 library of a particular version (or newer) # # This macro takes only one optional argument, required version of SQLite # 3 library. If required version is not passed, 3.0.0 is used in the test # of existance of SQLite 3. # # If no intallation prefix to the installed SQLite library is given the # macro searches under /usr, /usr/local, and /opt. # # This macro calls: # # AC_SUBST(SQLITE3_CFLAGS) # AC_SUBST(SQLITE3_LDFLAGS) # AC_SUBST(SQLITE3_VERSION) # # And sets: # # HAVE_SQLITE3 # # LICENSE # # Copyright (c) 2008 Mateusz Loskot <mateusz@loskot.net> # # Copying and distribution of this file, with or without modification, are # permitted in any medium without royalty provided the copyright notice # and this notice are preserved. This file is offered as-is, without any # warranty. #serial 14 AC_DEFUN([AX_LIB_SQLITE3], [ AC_ARG_WITH([sqlite3], AS_HELP_STRING( [--with-sqlite3=@<:@ARG@:>@], [use SQLite 3 library @<:@default=yes@:>@, optionally specify the prefix for sqlite3 library] ), [ if test "$withval" = "no"; then WANT_SQLITE3="no" elif test "$withval" = "yes"; then WANT_SQLITE3="yes" ac_sqlite3_path="" else WANT_SQLITE3="yes" ac_sqlite3_path="$withval" fi ], [WANT_SQLITE3="yes"] ) SQLITE3_CFLAGS="" SQLITE3_LDFLAGS="" SQLITE3_VERSION="" if test "x$WANT_SQLITE3" = "xyes"; then ac_sqlite3_header="sqlite3.h" sqlite3_version_req=ifelse([$1], [], [3.0.0], [$1]) sqlite3_version_req_shorten=`expr $sqlite3_version_req : '\([[0-9]]*\.[[0-9]]*\)'` sqlite3_version_req_major=`expr $sqlite3_version_req : '\([[0-9]]*\)'` sqlite3_version_req_minor=`expr $sqlite3_version_req : '[[0-9]]*\.\([[0-9]]*\)'` sqlite3_version_req_micro=`expr $sqlite3_version_req : '[[0-9]]*\.[[0-9]]*\.\([[0-9]]*\)'` if test "x$sqlite3_version_req_micro" = "x" ; then sqlite3_version_req_micro="0" fi sqlite3_version_req_number=`expr $sqlite3_version_req_major \* 1000000 \ \+ $sqlite3_version_req_minor \* 1000 \ \+ $sqlite3_version_req_micro` AC_MSG_CHECKING([for SQLite3 library >= $sqlite3_version_req]) if test "$ac_sqlite3_path" != ""; then ac_sqlite3_ldflags="-L$ac_sqlite3_path/lib" ac_sqlite3_cppflags="-I$ac_sqlite3_path/include" else for ac_sqlite3_path_tmp in /usr /usr/local /opt ; do if test -f "$ac_sqlite3_path_tmp/include/$ac_sqlite3_header" \ && test -r "$ac_sqlite3_path_tmp/include/$ac_sqlite3_header"; then ac_sqlite3_path=$ac_sqlite3_path_tmp ac_sqlite3_cppflags="-I$ac_sqlite3_path_tmp/include" ac_sqlite3_ldflags="-L$ac_sqlite3_path_tmp/lib" break; fi done fi ac_sqlite3_ldflags="$ac_sqlite3_ldflags -lsqlite3" saved_CPPFLAGS="$CPPFLAGS" CPPFLAGS="$CPPFLAGS $ac_sqlite3_cppflags" AC_LANG_PUSH(C) AC_COMPILE_IFELSE( [ AC_LANG_PROGRAM([[@%:@include <sqlite3.h>]], [[ #if (SQLITE_VERSION_NUMBER >= $sqlite3_version_req_number) /* Everything is okay */ #else # error SQLite version is too old #endif ]] ) ], [ AC_MSG_RESULT([yes]) success="yes" ], [ AC_MSG_RESULT([not found]) success="no" ] ) AC_LANG_POP(C) CPPFLAGS="$saved_CPPFLAGS" if test "$success" = "yes"; then SQLITE3_CFLAGS="$ac_sqlite3_cppflags" SQLITE3_LDFLAGS="$ac_sqlite3_ldflags" ac_sqlite3_header_path="$ac_sqlite3_path/include/$ac_sqlite3_header" dnl Retrieve SQLite release version if test "x$ac_sqlite3_header_path" != "x"; then ac_sqlite3_version=`cat $ac_sqlite3_header_path \ | grep '#define.*SQLITE_VERSION.*\"' | sed -e 's/.* "//' \ | sed -e 's/"//'` if test $ac_sqlite3_version != ""; then SQLITE3_VERSION=$ac_sqlite3_version else AC_MSG_WARN([Cannot find SQLITE_VERSION macro in sqlite3.h header to retrieve SQLite version!]) fi fi AC_SUBST(SQLITE3_CFLAGS) AC_SUBST(SQLITE3_LDFLAGS) AC_SUBST(SQLITE3_VERSION) AC_DEFINE([HAVE_SQLITE3], [], [Have the SQLITE3 library]) fi fi ]) ���ejabberd-21.12/man/���������������������������������������������������������������������������������0000755�0002322�0002322�00000000000�14154362354�014405� 5����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/man/ejabberd.yml.5�������������������������������������������������������������������0000644�0002322�0002322�00000604605�14154362354�017044� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������'\" t .\" Title: ejabberd.yml .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 <http://docbook.sf.net/> .\" Date: 12/08/2021 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" .TH "EJABBERD\&.YML" "5" "12/08/2021" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" ejabberd.yml \- main configuration file for ejabberd\&. .SH "SYNOPSIS" .sp ejabberd\&.yml .SH "DESCRIPTION" .sp The configuration file is written in YAML language\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp YAML is indentation sensitive, so make sure you respect indentation, or otherwise you will get pretty cryptic configuration errors\&. .sp .5v .RE .sp Logically, configuration options are splitted into 3 main categories: \fIModules\fR, \fIListeners\fR and everything else called \fITop Level\fR options\&. Thus this document is splitted into 3 main chapters describing each category separately\&. So, the contents of ejabberd\&.yml will typically look like this: .sp .if n \{\ .RS 4 .\} .nf hosts: \- example\&.com \- domain\&.tld loglevel: info \&.\&.\&. listen: \- port: 5222 module: ejabberd_c2s \&.\&.\&. modules: mod_roster: {} \&.\&.\&. .fi .if n \{\ .RE .\} .sp Any configuration error (such as syntax error, unknown option or invalid option value) is fatal in the sense that ejabberd will refuse to load the whole configuration file and will not start or will abort configuration reload\&. .sp All options can be changed in runtime by running \fIejabberdctl reload\-config\fR command\&. Configuration reload is atomic: either all options are accepted and applied simultaneously or the new configuration is refused without any impact on currently running configuration\&. .sp Some options can be specified for particular virtual host(s) only using \fIhost_config\fR or \fIappend_host_config\fR options\&. Such options are called \fIlocal\fR\&. Examples are \fImodules\fR, \fIauth_method\fR and \fIdefault_db\fR\&. The options that cannot be defined per virtual host are called \fIglobal\fR\&. Examples are \fIloglevel\fR, \fIcertfiles\fR and \fIlisten\fR\&. It is a configuration mistake to put \fIglobal\fR options under \fIhost_config\fR or \fIappend_host_config\fR section \- ejabberd will refuse to load such configuration\&. .sp It is not recommended to write ejabberd\&.yml from scratch\&. Instead it is better to start from "default" configuration file available at https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example\&. Once you get ejabberd running you can start changing configuration options to meet your requirements\&. .sp Note that this document is intended to provide comprehensive description of all configuration options that can be consulted to understand the meaning of a particular option, its format and possible values\&. It will be quite hard to understand how to configure ejabberd by reading this document only \- for this purpose the reader is recommended to read online Configuration Guide available at https://docs\&.ejabberd\&.im/admin/configuration\&. .SH "TOP LEVEL OPTIONS" .sp This section describes top level options of ejabberd\&. .PP \fBaccess_rules\fR: \fI{AccessName: {allow|deny: ACLRules|ACLName}}\fR .RS 4 The option specifies access rules\&. Each access rule is assigned a name that can be referenced from other parts of the configuration file (mostly from \fIaccess\fR options of ejabberd modules)\&. Each rule definition may contain arbitrary number of \fIallow\fR or \fIdeny\fR sections, and each section may contain any number of ACL rules (see \fIacl\fR option)\&. There are no access rules defined by default\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf access_rules: configure: allow: admin something: deny: someone allow: all s2s_banned: deny: problematic_hosts deny: banned_forever deny: ip: 222\&.111\&.222\&.111/32 deny: ip: 111\&.222\&.111\&.222/32 allow: all xmlrpc_access: allow: user: peter@example\&.com allow: user: ivone@example\&.com allow: user: bot@example\&.com ip: 10\&.0\&.0\&.0/24 .fi .if n \{\ .RE .\} .RE .PP \fBacl\fR: \fI{ACLName: {ACLType: ACLValue}}\fR .RS 4 The option defines access control lists: named sets of rules which are used to match against different targets (such as a JID or an IP address)\&. Every set of rules has name \fIACLName\fR: it can be any string except \fIall\fR or \fInone\fR (those are predefined names for the rules that match all or nothing respectively)\&. The name \fIACLName\fR can be referenced from other parts of the configuration file, for example in \fIaccess_rules\fR option\&. The rules of \fIACLName\fR are represented by mapping \fI{ACLType: ACLValue}\fR\&. These can be one of the following: .PP \fBip\fR: \fINetwork\fR .RS 4 The rule matches any IP address from the \fINetwork\fR\&. .RE .PP \fBnode_glob\fR: \fIPattern\fR .RS 4 Same as \fInode_regexp\fR, but matching is performed on a specified \fIPattern\fR according to the rules used by the Unix shell\&. .RE .PP \fBnode_regexp\fR: \fIuser_regexp@server_regexp\fR .RS 4 The rule matches any JID with node part matching regular expression \fIuser_regexp\fR and server part matching regular expression \fIserver_regexp\fR\&. .RE .PP \fBresource\fR: \fIResource\fR .RS 4 The rule matches any JID with a resource \fIResource\fR\&. .RE .PP \fBresource_glob\fR: \fIPattern\fR .RS 4 Same as \fIresource_regexp\fR, but matching is performed on a specified \fIPattern\fR according to the rules used by the Unix shell\&. .RE .PP \fBresource_regexp\fR: \fIRegexp\fR .RS 4 The rule matches any JID with a resource that matches regular expression \fIRegexp\fR\&. .RE .PP \fBserver\fR: \fIServer\fR .RS 4 The rule matches any JID from server \fIServer\fR\&. The value of \fIServer\fR must be a valid hostname or an IP address\&. .RE .PP \fBserver_glob\fR: \fIPattern\fR .RS 4 Same as \fIserver_regexp\fR, but matching is performed on a specified \fIPattern\fR according to the rules used by the Unix shell\&. .RE .PP \fBserver_regexp\fR: \fIRegexp\fR .RS 4 The rule matches any JID from the server that matches regular expression \fIRegexp\fR\&. .RE .PP \fBuser\fR: \fIUsername\fR .RS 4 If \fIUsername\fR is in the form of "user@server", the rule matches a JID against this value\&. Otherwise, if \fIUsername\fR is in the form of "user", the rule matches any JID that has \fIUsername\fR in the node part as long as the server part of this JID is any virtual host served by ejabberd\&. .RE .PP \fBuser_glob\fR: \fIPattern\fR .RS 4 Same as \fIuser_regexp\fR, but matching is performed on a specified \fIPattern\fR according to the rules used by the Unix shell\&. .RE .PP \fBuser_regexp\fR: \fIRegexp\fR .RS 4 If \fIRegexp\fR is in the form of "regexp@server", the rule matches any JID with node part matching regular expression "regexp" as long as the server part of this JID is equal to "server"\&. If \fIRegexp\fR is in the form of "regexp", the rule matches any JID with node part matching regular expression "regexp" as long as the server part of this JID is any virtual host served by ejabberd\&. .RE .RE .PP \fBacme\fR: \fIOptions\fR .RS 4 ACME configuration, to automatically obtain SSL certificates for the domains served by ejabberd, which means that certificate requests and renewals are performed to some CA server (aka "ACME server") in a fully automated mode\&. The \fIOptions\fR are: .PP \fBauto\fR: \fItrue | false\fR .RS 4 Whether to automatically request certificates for all configured domains (that yet have no a certificate) on server start or configuration reload\&. The default is \fItrue\fR\&. .RE .PP \fBca_url\fR: \fIURL\fR .RS 4 The ACME directory URL used as an entry point for the ACME server\&. The default value is https://acme\-v02\&.api\&.letsencrypt\&.org/directory \- the directory URL of Let\(cqs Encrypt authority\&. .RE .PP \fBcert_type\fR: \fIrsa | ec\fR .RS 4 A type of a certificate key\&. Available values are \fIec\fR and \fIrsa\fR for EC and RSA certificates respectively\&. It\(cqs better to have RSA certificates for the purpose of backward compatibility with legacy clients and servers, thus the default is \fIrsa\fR\&. .RE .PP \fBcontact\fR: \fI[Contact, \&.\&.\&.]\fR .RS 4 A list of contact addresses (typically emails) where an ACME server will send notifications when problems occur\&. The value of \fIContact\fR must be in the form of "scheme:address" (e\&.g\&. "mailto:user@domain\&.tld")\&. The default is an empty list which means an ACME server will send no notices\&. .RE .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf acme: ca_url: https://acme\-v02\&.api\&.letsencrypt\&.org/directory contact: \- mailto:admin@domain\&.tld \- mailto:bot@domain\&.tld auto: true cert_type: rsa .fi .if n \{\ .RE .\} .RE .PP \fBallow_contrib_modules\fR: \fItrue | false\fR .RS 4 Whether to allow installation of third\-party modules or not\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_multiple_connections\fR: \fItrue | false\fR .RS 4 This option is only used when the anonymous mode is enabled\&. Setting it to \fItrue\fR means that the same username can be taken multiple times in anonymous login mode if different resource are used to connect\&. This option is only useful in very special occasions\&. The default value is \fIfalse\fR\&. .RE .PP \fBanonymous_protocol\fR: \fIlogin_anon | sasl_anon | both\fR .RS 4 Define what anonymous protocol will be used: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIlogin_anon\fR means that the anonymous login method will be used\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIsasl_anon\fR means that the SASL Anonymous method will be used\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIboth\fR means that SASL Anonymous and login anonymous are both enabled\&. .RE .RE .sp The default value is \fIsasl_anon\fR\&. .PP \fBapi_permissions\fR: \fI[Permission, \&.\&.\&.]\fR .RS 4 Define the permissions for API access\&. Please consult the ejabberd Docs web → For Developers → ejabberd ReST API → API Permissions\&. .RE .PP \fBappend_host_config\fR: \fI{Host: Options}\fR .RS 4 To define specific ejabberd modules in a virtual host, you can define the global \fImodules\fR option with the common modules, and later add specific modules to certain virtual hosts\&. To accomplish that, \fIappend_host_config\fR option can be used\&. .RE .PP \fBauth_cache_life_time\fR: \fItimeout()\fR .RS 4 Same as \fIcache_life_time\fR, but applied to authentication cache only\&. If not set, the value from \fIcache_life_time\fR will be used\&. .RE .PP \fBauth_cache_missed\fR: \fItrue | false\fR .RS 4 Same as \fIcache_missed\fR, but applied to authentication cache only\&. If not set, the value from \fIcache_missed\fR will be used\&. .RE .PP \fBauth_cache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as \fIcache_size\fR, but applied to authentication cache only\&. If not set, the value from \fIcache_size\fR will be used\&. .RE .PP \fBauth_method\fR: \fI[mnesia | sql | anonymous | external | jwt | ldap | pam, \&.\&.\&.]\fR .RS 4 A list of authentication methods to use\&. If several methods are defined, authentication is considered successful as long as authentication of at least one of the methods succeeds\&. The default value is \fI[mnesia]\fR\&. .RE .PP \fBauth_opts\fR: \fI[Option, \&.\&.\&.]\fR .RS 4 This is used by the contributed module \fIejabberd_auth_http\fR that can be installed from the ejabberd\-contrib Git repository\&. Please refer to that module\(cqs README file for details\&. .RE .sp \fINote\fR about the next option: improved in 20\&.01: .PP \fBauth_password_format\fR: \fIplain | scram\fR .RS 4 The option defines in what format the users passwords are stored: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIplain\fR: The password is stored as plain text in the database\&. This is risky because the passwords can be read if your database gets compromised\&. This is the default value\&. This format allows clients to authenticate using: the old Jabber Non\-SASL (XEP\-0078), SASL PLAIN, SASL DIGEST\-MD5, and SASL SCRAM\-SHA\-1\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIscram\fR: The password is not stored, only some information that allows to verify the hash provided by the client\&. It is impossible to obtain the original plain password from the stored information; for this reason, when this value is configured it cannot be changed to plain anymore\&. This format allows clients to authenticate using: SASL PLAIN and SASL SCRAM\-SHA\-1\&. The default value is \fIplain\fR\&. .RE .RE .PP \fBauth_scram_hash\fR: \fIsha | sha256 | sha512\fR .RS 4 Hash algorith that should be used to store password in SCRAM format\&. You shouldn\(cqt change this if you already have passwords generated with a different algorithm \- users that have such passwords will not be able to authenticate\&. The default value is \fIsha\fR\&. .RE .PP \fBauth_use_cache\fR: \fItrue | false\fR .RS 4 Same as \fIuse_cache\fR, but applied to authentication cache only\&. If not set, the value from \fIuse_cache\fR will be used\&. .RE .PP \fBc2s_cafile\fR: \fIPath\fR .RS 4 Full path to a file containing one or more CA certificates in PEM format\&. All client certificates should be signed by one of these root CA certificates and should contain the corresponding JID(s) in \fIsubjectAltName\fR field\&. There is no default value\&. .RE .sp You can use host_config to specify this option per\-vhost\&. .sp To set a specific file per listener, use the listener\(cqs cafile option\&. Please notice that \fIc2s_cafile\fR overrides the listener\(cqs \fIcafile\fR option\&. .PP \fBc2s_ciphers\fR: \fI[Cipher, \&.\&.\&.]\fR .RS 4 A list of OpenSSL ciphers to use for c2s connections\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf c2s_ciphers: \- HIGH \- "!aNULL" \- "!eNULL" \- "!3DES" \- "@STRENGTH" .fi .if n \{\ .RE .\} .RE .PP \fBc2s_dhfile\fR: \fIPath\fR .RS 4 Full path to a file containing custom DH parameters to use for c2s connections\&. Such a file could be created with the command "openssl dhparam \-out dh\&.pem 2048"\&. If this option is not specified, 2048\-bit MODP Group with 256\-bit Prime Order Subgroup will be used as defined in RFC5114 Section 2\&.3\&. .RE .PP \fBc2s_protocol_options\fR: \fI[Option, \&.\&.\&.]\fR .RS 4 List of general SSL options to use for c2s connections\&. These map to OpenSSL\(cqs \fIset_options()\fR\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf c2s_protocol_options: \- no_sslv3 \- cipher_server_preference \- no_compression .fi .if n \{\ .RE .\} .RE .PP \fBc2s_tls_compression\fR: \fItrue | false\fR .RS 4 Whether to enable or disable TLS compression for c2s connections\&. The default value is \fIfalse\fR\&. .RE .PP \fBca_file\fR: \fIPath\fR .RS 4 Path to a file of CA root certificates\&. The default is to use system defined file if possible\&. .RE .sp For server conections, this \fIca_file\fR option is overriden by the s2s_cafile option\&. .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 The time of a cached item to keep in cache\&. Once it\(cqs expired, the corresponding item is erased from cache\&. The default value is \fI1 hour\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_cache_life_time\fR, \fIoauth_cache_life_time\fR, \fIrouter_cache_life_time\fR, and \fIsm_cache_life_time\fR\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Whether or not to cache missed lookups\&. When there is an attempt to lookup for a value in a database and this value is not found and the option is set to \fItrue\fR, this attempt will be cached and no attempts will be performed until the cache expires (see \fIcache_life_time\fR)\&. Usually you don\(cqt want to change it\&. Default is \fItrue\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_cache_missed\fR, \fIoauth_cache_missed\fR, \fIrouter_cache_missed\fR, and \fIsm_cache_missed\fR\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 A maximum number of items (not memory!) in cache\&. The rule of thumb, for all tables except rosters, you should set it to the number of maximum online users you expect\&. For roster multiply this number by 20 or so\&. If the cache size reaches this threshold, it\(cqs fully cleared, i\&.e\&. all items are deleted, and the corresponding warning is logged\&. You should avoid frequent cache clearance, because this degrades performance\&. The default value is \fI1000\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_cache_size\fR, \fIoauth_cache_size\fR, \fIrouter_cache_size\fR, and \fIsm_cache_size\fR\&. .RE .PP \fBcaptcha_cmd\fR: \fIPath\fR .RS 4 Full path to a script that generates CAPTCHA images\&. There is no default value: when this option is not set, CAPTCHA functionality is completely disabled\&. .RE .PP \fBcaptcha_host\fR: \fIString\fR .RS 4 Deprecated\&. Use \fIcaptcha_url\fR instead\&. .RE .PP \fBcaptcha_limit\fR: \fIpos_integer() | infinity\fR .RS 4 Maximum number of CAPTCHA generated images per minute for any given JID\&. The option is intended to protect the server from CAPTCHA DoS\&. The default value is \fIinfinity\fR\&. .RE .PP \fBcaptcha_url\fR: \fIURL\fR .RS 4 An URL where CAPTCHA requests should be sent\&. NOTE: you need to configure \fIrequest_handlers\fR for \fIejabberd_http\fR listener as well\&. There is no default value\&. .RE .PP \fBcertfiles\fR: \fI[Path, \&.\&.\&.]\fR .RS 4 The option accepts a list of file paths (optionally with wildcards) containing either PEM certificates or PEM private keys\&. At startup or configuration reload, ejabberd reads all certificates from these files, sorts them, removes duplicates, finds matching private keys and then rebuilds full certificate chains for the use in TLS connections\&. Use this option when TLS is enabled in either of ejabberd listeners: \fIejabberd_c2s\fR, \fIejabberd_http\fR and so on\&. NOTE: if you modify the certificate files or change the value of the option, run \fIejabberdctl reload\-config\fR in order to rebuild and reload the certificate chains\&. .sp If you use Let\(cqs Encrypt certificates for your domain "domain\&.tld", the configuration will look like this: .sp .if n \{\ .RS 4 .\} .nf certfiles: \- /etc/letsencrypt/live/domain\&.tld/fullchain\&.pem \- /etc/letsencrypt/live/domain\&.tld/privkey\&.pem .fi .if n \{\ .RE .\} .RE .PP \fBcluster_backend\fR: \fIBackend\fR .RS 4 A database backend to use for storing information about cluster\&. The only available value so far is \fImnesia\fR\&. .RE .PP \fBcluster_nodes\fR: \fI[Node, \&.\&.\&.]\fR .RS 4 A list of Erlang nodes to connect on ejabberd startup\&. This option is mostly intended for ejabberd customization and sophisticated setups\&. The default value is an empty list\&. .RE .PP \fBdefault_db\fR: \fImnesia | sql\fR .RS 4 Default persistent storage for ejabberd\&. Modules and other components (e\&.g\&. authentication) may have its own value\&. The default value is \fImnesia\fR\&. .RE .PP \fBdefault_ram_db\fR: \fImnesia | redis | sql\fR .RS 4 Default volatile (in\-memory) storage for ejabberd\&. Modules and other components (e\&.g\&. session management) may have its own value\&. The default value is \fImnesia\fR\&. .RE .PP \fBdefine_macro\fR: \fI{MacroName: MacroValue}\fR .RS 4 Defines a macro\&. The value can be any valid arbitrary YAML value\&. For convenience, it\(cqs recommended to define a \fIMacroName\fR in capital letters\&. Duplicated macros are not allowed\&. Macros are processed after additional configuration files have been included, so it is possible to use macros that are defined in configuration files included before the usage\&. It is possible to use a \fIMacroValue\fR in the definition of another macro\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf define_macro: DEBUG: debug LOG_LEVEL: DEBUG USERBOB: user: bob@localhost loglevel: LOG_LEVEL acl: admin: USERBOB .fi .if n \{\ .RE .\} .RE .PP \fBdisable_sasl_mechanisms\fR: \fI[Mechanism, \&.\&.\&.]\fR .RS 4 Specify a list of SASL mechanisms (such as \fIDIGEST\-MD5\fR or \fISCRAM\-SHA1\fR) that should not be offered to the client\&. For convenience, the value of \fIMechanism\fR is case\-insensitive\&. The default value is an empty list, i\&.e\&. no mechanisms are disabled by default\&. .RE .PP \fBdomain_balancing\fR: \fI{Domain: Options}\fR .RS 4 An algorithm to load balance the components that are plugged on an ejabberd cluster\&. It means that you can plug one or several instances of the same component on each ejabberd node and that the traffic will be automatically distributed\&. The algorithm to deliver messages to the component(s) can be specified by this option\&. For any component connected as \fIDomain\fR, available \fIOptions\fR are: .PP \fBcomponent_number\fR: \fI2\&.\&.1000\fR .RS 4 The number of components to balance\&. .RE .PP \fBtype\fR: \fIrandom | source | destination | bare_source | bare_destination\fR .RS 4 How to deliver stanzas to connected components: \fIrandom\fR \- an instance is chosen at random; \fIdestination\fR \- an instance is chosen by the full JID of the packet\(cqs \fIto\fR attribute; \fIsource\fR \- by the full JID of the packet\(cqs \fIfrom\fR attribute; \fIbare_destination\fR \- by the the bare JID (without resource) of the packet\(cqs \fIto\fR attribute; \fIbare_source\fR \- by the bare JID (without resource) of the packet\(cqs \fIfrom\fR attribute is used\&. The default value is \fIrandom\fR\&. .RE .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf domain_balancing: component\&.domain\&.tld: type: destination component_number: 5 transport\&.example\&.org: type: bare_source .fi .if n \{\ .RE .\} .RE .PP \fBext_api_headers\fR: \fIHeaders\fR .RS 4 String of headers (separated with commas \fI,\fR) that will be provided by ejabberd when sending ReST requests\&. The default value is an empty string of headers: \fI""\fR\&. .RE .PP \fBext_api_http_pool_size\fR: \fIpos_integer()\fR .RS 4 Define the size of the HTTP pool, that is, the maximum number of sessions that the ejabberd ReST service will handle simultaneously\&. The default value is: \fI100\fR\&. .RE .PP \fBext_api_path_oauth\fR: \fIPath\fR .RS 4 Define the base URI path when performing OAUTH ReST requests\&. The default value is: \fI"/oauth"\fR\&. .RE .PP \fBext_api_url\fR: \fIURL\fR .RS 4 Define the base URI when performing ReST requests\&. The default value is: \fI"http://localhost/api"\fR\&. .RE .PP \fBextauth_pool_name\fR: \fIName\fR .RS 4 Define the pool name appendix, so the full pool name will be \fIextauth_pool_Name\fR\&. The default value is the hostname\&. .RE .PP \fBextauth_pool_size\fR: \fISize\fR .RS 4 The option defines the number of instances of the same external program to start for better load balancing\&. The default is the number of available CPU cores\&. .RE .PP \fBextauth_program\fR: \fIPath\fR .RS 4 Indicate in this option the full path to the external authentication script\&. The script must be executable by ejabberd\&. .RE .PP \fBfqdn\fR: \fIDomain\fR .RS 4 A fully qualified domain name that will be used in SASL DIGEST\-MD5 authentication\&. The default is detected automatically\&. .RE .PP \fBhide_sensitive_log_data\fR: \fItrue | false\fR .RS 4 A privacy option to not log sensitive data (mostly IP addresses)\&. The default value is \fIfalse\fR for backward compatibility\&. .RE .PP \fBhost_config\fR: \fI{Host: Options}\fR .RS 4 The option is used to redefine \fIOptions\fR for virtual host \fIHost\fR\&. In the example below LDAP authentication method will be used on virtual host \fIdomain\&.tld\fR and SQL method will be used on virtual host \fIexample\&.org\fR\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf hosts: \- domain\&.tld \- example\&.org auth_method: \- sql host_config: domain\&.tld: auth_method: \- ldap .fi .if n \{\ .RE .\} .RE .PP \fBhosts\fR: \fI[Domain1, Domain2, \&.\&.\&.]\fR .RS 4 The option defines a list containing one or more domains that \fIejabberd\fR will serve\&. This is a \fBmandatory\fR option\&. .RE .PP \fBinclude_config_file\fR: \fI[Filename, \&.\&.\&.] | {Filename: Options}\fR .RS 4 Read additional configuration from \fIFilename\fR\&. If the value is provided in \fI{Filename: Options}\fR format, the \fIOptions\fR must be one of the following: .PP \fBallow_only\fR: \fI[OptionName, \&.\&.\&.]\fR .RS 4 Allows only the usage of those options in the included file \fIFilename\fR\&. The options that do not match this criteria are not accepted\&. The default value is to include all options\&. .RE .PP \fBdisallow\fR: \fI[OptionName, \&.\&.\&.]\fR .RS 4 Disallows the usage of those options in the included file \fIFilename\fR\&. The options that match this criteria are not accepted\&. The default value is an empty list\&. .RE .RE .PP \fBjwt_auth_only_rule\fR: \fIAccessName\fR .RS 4 This ACL rule defines accounts that can use only this auth method, even if others are also defined in the ejabberd configuration file\&. In other words: if there are several auth methods enabled for this host (JWT, SQL, \&...), users that match this rule can only use JWT\&. The default value is \fInone\fR\&. .RE .PP \fBjwt_jid_field\fR: \fIFieldName\fR .RS 4 By default, the JID is defined in the \fI"jid"\fR JWT field\&. This option allows to specify other JWT field name where the JID is defined\&. .RE .PP \fBjwt_key\fR: \fIFilePath\fR .RS 4 Path to the file that contains the JWK Key\&. The default value is \fIundefined\fR\&. .RE .PP \fBlanguage\fR: \fILanguage\fR .RS 4 The option defines the default language of server strings that can be seen by XMPP clients\&. If an XMPP client does not possess \fIxml:lang\fR attribute, the specified language is used\&. The default value is \fI"en"\fR\&. .RE .PP \fBldap_backups\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 A list of IP addresses or DNS names of LDAP backup servers\&. When no servers listed in \fIldap_servers\fR option are reachable, ejabberd will try to connect to these backup servers\&. The default is an empty list, i\&.e\&. no backup servers specified\&. WARNING: ejabberd doesn\(cqt try to reconnect back to the main servers when they become operational again, so the only way to restore these connections is to restart ejabberd\&. This limitation might be fixed in future releases\&. .RE .PP \fBldap_base\fR: \fIBase\fR .RS 4 LDAP base directory which stores users accounts\&. There is no default value: you must set the option in order for LDAP connections to work properly\&. .RE .PP \fBldap_deref_aliases\fR: \fInever | always | finding | searching\fR .RS 4 Whether to dereference aliases or not\&. The default value is \fInever\fR\&. .RE .PP \fBldap_dn_filter\fR: \fI{Filter: FilterAttrs}\fR .RS 4 This filter is applied on the results returned by the main filter\&. The filter performs an additional LDAP lookup to make the complete result\&. This is useful when you are unable to define all filter rules in \fIldap_filter\fR\&. You can define "%u", "%d", "%s" and "%D" pattern variables in \fIFilter\fR: "%u" is replaced by a user\(cqs part of the JID, "%d" is replaced by the corresponding domain (virtual host), all "%s" variables are consecutively replaced by values from the attributes in \fIFilterAttrs\fR and "%D" is replaced by Distinguished Name from the result set\&. There is no default value, which means the result is not filtered\&. WARNING: Since this filter makes additional LDAP lookups, use it only as the last resort: try to define all filter rules in \fIldap_filter\fR option if possible\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf ldap_dn_filter: "(&(name=%s)(owner=%D)(user=%u@%d))": [sn] .fi .if n \{\ .RE .\} .RE .PP \fBldap_encrypt\fR: \fItls | none\fR .RS 4 Whether to encrypt LDAP connection using TLS or not\&. The default value is \fInone\fR\&. NOTE: STARTTLS encryption is not supported\&. .RE .PP \fBldap_filter\fR: \fIFilter\fR .RS 4 An LDAP filter as defined in RFC4515\&. There is no default value\&. Example: "(&(objectClass=shadowAccount)(memberOf=XMPP Users))"\&. NOTE: don\(cqt forget to close brackets and don\(cqt use superfluous whitespaces\&. Also you must not use "uid" attribute in the filter because this attribute will be appended to the filter automatically\&. .RE .PP \fBldap_password\fR: \fIPassword\fR .RS 4 Bind password\&. The default value is an empty string\&. .RE .PP \fBldap_port\fR: \fI1\&.\&.65535\fR .RS 4 Port to connect to your LDAP server\&. The default port is \fI389\fR if encryption is disabled and \fI636\fR if encryption is enabled\&. .RE .PP \fBldap_rootdn\fR: \fIRootDN\fR .RS 4 Bind Distinguished Name\&. The default value is an empty string, which means "anonymous connection"\&. .RE .PP \fBldap_servers\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 A list of IP addresses or DNS names of your LDAP servers\&. The default value is \fI[localhost]\fR\&. .RE .PP \fBldap_tls_cacertfile\fR: \fIPath\fR .RS 4 A path to a file containing PEM encoded CA certificates\&. This option is required when TLS verification is enabled\&. .RE .PP \fBldap_tls_certfile\fR: \fIPath\fR .RS 4 A path to a file containing PEM encoded certificate along with PEM encoded private key\&. This certificate will be provided by ejabberd when TLS enabled for LDAP connections\&. There is no default value, which means no client certificate will be sent\&. .RE .PP \fBldap_tls_depth\fR: \fINumber\fR .RS 4 Specifies the maximum verification depth when TLS verification is enabled, i\&.e\&. how far in a chain of certificates the verification process can proceed before the verification is considered to be failed\&. Peer certificate = 0, CA certificate = 1, higher level CA certificate = 2, etc\&. The value \fI2\fR thus means that a chain can at most contain peer cert, CA cert, next CA cert, and an additional CA cert\&. The default value is \fI1\fR\&. .RE .PP \fBldap_tls_verify\fR: \fIfalse | soft | hard\fR .RS 4 This option specifies whether to verify LDAP server certificate or not when TLS is enabled\&. When \fIhard\fR is set, ejabberd doesn\(cqt proceed if the certificate is invalid\&. When \fIsoft\fR is set, ejabberd proceeds even if the check has failed\&. The default is \fIfalse\fR, which means no checks are performed\&. .RE .PP \fBldap_uids\fR: \fI[Attr] | {Attr: AttrFormat}\fR .RS 4 LDAP attributes which hold a list of attributes to use as alternatives for getting the JID, where \fIAttr\fR is an LDAP attribute which holds the user\(cqs part of the JID and \fIAttrFormat\fR must contain one and only one pattern variable "%u" which will be replaced by the user\(cqs part of the JID\&. For example, "%u@example\&.org"\&. If the value is in the form of \fI[Attr]\fR then \fIAttrFormat\fR is assumed to be "%u"\&. .RE .PP \fBlisten\fR: \fI[Options, \&.\&.\&.]\fR .RS 4 The option for listeners configuration\&. See the Listen Modules section for details\&. .RE .PP \fBlog_rotate_count\fR: \fINumber\fR .RS 4 The number of rotated log files to keep\&. The default value is \fI1\fR, which means that only keeps ejabberd\&.log\&.0, error\&.log\&.0 and crash\&.log\&.0\&. .RE .PP \fBlog_rotate_size\fR: \fIpos_integer() | infinity\fR .RS 4 The size (in bytes) of a log file to trigger rotation\&. If set to \fIinfinity\fR, log rotation is disabled\&. The default value is \fI10485760\fR (that is, 10 Mb)\&. .RE .PP \fBloglevel\fR: \fInone | emergency | alert | critical | error | warning | notice | info | debug\fR .RS 4 Verbosity of log files generated by ejabberd\&. The default value is \fIinfo\fR\&. NOTE: previous versions of ejabberd had log levels defined in numeric format (\fI0\&.\&.5\fR)\&. The numeric values are still accepted for backward compatibility, but are not recommended\&. .RE .PP \fBmax_fsm_queue\fR: \fISize\fR .RS 4 This option specifies the maximum number of elements in the queue of the FSM (Finite State Machine)\&. Roughly speaking, each message in such queues represents one XML stanza queued to be sent into its relevant outgoing stream\&. If queue size reaches the limit (because, for example, the receiver of stanzas is too slow), the FSM and the corresponding connection (if any) will be terminated and error message will be logged\&. The reasonable value for this option depends on your hardware configuration\&. The allowed values are positive integers\&. The default value is \fI10000\fR\&. .RE .PP \fBmodules\fR: \fI{Module: Options}\fR .RS 4 The option for modules configuration\&. See Modules section for details\&. .RE .PP \fBnegotiation_timeout\fR: \fItimeout()\fR .RS 4 Time to wait for an XMPP stream negotiation to complete\&. When timeout occurs, the corresponding XMPP stream is closed\&. The default value is \fI30\fR seconds\&. .RE .PP \fBnet_ticktime\fR: \fItimeout()\fR .RS 4 This option can be used to tune tick time parameter of \fInet_kernel\fR\&. It tells Erlang VM how often nodes should check if intra\-node communication was not interrupted\&. This option must have identical value on all nodes, or it will lead to subtle bugs\&. Usually leaving default value of this is option is best, tweak it only if you know what you are doing\&. The default value is \fI1 minute\fR\&. .RE .PP \fBnew_sql_schema\fR: \fItrue | false\fR .RS 4 Whether to use \fInew\fR SQL schema\&. All schemas are located at https://github\&.com/processone/ejabberd/tree/21\&.12/sql\&. There are two schemas available\&. The default legacy schema allows to store one XMPP domain into one ejabberd database\&. The \fInew\fR schema allows to handle several XMPP domains in a single ejabberd database\&. Using this \fInew\fR schema is best when serving several XMPP domains and/or changing domains from time to time\&. This avoid need to manage several databases and handle complex configuration changes\&. The default depends on configuration flag \fI\-\-enable\-new\-sql\-schema\fR which is set at compile time\&. .RE .PP \fBoauth_access\fR: \fIAccessName\fR .RS 4 By default creating OAuth tokens is not allowed\&. To define which users can create OAuth tokens, you can refer to an ejabberd access rule in the \fIoauth_access\fR option\&. Use \fIall\fR to allow everyone to create tokens\&. .RE .PP \fBoauth_cache_life_time\fR: \fItimeout()\fR .RS 4 Same as \fIcache_life_time\fR, but applied to OAuth cache only\&. If not set, the value from \fIcache_life_time\fR will be used\&. .RE .PP \fBoauth_cache_missed\fR: \fItrue | false\fR .RS 4 Same as \fIcache_missed\fR, but applied to OAuth cache only\&. If not set, the value from \fIcache_missed\fR will be used\&. .RE .sp \fINote\fR about the next option: added in 21\&.01: .PP \fBoauth_cache_rest_failure_life_time\fR: \fItimeout()\fR .RS 4 The time that a failure in OAuth ReST is cached\&. The default value is \fIinfinity\fR\&. .RE .PP \fBoauth_cache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as \fIcache_size\fR, but applied to OAuth cache only\&. If not set, the value from \fIcache_size\fR will be used\&. .RE .PP \fBoauth_client_id_check\fR: \fIallow | db | deny\fR .RS 4 Define whether the client authentication is always allowed, denied, or it will depend if the client ID is present in the database\&. The default value is \fIallow\fR\&. .RE .PP \fBoauth_db_type\fR: \fImnesia | sql\fR .RS 4 Database backend to use for OAuth authentication\&. The default value is picked from \fIdefault_db\fR option, or if it\(cqs not set, \fImnesia\fR will be used\&. .RE .PP \fBoauth_expire\fR: \fItimeout()\fR .RS 4 Time during which the OAuth token is valid, in seconds\&. After that amount of time, the token expires and the delegated credential cannot be used and is removed from the database\&. The default is \fI4294967\fR seconds\&. .RE .PP \fBoauth_use_cache\fR: \fItrue | false\fR .RS 4 Same as \fIuse_cache\fR, but applied to OAuth cache only\&. If not set, the value from \fIuse_cache\fR will be used\&. .RE .PP \fBoom_killer\fR: \fItrue | false\fR .RS 4 Enable or disable OOM (out\-of\-memory) killer\&. When system memory raises above the limit defined in \fIoom_watermark\fR option, ejabberd triggers OOM killer to terminate most memory consuming Erlang processes\&. Note that in order to maintain functionality, ejabberd only attempts to kill transient processes, such as those managing client sessions, s2s or database connections\&. The default value is \fItrue\fR\&. .RE .PP \fBoom_queue\fR: \fISize\fR .RS 4 Trigger OOM killer when some of the running Erlang processes have messages queue above this \fISize\fR\&. Note that such processes won\(cqt be killed if \fIoom_killer\fR option is set to \fIfalse\fR or if \fIoom_watermark\fR is not reached yet\&. .RE .PP \fBoom_watermark\fR: \fIPercent\fR .RS 4 A percent of total system memory consumed at which OOM killer should be activated with some of the processes possibly be killed (see \fIoom_killer\fR option)\&. Later, when memory drops below this \fIPercent\fR, OOM killer is deactivated\&. The default value is \fI80\fR percents\&. .RE .PP \fBoutgoing_s2s_families\fR: \fI[ipv4 | ipv6, \&.\&.\&.]\fR .RS 4 Specify which address families to try, in what order\&. The default is \fI[ipv4, ipv6]\fR which means it first tries connecting with IPv4, if that fails it tries using IPv6\&. .RE .sp \fINote\fR about the next option: added in 20\&.12: .PP \fBoutgoing_s2s_ipv4_address\fR: \fIAddress\fR .RS 4 Specify the IPv4 address that will be used when establishing an outgoing S2S IPv4 connection, for example "127\&.0\&.0\&.1"\&. The default value is \fIundefined\fR\&. .RE .sp \fINote\fR about the next option: added in 20\&.12: .PP \fBoutgoing_s2s_ipv6_address\fR: \fIAddress\fR .RS 4 Specify the IPv6 address that will be used when establishing an outgoing S2S IPv6 connection, for example "::FFFF:127\&.0\&.0\&.1"\&. The default value is \fIundefined\fR\&. .RE .PP \fBoutgoing_s2s_port\fR: \fI1\&.\&.65535\fR .RS 4 A port number to use for outgoing s2s connections when the target server doesn\(cqt have an SRV record\&. The default value is \fI5269\fR\&. .RE .PP \fBoutgoing_s2s_timeout\fR: \fItimeout()\fR .RS 4 The timeout in seconds for outgoing S2S connection attempts\&. The default value is \fI10\fR seconds\&. .RE .PP \fBpam_service\fR: \fIName\fR .RS 4 This option defines the PAM service name\&. Refer to the PAM documentation of your operation system for more information\&. The default value is \fIejabberd\fR\&. .RE .PP \fBpam_userinfotype\fR: \fIusername | jid\fR .RS 4 This option defines what type of information about the user ejabberd provides to the PAM service: only the username, or the user\(cqs JID\&. Default is \fIusername\fR\&. .RE .PP \fBpgsql_users_number_estimate\fR: \fItrue | false\fR .RS 4 Whether to use PostgreSQL estimation when counting registered users\&. The default value is \fIfalse\fR\&. .RE .PP \fBqueue_dir\fR: \fIDirectory\fR .RS 4 If \fIqueue_type\fR option is set to \fIfile\fR, use this \fIDirectory\fR to store file queues\&. The default is to keep queues inside Mnesia directory\&. .RE .PP \fBqueue_type\fR: \fIram | file\fR .RS 4 Default type of queues in ejabberd\&. Modules may have its own value of the option\&. The value of \fIram\fR means that queues will be kept in memory\&. If value \fIfile\fR is set, you may also specify directory in \fIqueue_dir\fR option where file queues will be placed\&. The default value is \fIram\fR\&. .RE .PP \fBredis_connect_timeout\fR: \fItimeout()\fR .RS 4 A timeout to wait for the connection to be re\-established to the Redis server\&. The default is \fI1 second\fR\&. .RE .PP \fBredis_db\fR: \fINumber\fR .RS 4 Redis database number\&. The default is \fI0\fR\&. .RE .PP \fBredis_password\fR: \fIPassword\fR .RS 4 The password to the Redis server\&. The default is an empty string, i\&.e\&. no password\&. .RE .PP \fBredis_pool_size\fR: \fINumber\fR .RS 4 The number of simultaneous connections to the Redis server\&. The default value is \fI10\fR\&. .RE .PP \fBredis_port\fR: \fI1\&.\&.65535\fR .RS 4 The port where the Redis server is accepting connections\&. The default is \fI6379\fR\&. .RE .PP \fBredis_queue_type\fR: \fIram | file\fR .RS 4 The type of request queue for the Redis server\&. See description of \fIqueue_type\fR option for the explanation\&. The default value is the value defined in \fIqueue_type\fR or \fIram\fR if the latter is not set\&. .RE .PP \fBredis_server\fR: \fIHostname\fR .RS 4 A hostname or an IP address of the Redis server\&. The default is \fIlocalhost\fR\&. .RE .PP \fBregistration_timeout\fR: \fItimeout()\fR .RS 4 This is a global option for module \fImod_register\fR\&. It limits the frequency of registrations from a given IP or username\&. So, a user that tries to register a new account from the same IP address or JID during this time after their previous registration will receive an error with the corresponding explanation\&. To disable this limitation, set the value to \fIinfinity\fR\&. The default value is \fI600 seconds\fR\&. .RE .PP \fBresource_conflict\fR: \fIsetresource | closeold | closenew\fR .RS 4 NOTE: this option is deprecated and may be removed anytime in the future versions\&. The possible values match exactly the three possibilities described in XMPP Core: section 7\&.7\&.2\&.2\&. The default value is \fIcloseold\fR\&. If the client uses old Jabber Non\-SASL authentication (XEP\-0078), then this option is not respected, and the action performed is \fIcloseold\fR\&. .RE .PP \fBrouter_cache_life_time\fR: \fItimeout()\fR .RS 4 Same as \fIcache_life_time\fR, but applied to routing table cache only\&. If not set, the value from \fIcache_life_time\fR will be used\&. .RE .PP \fBrouter_cache_missed\fR: \fItrue | false\fR .RS 4 Same as \fIcache_missed\fR, but applied to routing table cache only\&. If not set, the value from \fIcache_missed\fR will be used\&. .RE .PP \fBrouter_cache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as \fIcache_size\fR, but applied to routing table cache only\&. If not set, the value from \fIcache_size\fR will be used\&. .RE .PP \fBrouter_db_type\fR: \fImnesia | redis | sql\fR .RS 4 Database backend to use for routing information\&. The default value is picked from \fIdefault_ram_db\fR option, or if it\(cqs not set, \fImnesia\fR will be used\&. .RE .PP \fBrouter_use_cache\fR: \fItrue | false\fR .RS 4 Same as \fIuse_cache\fR, but applied to routing table cache only\&. If not set, the value from \fIuse_cache\fR will be used\&. .RE .PP \fBrpc_timeout\fR: \fItimeout()\fR .RS 4 A timeout for remote function calls between nodes in an ejabberd cluster\&. You should probably never change this value since those calls are used for internal needs only\&. The default value is \fI5\fR seconds\&. .RE .PP \fBs2s_access\fR: \fIAccess\fR .RS 4 The access rule to restrict server\-to\-server connections\&. The default value is \fIall\fR which means no restrictions are applied\&. .RE .PP \fBs2s_cafile\fR: \fIPath\fR .RS 4 A path to a file with CA root certificates that will be used to authenticate s2s connections\&. If not set, the value of ca_file will be used\&. .RE .sp You can use host_config to specify this option per\-vhost\&. .PP \fBs2s_ciphers\fR: \fI[Cipher, \&.\&.\&.]\fR .RS 4 A list of OpenSSL ciphers to use for s2s connections\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf s2s_ciphers: \- HIGH \- "!aNULL" \- "!eNULL" \- "!3DES" \- "@STRENGTH" .fi .if n \{\ .RE .\} .RE .PP \fBs2s_dhfile\fR: \fIPath\fR .RS 4 Full path to a file containing custom DH parameters to use for s2s connections\&. Such a file could be created with the command "openssl dhparam \-out dh\&.pem 2048"\&. If this option is not specified, 2048\-bit MODP Group with 256\-bit Prime Order Subgroup will be used as defined in RFC5114 Section 2\&.3\&. .RE .PP \fBs2s_dns_retries\fR: \fINumber\fR .RS 4 DNS resolving retries\&. The default value is \fI2\fR\&. .RE .PP \fBs2s_dns_timeout\fR: \fItimeout()\fR .RS 4 The timeout for DNS resolving\&. The default value is \fI10\fR seconds\&. .RE .PP \fBs2s_max_retry_delay\fR: \fItimeout()\fR .RS 4 The maximum allowed delay for s2s connection retry to connect after a failed connection attempt\&. The default value is \fI300\fR seconds (5 minutes)\&. .RE .PP \fBs2s_protocol_options\fR: \fI[Option, \&.\&.\&.]\fR .RS 4 List of general SSL options to use for s2s connections\&. These map to OpenSSL\(cqs \fIset_options()\fR\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf s2s_protocol_options: \- no_sslv3 \- cipher_server_preference \- no_compression .fi .if n \{\ .RE .\} .RE .PP \fBs2s_queue_type\fR: \fIram | file\fR .RS 4 The type of a queue for s2s packets\&. See description of \fIqueue_type\fR option for the explanation\&. The default value is the value defined in \fIqueue_type\fR or \fIram\fR if the latter is not set\&. .RE .PP \fBs2s_timeout\fR: \fItimeout()\fR .RS 4 A time to wait before closing an idle s2s connection\&. The default value is \fI10 minutes\fR\&. .RE .PP \fBs2s_tls_compression\fR: \fItrue | false\fR .RS 4 Whether to enable or disable TLS compression for s2s connections\&. The default value is \fIfalse\fR\&. .RE .PP \fBs2s_use_starttls\fR: \fItrue | false | optional | required\fR .RS 4 Whether to use STARTTLS for s2s connections\&. The value of \fIfalse\fR means STARTTLS is prohibited\&. The value of \fItrue\fR or \fIoptional\fR means STARTTLS is enabled but plain connections are still allowed\&. And the value of \fIrequired\fR means that only STARTTLS connections are allowed\&. The default value is \fIfalse\fR (for historical reasons)\&. .RE .PP \fBs2s_zlib\fR: \fItrue | false\fR .RS 4 Whether to use \fIzlib\fR compression (as defined in XEP\-0138) or not\&. The default value is \fIfalse\fR\&. WARNING: this type of compression is nowadays considered insecure\&. .RE .PP \fBshaper\fR: \fI{ShaperName: Rate}\fR .RS 4 The option defines a set of shapers\&. Every shaper is assigned a name \fIShaperName\fR that can be used in other parts of the configuration file, such as \fIshaper_rules\fR option\&. The shaper itself is defined by its \fIRate\fR, where \fIRate\fR stands for the maximum allowed incoming rate in \fBbytes\fR per second\&. When a connection exceeds this limit, ejabberd stops reading from the socket until the average rate is again below the allowed maximum\&. In the example below shaper \fInormal\fR limits the traffic speed to 1,000 bytes/sec and shaper \fIfast\fR limits the traffic speed to 50,000 bytes/sec: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf shaper: normal: 1000 fast: 50000 .fi .if n \{\ .RE .\} .RE .PP \fBshaper_rules\fR: \fI{ShaperRuleName: {Number|ShaperName: ACLRule|ACLName}}\fR .RS 4 An entry allowing to declaring shaper to use for matching user/hosts\&. Semantics is similar to \fIaccess_rules\fR option, the only difference is that instead using \fIallow\fR or \fIdeny\fR, a name of a shaper (defined in \fIshaper\fR option) or a positive number should be used\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf shaper_rules: connections_limit: 10: user: peter@example\&.com 100: admin 5: all download_speed: fast: admin slow: anonymous_users normal: all log_days: 30 .fi .if n \{\ .RE .\} .RE .PP \fBsm_cache_life_time\fR: \fItimeout()\fR .RS 4 Same as \fIcache_life_time\fR, but applied to client sessions table cache only\&. If not set, the value from \fIcache_life_time\fR will be used\&. .RE .PP \fBsm_cache_missed\fR: \fItrue | false\fR .RS 4 Same as \fIcache_missed\fR, but applied to client sessions table cache only\&. If not set, the value from \fIcache_missed\fR will be used\&. .RE .PP \fBsm_cache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as \fIcache_size\fR, but applied to client sessions table cache only\&. If not set, the value from \fIcache_size\fR will be used\&. .RE .PP \fBsm_db_type\fR: \fImnesia | redis | sql\fR .RS 4 Database backend to use for client sessions information\&. The default value is picked from \fIdefault_ram_db\fR option, or if it\(cqs not set, \fImnesia\fR will be used\&. .RE .PP \fBsm_use_cache\fR: \fItrue | false\fR .RS 4 Same as \fIuse_cache\fR, but applied to client sessions table cache only\&. If not set, the value from \fIuse_cache\fR will be used\&. .RE .PP \fBsql_connect_timeout\fR: \fItimeout()\fR .RS 4 A time to wait for connection to an SQL server to be established\&. The default value is \fI5\fR seconds\&. .RE .PP \fBsql_database\fR: \fIDatabase\fR .RS 4 An SQL database name\&. For SQLite this must be a full path to a database file\&. The default value is \fIejabberd\fR\&. .RE .PP \fBsql_keepalive_interval\fR: \fItimeout()\fR .RS 4 An interval to make a dummy SQL request to keep alive the connections to the database\&. There is no default value, so no keepalive requests are made\&. .RE .sp \fINote\fR about the next option: added in 20\&.12: .PP \fBsql_odbc_driver\fR: \fIPath\fR .RS 4 Path to the ODBC driver to use to connect to a Microsoft SQL Server database\&. This option is only valid if the \fIsql_type\fR option is set to \fImssql\fR\&. The default value is: \fIlibtdsodbc\&.so\fR .RE .PP \fBsql_password\fR: \fIPassword\fR .RS 4 The password for SQL authentication\&. The default is empty string\&. .RE .PP \fBsql_pool_size\fR: \fISize\fR .RS 4 Number of connections to the SQL server that ejabberd will open for each virtual host\&. The default value is 10\&. WARNING: for SQLite this value is \fI1\fR by default and it\(cqs not recommended to change it due to potential race conditions\&. .RE .PP \fBsql_port\fR: \fI1\&.\&.65535\fR .RS 4 The port where the SQL server is accepting connections\&. The default is \fI3306\fR for MySQL, \fI5432\fR for PostgreSQL and \fI1433\fR for MS SQL\&. The option has no effect for SQLite\&. .RE .sp \fINote\fR about the next option: added in 20\&.01: .PP \fBsql_prepared_statements\fR: \fItrue | false\fR .RS 4 This option is \fItrue\fR by default, and is useful to disable prepared statements\&. The option is valid for PostgreSQL\&. .RE .PP \fBsql_query_timeout\fR: \fItimeout()\fR .RS 4 A time to wait for an SQL query response\&. The default value is \fI60\fR seconds\&. .RE .PP \fBsql_queue_type\fR: \fIram | file\fR .RS 4 The type of a request queue for the SQL server\&. See description of \fIqueue_type\fR option for the explanation\&. The default value is the value defined in \fIqueue_type\fR or \fIram\fR if the latter is not set\&. .RE .PP \fBsql_server\fR: \fIHost\fR .RS 4 A hostname or an IP address of the SQL server\&. The default value is \fIlocalhost\fR\&. .RE .sp \fINote\fR about the next option: improved in 20\&.03: .PP \fBsql_ssl\fR: \fItrue | false\fR .RS 4 Whether to use SSL encrypted connections to the SQL server\&. The option is only available for MySQL and PostgreSQL\&. The default value is \fIfalse\fR\&. .RE .PP \fBsql_ssl_cafile\fR: \fIPath\fR .RS 4 A path to a file with CA root certificates that will be used to verify SQL connections\&. Implies \fIsql_ssl\fR and \fIsql_ssl_verify\fR options are set to \fItrue\fR\&. There is no default which means certificate verification is disabled\&. .RE .PP \fBsql_ssl_certfile\fR: \fIPath\fR .RS 4 A path to a certificate file that will be used for SSL connections to the SQL server\&. Implies \fIsql_ssl\fR option is set to \fItrue\fR\&. There is no default which means ejabberd won\(cqt provide a client certificate to the SQL server\&. .RE .PP \fBsql_ssl_verify\fR: \fItrue | false\fR .RS 4 Whether to verify SSL connection to the SQL server against CA root certificates defined in \fIsql_ssl_cafile\fR option\&. Implies \fIsql_ssl\fR option is set to \fItrue\fR\&. The default value is \fIfalse\fR\&. .RE .PP \fBsql_start_interval\fR: \fItimeout()\fR .RS 4 A time to wait before retrying to restore failed SQL connection\&. The default value is \fI30\fR seconds\&. .RE .PP \fBsql_type\fR: \fImssql | mysql | odbc | pgsql | sqlite\fR .RS 4 The type of an SQL connection\&. The default is \fIodbc\fR\&. .RE .PP \fBsql_username\fR: \fIUsername\fR .RS 4 A user name for SQL authentication\&. The default value is \fIejabberd\fR\&. .RE .PP \fBtrusted_proxies\fR: \fIall | [Network1, Network2, \&.\&.\&.]\fR .RS 4 Specify what proxies are trusted when an HTTP request contains the header \fIX\-Forwarded\-For\fR\&. You can specify \fIall\fR to allow all proxies, or specify a list of IPs, possibly with masks\&. The default value is an empty list\&. This allows, if enabled, to be able to know the real IP of the request, for admin purpose, or security configuration (for example using \fImod_fail2ban\fR)\&. IMPORTANT: The proxy MUST be configured to set the \fIX\-Forwarded\-For\fR header if you enable this option as, otherwise, the client can set it itself and as a result the IP value cannot be trusted for security rules in ejabberd\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Enable or disable cache\&. The default is \fItrue\fR\&. Several modules have a similar option; and some core ejabberd parts support similar options too, see \fIauth_use_cache\fR, \fIoauth_use_cache\fR, \fIrouter_use_cache\fR, and \fIsm_use_cache\fR\&. .RE .PP \fBvalidate_stream\fR: \fItrue | false\fR .RS 4 Whether to validate any incoming XML packet according to the schemas of supported XMPP extensions\&. WARNING: the validation is only intended for the use by client developers \- don\(cqt enable it in production environment\&. The default value is \fIfalse\fR\&. .RE .PP \fBversion\fR: \fIstring()\fR .RS 4 The option can be used to set custom ejabberd version, that will be used by different parts of ejabberd, for example by \fImod_version\fR module\&. The default value is obtained at compile time from the underlying version control system\&. .RE .PP \fBwebsocket_origin\fR: \fIignore | URL\fR .RS 4 This option enables validation for \fIOrigin\fR header to protect against connections from other domains than given in the configuration file\&. In this way, the lower layer load balancer can be chosen for a specific ejabberd implementation while still providing a secure Websocket connection\&. The default value is \fIignore\fR\&. An example value of the \fIURL\fR is "https://test\&.example\&.org:8081"\&. .RE .PP \fBwebsocket_ping_interval\fR: \fItimeout()\fR .RS 4 Defines time between pings sent by the server to a client (Websocket level protocol pings are used for this) to keep a connection active\&. If the client doesn\(cqt respond to two consecutive pings, the connection will be assumed as closed\&. The value of \fI0\fR can be used to disable the feature\&. This option makes the server sending pings only for connections using the RFC compliant protocol\&. For older style connections the server expects that whitespace pings would be used for this purpose\&. The default value is \fI60\fR seconds\&. .RE .PP \fBwebsocket_timeout\fR: \fItimeout()\fR .RS 4 Amount of time without any communication after which the connection would be closed\&. The default value is \fI300\fR seconds\&. .RE .SH "MODULES" .sp This section describes options of all ejabberd modules\&. .SS "mod_adhoc" .sp This module implements XEP\-0050: Ad\-Hoc Commands\&. It\(cqs an auxiliary module and is only needed by some of the other modules\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBreport_commands_node\fR: \fItrue | false\fR .RS 4 Provide the Commands item in the Service Discovery\&. Default value: \fIfalse\fR\&. .RE .RE .SS "mod_admin_extra" .sp This module provides additional administrative commands\&. .sp Details for some commands: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIban\-acount\fR: This command kicks all the connected sessions of the account from the server\&. It also changes their password to a randomly generated one, so they can\(cqt login anymore unless a server administrator changes their password again\&. It is possible to define the reason of the ban\&. The new password also includes the reason and the date and time of the ban\&. See an example below\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIpushroster\fR: (and \fIpushroster\-all\fR) The roster file must be placed, if using Windows, on the directory where you installed ejabberd: C:/Program Files/ejabberd or similar\&. If you use other Operating System, place the file on the same directory where the \&.beam files are installed\&. See below an example roster file\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIsrg\-create\fR: If you want to put a group Name with blankspaces, use the characters "\*(Aq and \*(Aq" to define when the Name starts and ends\&. See an example below\&. .RE .sp The module has no options\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp With this configuration, vCards can only be modified with mod_admin_extra commands: .sp .if n \{\ .RS 4 .\} .nf acl: adminextraresource: \- resource: "modadminextraf8x,31ad" access_rules: vcard_set: \- allow: adminextraresource modules: mod_admin_extra: {} mod_vcard: access_set: vcard_set .fi .if n \{\ .RE .\} .sp Content of roster file for \fIpushroster\fR command: .sp .if n \{\ .RS 4 .\} .nf [{<<"bob">>, <<"example\&.org">>, <<"workers">>, <<"Bob">>}, {<<"mart">>, <<"example\&.org">>, <<"workers">>, <<"Mart">>}, {<<"Rich">>, <<"example\&.org">>, <<"bosses">>, <<"Rich">>}]\&. .fi .if n \{\ .RE .\} .sp With this call, the sessions of the local account which JID is boby@example\&.org will be kicked, and its password will be set to something like \fIBANNED_ACCOUNT\(em20080425T21:45:07\(em2176635\(emSpammed_rooms\fR .sp .if n \{\ .RS 4 .\} .nf ejabberdctl vhost example\&.org ban\-account boby "Spammed rooms" .fi .if n \{\ .RE .\} .sp Call to srg\-create using double\-quotes and single\-quotes: .sp .if n \{\ .RS 4 .\} .nf ejabberdctl srg\-create g1 example\&.org "\*(AqGroup number 1\*(Aq" this_is_g1 g1 .fi .if n \{\ .RE .\} .RE .SS "mod_admin_update_sql" .sp This module can be used to update existing SQL database from the default to the new schema\&. Check the section Default and New Schemas for details\&. Please note that only PostgreSQL is supported\&. When the module is loaded use \fIupdate_sql\fR API\&. .sp The module has no options\&. .SS "mod_announce" .sp This module enables configured users to broadcast announcements and to set the message of the day (MOTD)\&. Configured users can perform these actions with an XMPP client either using Ad\-hoc Commands or sending messages to specific JIDs\&. .sp Note that this module can be resource intensive on large deployments as it may broadcast a lot of messages\&. This module should be disabled for instances of ejabberd with hundreds of thousands users\&. .sp The Ad\-hoc Commands are listed in the Server Discovery\&. For this feature to work, \fImod_adhoc\fR must be enabled\&. .sp The specific JIDs where messages can be sent are listed below\&. The first JID in each entry will apply only to the specified virtual host example\&.org, while the JID between brackets will apply to all virtual hosts in ejabberd: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} example\&.org/announce/all (example\&.org/announce/all\-hosts/all):: The message is sent to all registered users\&. If the user is online and connected to several resources, only the resource with the highest priority will receive the message\&. If the registered user is not connected, the message will be stored offline in assumption that offline storage (see \fImod_offline\fR) is enabled\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} example\&.org/announce/online (example\&.org/announce/all\-hosts/online):: The message is sent to all connected users\&. If the user is online and connected to several resources, all resources will receive the message\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} example\&.org/announce/motd (example\&.org/announce/all\-hosts/motd):: The message is set as the message of the day (MOTD) and is sent to users when they login\&. In addition the message is sent to all connected users (similar to announce/online)\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} example\&.org/announce/motd/update (example\&.org/announce/all\-hosts/motd/update):: The message is set as message of the day (MOTD) and is sent to users when they login\&. The message is not sent to any currently connected user\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} example\&.org/announce/motd/delete (example\&.org/announce/all\-hosts/motd/delete):: Any message sent to this JID removes the existing message of the day (MOTD)\&. .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 This option specifies who is allowed to send announcements and to set the message of the day\&. The default value is \fInone\fR (i\&.e\&. nobody is able to send such messages)\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_avatar" .sp The purpose of the module is to cope with legacy and modern XMPP clients posting avatars\&. The process is described in XEP\-0398: User Avatar to vCard\-Based Avatars Conversion\&. .sp Also, the module supports conversion between avatar image formats on the fly\&. .sp The module depends on \fImod_vcard\fR, \fImod_vcard_xupdate\fR and \fImod_pubsub\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBconvert\fR: \fI{From: To}\fR .RS 4 Defines image convertion rules: the format in \fIFrom\fR will be converted to format in \fITo\fR\&. The value of \fIFrom\fR can also be \fIdefault\fR, which is match\-all rule\&. NOTE: the list of supported formats is detected at compile time depending on the image libraries installed in the system\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf convert: webp: jpg default: png .fi .if n \{\ .RE .\} .RE .PP \fBrate_limit\fR: \fINumber\fR .RS 4 Limit any given JID by the number of avatars it is able to convert per minute\&. This is to protect the server from image convertion DoS\&. The default value is \fI10\fR\&. .RE .RE .SS "mod_block_strangers" .sp This module allows to block/log messages coming from an unknown entity\&. If a writing entity is not in your roster, you can let this module drop and/or log the message\&. By default you\(cqll just not receive message from that entity\&. Enable this module if you want to drop SPAM messages\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 The option is supposed to be used when \fIallow_local_users\fR and \fIallow_transports\fR are not enough\&. It\(cqs an ACL where \fIdeny\fR means the message will be rejected (or a CAPTCHA would be generated for a presence, if configured), and \fIallow\fR means the sender is whitelisted and the stanza will pass through\&. The default value is \fInone\fR, which means nothing is whitelisted\&. .RE .PP \fBallow_local_users\fR: \fItrue | false\fR .RS 4 This option specifies if strangers from the same local host should be accepted or not\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_transports\fR: \fItrue | false\fR .RS 4 If set to \fItrue\fR and some server\(cqs JID is in user\(cqs roster, then messages from any user of this server are accepted even if no subscription present\&. The default value is \fItrue\fR\&. .RE .PP \fBcaptcha\fR: \fItrue | false\fR .RS 4 Whether to generate CAPTCHA or not in response to messages from strangers\&. See also section CAPTCHA of the Configuration Guide\&. The default value is \fIfalse\fR\&. .RE .PP \fBdrop\fR: \fItrue | false\fR .RS 4 This option specifies if strangers messages should be dropped or not\&. The default value is \fItrue\fR\&. .RE .PP \fBlog\fR: \fItrue | false\fR .RS 4 This option specifies if strangers\*(Aq messages should be logged (as info message) in ejabberd\&.log\&. The default value is \fIfalse\fR\&. .RE .RE .SS "mod_blocking" .sp The module implements XEP\-0191: Blocking Command\&. .sp This module depends on \fImod_privacy\fR where all the configuration is performed\&. .sp The module has no options\&. .SS "mod_bosh" .sp This module implements XMPP over BOSH as defined in XEP\-0124 and XEP\-0206\&. BOSH stands for Bidirectional\-streams Over Synchronous HTTP\&. It makes it possible to simulate long lived connections required by XMPP over the HTTP protocol\&. In practice, this module makes it possible to use XMPP in a browser without Websocket support and more generally to have a way to use XMPP while having to get through an HTTP proxy\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBjson\fR: \fItrue | false\fR .RS 4 This option has no effect\&. .RE .PP \fBmax_concat\fR: \fIpos_integer() | infinity\fR .RS 4 This option limits the number of stanzas that the server will send in a single bosh request\&. The default value is \fIunlimited\fR\&. .RE .PP \fBmax_inactivity\fR: \fItimeout()\fR .RS 4 The option defines the maximum inactivity period\&. The default value is \fI30\fR seconds\&. .RE .PP \fBmax_pause\fR: \fIpos_integer()\fR .RS 4 Indicate the maximum length of a temporary session pause (in seconds) that a client can request\&. The default value is \fI120\fR\&. .RE .PP \fBprebind\fR: \fItrue | false\fR .RS 4 If enabled, the client can create the session without going through authentication\&. Basically, it creates a new session with anonymous authentication\&. The default value is \fIfalse\fR\&. .RE .PP \fBqueue_type\fR: \fIram | file\fR .RS 4 Same as top\-level \fIqueue_type\fR option, but applied to this module only\&. .RE .PP \fBram_db_type\fR: \fImnesia | sql | redis\fR .RS 4 Same as \fIdefault_ram_db\fR but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf listen: \- port: 5222 module: ejabberd_c2s \- port: 5443 module: ejabberd_http request_handlers: /bosh: mod_bosh modules: mod_bosh: {} .fi .if n \{\ .RE .\} .RE .SS "mod_caps" .sp This module implements XEP\-0115: Entity Capabilities\&. The main purpose of the module is to provide PEP functionality (see \fImod_pubsub\fR)\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_carboncopy" .sp The module implements XEP\-0280: Message Carbons\&. The module broadcasts messages on all connected user resources (devices)\&. .sp The module has no options\&. .SS "mod_client_state" .sp This module allows for queueing certain types of stanzas when a client indicates that the user is not actively using the client right now (see XEP\-0352: Client State Indication)\&. This can save bandwidth and resources\&. .sp A stanza is dropped from the queue if it\(cqs effectively obsoleted by a new one (e\&.g\&., a new presence stanza would replace an old one from the same client)\&. The queue is flushed if a stanza arrives that won\(cqt be queued, or if the queue size reaches a certain limit (currently 100 stanzas), or if the client becomes active again\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBqueue_chat_states\fR: \fItrue | false\fR .RS 4 Queue "standalone" chat state notifications (as defined in XEP\-0085: Chat State Notifications) while a client indicates inactivity\&. The default value is \fItrue\fR\&. .RE .PP \fBqueue_pep\fR: \fItrue | false\fR .RS 4 Queue PEP notifications while a client is inactive\&. When the queue is flushed, only the most recent notification of a given PEP node is delivered\&. The default value is \fItrue\fR\&. .RE .PP \fBqueue_presence\fR: \fItrue | false\fR .RS 4 While a client is inactive, queue presence stanzas that indicate (un)availability\&. The default value is \fItrue\fR\&. .RE .RE .SS "mod_configure" .sp The module provides server configuration functionality via XEP\-0050: Ad\-Hoc Commands\&. This module requires \fImod_adhoc\fR to be loaded\&. .sp The module has no options\&. .SS "mod_conversejs" .sp This module serves a simple page for the Converse XMPP web browser client\&. .sp To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp You must also setup either the option \fIwebsocket_url\fR or \fIbosh_service_url\fR\&. .sp By default, the options \fIconversejs_css\fR and \fIconversejs_script\fR point to the public Converse\&.js client\&. Alternatively, you can host the client locally using \fImod_http_fileserver\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBbosh_service_url\fR: \fIBoshURL\fR .RS 4 BOSH service URL to which Converse\&.js can connect to\&. .RE .PP \fBconversejs_css\fR: \fIURL\fR .RS 4 Converse\&.js CSS URL\&. .RE .PP \fBconversejs_script\fR: \fIURL\fR .RS 4 Converse\&.js main script URL\&. .RE .PP \fBdefault_domain\fR: \fIDomain\fR .RS 4 Specify a domain to act as the default for user JIDs\&. The default value is the first domain defined in the ejabberd configuration file\&. .RE .PP \fBwebsocket_url\fR: \fIWebsocketURL\fR .RS 4 A websocket URL to which Converse\&.js can connect to\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf listen: \- port: 5280 module: ejabberd_http request_handlers: /websocket: ejabberd_http_ws /conversejs: mod_conversejs modules: mod_conversejs: websocket_url: "ws://example\&.org:5280/websocket" .fi .if n \{\ .RE .\} .RE .SS "mod_delegation" .sp This module is an implementation of XEP\-0355: Namespace Delegation\&. Only admin mode has been implemented by now\&. Namespace delegation allows external services to handle IQ using specific namespace\&. This may be applied for external PEP service\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp Security issue: Namespace delegation gives components access to sensitive data, so permission should be granted carefully, only if you trust the component\&. .sp .5v .RE .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp This module is complementary to \fImod_privilege\fR but can also be used separately\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBnamespaces\fR: \fI{Namespace: Options}\fR .RS 4 If you want to delegate namespaces to a component, specify them in this option, and associate them to an access rule\&. The \fIOptions\fR are: .PP \fBaccess\fR: \fIAccessName\fR .RS 4 The option defines which components are allowed for namespace delegation\&. The default value is \fInone\fR\&. .RE .PP \fBfiltering\fR: \fIAttributes\fR .RS 4 The list of attributes\&. Currently not used\&. .RE .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp Make sure you do not delegate the same namespace to several services at the same time\&. As in the example provided later, to have the \fIsat\-pubsub\&.example\&.org\fR component perform correctly disable the \fImod_pubsub\fR module\&. .sp .if n \{\ .RS 4 .\} .nf access_rules: external_pubsub: allow: external_component external_mam: allow: external_component acl: external_component: server: sat\-pubsub\&.example\&.org modules: \&.\&.\&. mod_delegation: namespaces: urn:xmpp:mam:1: access: external_mam http://jabber\&.org/protocol/pubsub: access: external_pubsub .fi .if n \{\ .RE .\} .RE .SS "mod_disco" .sp This module adds support for XEP\-0030: Service Discovery\&. With this module enabled, services on your server can be discovered by XMPP clients\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBextra_domains\fR: \fI[Domain, \&.\&.\&.]\fR .RS 4 With this option, you can specify a list of extra domains that are added to the Service Discovery item list\&. The default value is an empty list\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 A name of the server in the Service Discovery\&. This will only be displayed by special XMPP clients\&. The default value is \fIejabberd\fR\&. .RE .PP \fBserver_info\fR: \fI[Info, \&.\&.\&.]\fR .RS 4 Specify additional information about the server, as described in XEP\-0157: Contact Addresses for XMPP Services\&. Every \fIInfo\fR element in the list is constructed from the following options: .PP \fBmodules\fR: \fIall | [Module, \&.\&.\&.]\fR .RS 4 The value can be the keyword \fIall\fR, in which case the information is reported in all the services, or a list of ejabberd modules, in which case the information is only specified for the services provided by those modules\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 The field \fIvar\fR name that will be defined\&. See XEP\-0157 for some standardized names\&. .RE .PP \fBurls\fR: \fI[URI, \&.\&.\&.]\fR .RS 4 A list of contact URIs, such as HTTP URLs, XMPP URIs and so on\&. .RE .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf server_info: \- modules: all name: abuse\-addresses urls: ["mailto:abuse@shakespeare\&.lit"] \- modules: [mod_muc] name: "Web chatroom logs" urls: ["http://www\&.example\&.org/muc\-logs"] \- modules: [mod_disco] name: feedback\-addresses urls: \- http://shakespeare\&.lit/feedback\&.php \- mailto:feedback@shakespeare\&.lit \- xmpp:feedback@shakespeare\&.lit \- modules: \- mod_disco \- mod_vcard name: admin\-addresses urls: \- mailto:xmpp@shakespeare\&.lit \- xmpp:admins@shakespeare\&.lit .fi .if n \{\ .RE .\} .RE .RE .SS "mod_fail2ban" .sp The module bans IPs that show the malicious signs\&. Currently only C2S authentication failures are detected\&. .sp Unlike the standalone program, \fImod_fail2ban\fR clears the record of authentication failures after some time since the first failure or on a successful authentication\&. It also does not simply block network traffic, but provides the client with a descriptive error message\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp You should not use this module behind a proxy or load balancer\&. ejabberd will see the failures as coming from the load balancer and, when the threshold of auth failures is reached, will reject all connections coming from the load balancer\&. You can lock all your user base out of ejabberd when using this module behind a proxy\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 Specify an access rule for whitelisting IP addresses or networks\&. If the rule returns \fIallow\fR for a given IP address, that address will never be banned\&. The \fIAccessName\fR should be of type \fIip\fR\&. The default value is \fInone\fR\&. .RE .PP \fBc2s_auth_ban_lifetime\fR: \fItimeout()\fR .RS 4 The lifetime of the IP ban caused by too many C2S authentication failures\&. The default value is \fI1\fR hour\&. .RE .PP \fBc2s_max_auth_failures\fR: \fINumber\fR .RS 4 The number of C2S authentication failures to trigger the IP ban\&. The default value is \fI20\fR\&. .RE .RE .SS "mod_http_api" .sp This module provides a ReST API to call ejabberd commands using JSON data\&. .sp To use this module, in addition to adding it to the \fImodules\fR section, you must also enable it in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp To use a specific API version N, when defining the URL path in the request_handlers, add a \fIvN\fR\&. For example: \fI/api/v2: mod_http_api\fR .sp To run a command, send a POST request to the corresponding URL: \fIhttp://localhost:5280/api/<command_name>\fR .sp The module has no options\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf listen: \- port: 5280 module: ejabberd_http request_handlers: /api: mod_http_api modules: mod_http_api: {} .fi .if n \{\ .RE .\} .RE .SS "mod_http_fileserver" .sp This simple module serves files from the local disk over HTTP\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccesslog\fR: \fIPath\fR .RS 4 File to log accesses using an Apache\-like format\&. No log will be recorded if this option is not specified\&. .RE .PP \fBcontent_types\fR: \fI{Extension: Type}\fR .RS 4 Specify mappings of extension to content type\&. There are several content types already defined\&. With this option you can add new definitions or modify existing ones\&. The default values are: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf content_types: \&.css: text/css \&.gif: image/gif \&.html: text/html \&.jar: application/java\-archive \&.jpeg: image/jpeg \&.jpg: image/jpeg \&.js: text/javascript \&.png: image/png \&.svg: image/svg+xml \&.txt: text/plain \&.xml: application/xml \&.xpi: application/x\-xpinstall \&.xul: application/vnd\&.mozilla\&.xul+xml .fi .if n \{\ .RE .\} .RE .PP \fBcustom_headers\fR: \fI{Name: Value}\fR .RS 4 Indicate custom HTTP headers to be included in all responses\&. There are no custom headers by default\&. .RE .PP \fBdefault_content_type\fR: \fIType\fR .RS 4 Specify the content type to use for unknown extensions\&. The default value is \fIapplication/octet\-stream\fR\&. .RE .PP \fBdirectory_indices\fR: \fI[Index, \&.\&.\&.]\fR .RS 4 Indicate one or more directory index files, similarly to Apache\(cqs \fIDirectoryIndex\fR variable\&. When an HTTP request hits a directory instead of a regular file, those directory indices are looked in order, and the first one found is returned\&. The default value is an empty list\&. .RE .PP \fBdocroot\fR: \fIPath\fR .RS 4 Directory to serve the files from\&. This is a mandatory option\&. .RE .PP \fBmust_authenticate_with\fR: \fI[{Username, Hostname}, \&.\&.\&.]\fR .RS 4 List of accounts that are allowed to use this service\&. Default value: \fI[]\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp This example configuration will serve the files from the local directory \fI/var/www\fR in the address \fIhttp://example\&.org:5280/pub/archive/\fR\&. In this example a new content type \fIogg\fR is defined, \fIpng\fR is redefined, and \fIjpg\fR definition is deleted: .sp .if n \{\ .RS 4 .\} .nf listen: \&.\&.\&. \- port: 5280 module: ejabberd_http request_handlers: \&.\&.\&. /pub/archive: mod_http_fileserver \&.\&.\&. \&.\&.\&. modules: \&.\&.\&. mod_http_fileserver: docroot: /var/www accesslog: /var/log/ejabberd/access\&.log directory_indices: \- index\&.html \- main\&.htm custom_headers: X\-Powered\-By: Erlang/OTP X\-Fry: "It\*(Aqs a widely\-believed fact!" content_types: \&.ogg: audio/ogg \&.png: image/png default_content_type: text/html \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_http_upload" .sp This module allows for requesting permissions to upload a file via HTTP as described in XEP\-0363: HTTP File Upload\&. If the request is accepted, the client receives a URL for uploading the file and another URL from which that file can later be downloaded\&. .sp In order to use this module, it must be enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 This option defines the access rule to limit who is permitted to use the HTTP upload service\&. The default value is \fIlocal\fR\&. If no access rule of that name exists, no user will be allowed to use the service\&. .RE .PP \fBcustom_headers\fR: \fI{Name: Value}\fR .RS 4 This option specifies additional header fields to be included in all HTTP responses\&. By default no custom headers are included\&. .RE .PP \fBdir_mode\fR: \fIPermission\fR .RS 4 This option defines the permission bits of the \fIdocroot\fR directory and any directories created during file uploads\&. The bits are specified as an octal number (see the chmod(1) manual page) within double quotes\&. For example: "0755"\&. The default is undefined, which means no explicit permissions will be set\&. .RE .PP \fBdocroot\fR: \fIPath\fR .RS 4 Uploaded files are stored below the directory specified (as an absolute path) with this option\&. The keyword @HOME@ is replaced with the home directory of the user running ejabberd, and the keyword @HOST@ with the virtual host name\&. The default value is "@HOME@/upload"\&. .RE .PP \fBexternal_secret\fR: \fIText\fR .RS 4 This option makes it possible to offload all HTTP Upload processing to a separate HTTP server\&. Both ejabberd and the HTTP server should share this secret and behave exactly as described at Prosody\(cqs mod_http_upload_external in the \fIImplementation\fR section\&. There is no default value\&. .RE .PP \fBfile_mode\fR: \fIPermission\fR .RS 4 This option defines the permission bits of uploaded files\&. The bits are specified as an octal number (see the chmod(1) manual page) within double quotes\&. For example: "0644"\&. The default is undefined, which means no explicit permissions will be set\&. .RE .PP \fBget_url\fR: \fIURL\fR .RS 4 This option specifies the initial part of the GET URLs used for downloading the files\&. The default value is \fIundefined\fR\&. When this option is \fIundefined\fR, this option is set to the same value as \fIput_url\fR\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: if GET requests are handled by \fImod_http_upload\fR, the \fIget_url\fR must match the \fIput_url\fR\&. Setting it to a different value only makes sense if an external web server or \fImod_http_fileserver\fR is used to serve the uploaded files\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "upload\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .PP \fBjid_in_url\fR: \fInode | sha1\fR .RS 4 When this option is set to \fInode\fR, the node identifier of the user\(cqs JID (i\&.e\&., the user name) is included in the GET and PUT URLs generated by \fImod_http_upload\fR\&. Otherwise, a SHA\-1 hash of the user\(cqs bare JID is included instead\&. The default value is \fIsha1\fR\&. .RE .PP \fBmax_size\fR: \fISize\fR .RS 4 This option limits the acceptable file size\&. Either a number of bytes (larger than zero) or \fIinfinity\fR must be specified\&. The default value is \fI104857600\fR\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 A name of the service in the Service Discovery\&. This will only be displayed by special XMPP clients\&. The default value is "HTTP File Upload"\&. .RE .PP \fBput_url\fR: \fIURL\fR .RS 4 This option specifies the initial part of the PUT URLs used for file uploads\&. The keyword @HOST@ is replaced with the virtual host name\&. NOTE: different virtual hosts cannot use the same PUT URL\&. The default value is "https://@HOST@:5443/upload"\&. .RE .PP \fBrm_on_unregister\fR: \fItrue | false\fR .RS 4 This option specifies whether files uploaded by a user should be removed when that user is unregistered\&. The default value is \fItrue\fR\&. .RE .PP \fBsecret_length\fR: \fILength\fR .RS 4 This option defines the length of the random string included in the GET and PUT URLs generated by \fImod_http_upload\fR\&. The minimum length is 8 characters, but it is recommended to choose a larger value\&. The default value is \fI40\fR\&. .RE .PP \fBservice_url\fR .RS 4 Deprecated\&. .RE .PP \fBthumbnail\fR: \fItrue | false\fR .RS 4 This option specifies whether ejabberd should create thumbnails of uploaded images\&. If a thumbnail is created, a <thumbnail/> element that contains the download <uri/> and some metadata is returned with the PUT response\&. The default value is \fIfalse\fR\&. .RE .PP \fBvcard\fR: \fIvCard\fR .RS 4 A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp For example, the following XML representation of vCard: .sp .if n \{\ .RS 4 .\} .nf <vCard xmlns=\*(Aqvcard\-temp\*(Aq> <FN>Conferences</FN> <ADR> <WORK/> <STREET>Elm Street</STREET> </ADR> </vCard> .fi .if n \{\ .RE .\} .sp will be translated to: .sp .if n \{\ .RS 4 .\} .nf vcard: fn: Conferences adr: \- work: true street: Elm Street .fi .if n \{\ .RE .\} .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf listen: \&.\&.\&. \- port: 5443 module: ejabberd_http tls: true request_handlers: \&.\&.\&. /upload: mod_http_upload \&.\&.\&. \&.\&.\&. modules: \&.\&.\&. mod_http_upload: docroot: /ejabberd/upload put_url: "https://@HOST@:5443/upload" \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_http_upload_quota" .sp This module adds quota support for mod_http_upload\&. .sp This module depends on \fImod_http_upload\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_hard_quota\fR: \fIAccessName\fR .RS 4 This option defines which access rule is used to specify the "hard quota" for the matching JIDs\&. That rule must yield a positive number for any JID that is supposed to have a quota limit\&. This is the number of megabytes a corresponding user may upload\&. When this threshold is exceeded, ejabberd deletes the oldest files uploaded by that user until their disk usage equals or falls below the specified soft quota (see \fIaccess_soft_quota\fR)\&. The default value is \fIhard_upload_quota\fR\&. .RE .PP \fBaccess_soft_quota\fR: \fIAccessName\fR .RS 4 This option defines which access rule is used to specify the "soft quota" for the matching JIDs\&. That rule must yield a positive number of megabytes for any JID that is supposed to have a quota limit\&. See the description of the \fIaccess_hard_quota\fR option for details\&. The default value is \fIsoft_upload_quota\fR\&. .RE .PP \fBmax_days\fR: \fIDays\fR .RS 4 If a number larger than zero is specified, any files (and directories) older than this number of days are removed from the subdirectories of the \fIdocroot\fR directory, once per day\&. The default value is \fIinfinity\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp Please note that it\(cqs not necessary to specify the \fIaccess_hard_quota\fR and \fIaccess_soft_quota\fR options in order to use the quota feature\&. You can stick to the default names and just specify access rules such as those in this example: .sp .if n \{\ .RS 4 .\} .nf shaper_rules: \&.\&.\&. soft_upload_quota: 1000: all # MiB hard_upload_quota: 1100: all # MiB \&.\&.\&. modules: \&.\&.\&. mod_http_upload: {} mod_http_upload_quota: max_days: 100 \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_jidprep" .sp This module allows XMPP clients to ask the server to normalize a JID as per the rules specified in RFC 6122: XMPP Address Format\&. This might be useful for clients in certain constrained environments, or for testing purposes\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 This option defines which access rule will be used to control who is allowed to use this service\&. The default value is \fIlocal\fR\&. .RE .RE .SS "mod_last" .sp This module adds support for XEP\-0012: Last Activity\&. It can be used to discover when a disconnected user last accessed the server, to know when a connected user was last active on the server, or to query the uptime of the ejabberd server\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_legacy_auth" .sp The module implements XEP\-0078: Non\-SASL Authentication\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp This type of authentication was obsoleted in 2008 and you unlikely need this module unless you have something like outdated Jabber bots\&. .sp .5v .RE .sp The module has no options\&. .SS "mod_mam" .sp This module implements XEP\-0313: Message Archive Management\&. Compatible XMPP clients can use it to store their chat history on the server\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_preferences\fR: \fIAccessName\fR .RS 4 This access rule defines who is allowed to modify the MAM preferences\&. The default value is \fIall\fR\&. .RE .PP \fBassume_mam_usage\fR: \fItrue | false\fR .RS 4 This option determines how ejabberd\(cqs stream management code (see \fImod_stream_mgmt\fR) handles unacknowledged messages when the connection is lost\&. Usually, such messages are either bounced or resent\&. However, neither is done for messages that were stored in the user\(cqs MAM archive if this option is set to \fItrue\fR\&. In this case, ejabberd assumes those messages will be retrieved from the archive\&. The default value is \fIfalse\fR\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBclear_archive_on_room_destroy\fR: \fItrue | false\fR .RS 4 Whether to destroy message archive of a room (see \fImod_muc\fR) when it gets destroyed\&. The default value is \fItrue\fR\&. .RE .PP \fBcompress_xml\fR: \fItrue | false\fR .RS 4 When enabled, new messages added to archives are compressed using a custom compression algorithm\&. This feature works only with SQL backends\&. The default value is \fIfalse\fR\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBdefault\fR: \fIalways | never | roster\fR .RS 4 The option defines default policy for chat history\&. When \fIalways\fR is set every chat message is stored\&. With \fIroster\fR only chat history with contacts from user\(cqs roster is stored\&. And \fInever\fR fully disables chat history\&. Note that a client can change its policy via protocol commands\&. The default value is \fInever\fR\&. .RE .PP \fBrequest_activates_archiving\fR: \fItrue | false\fR .RS 4 If the value is \fItrue\fR, no messages are stored for a user until their client issue a MAM request, regardless of the value of the \fIdefault\fR option\&. Once the server received a request, that user\(cqs messages are archived as usual\&. The default value is \fIfalse\fR\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .PP \fBuser_mucsub_from_muc_archive\fR: \fItrue | false\fR .RS 4 When this option is disabled, for each individual subscriber a separa mucsub message is stored\&. With this option enabled, when a user fetches archive virtual mucsub, messages are generated from muc archives\&. The default value is \fIfalse\fR\&. .RE .RE .SS "mod_metrics" .sp This module sends events to external backend (by now only grapherl is supported)\&. Supported events are: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} sm_register_connection .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} sm_remove_connection .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} user_send_packet .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} user_receive_packet .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} s2s_send_packet .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} s2s_receive_packet .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} register_user .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} remove_user .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} offline_message .RE .sp When enabled, every call to these hooks triggers a counter event to be sent to the external backend\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBip\fR: \fIIPv4Address\fR .RS 4 IPv4 address where the backend is located\&. The default value is \fI127\&.0\&.0\&.1\fR\&. .RE .PP \fBport\fR: \fIPort\fR .RS 4 An internet port number at which the backend is listening for incoming connections/packets\&. The default value is \fI11111\fR\&. .RE .RE .SS "mod_mix" .sp This module is an experimental implementation of XEP\-0369: Mediated Information eXchange (MIX)\&. MIX support was added in ejabberd 16\&.03 as an experimental feature, updated in 19\&.02, and is not yet ready to use in production\&. It\(cqs asserted that the MIX protocol is going to replace the MUC protocol in the future (see \fImod_muc\fR)\&. .sp To learn more about how to use that feature, you can refer to our tutorial: Getting started with XEP\-0369: Mediated Information eXchange (MIX) v0\&.1\&. .sp The module depends on \fImod_mam\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_create\fR: \fIAccessName\fR .RS 4 An access rule to control MIX channels creations\&. The default value is \fIall\fR\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "mix\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 A name of the service in the Service Discovery\&. This will only be displayed by special XMPP clients\&. The default value is \fIChannels\fR\&. .RE .RE .SS "mod_mix_pam" .sp This module implements XEP\-0405: Mediated Information eXchange (MIX): Participant Server Requirements\&. The module is needed if MIX compatible clients on your server are going to join MIX channels (either on your server or on any remote servers)\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp \fImod_mix\fR is not required for this module to work, however, without \fImod_mix_pam\fR the MIX functionality of your local XMPP clients will be impaired\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_mqtt" .sp This module adds support for the MQTT protocol version \fI3\&.1\&.1\fR and \fI5\&.0\fR\&. Remember to configure \fImod_mqtt\fR in \fImodules\fR and \fIlisten\fR sections\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_publish\fR: \fI{TopicFilter: AccessName}\fR .RS 4 Access rules to restrict access to topics for publishers\&. By default there are no restrictions\&. .RE .PP \fBaccess_subscribe\fR: \fI{TopicFilter: AccessName}\fR .RS 4 Access rules to restrict access to topics for subscribers\&. By default there are no restrictions\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBmatch_retained_limit\fR: \fIpos_integer() | infinity\fR .RS 4 The option limits the number of retained messages returned to a client when it subscribes to some topic filter\&. The default value is \fI1000\fR\&. .RE .PP \fBmax_queue\fR: \fISize\fR .RS 4 Maximum queue size for outgoing packets\&. The default value is \fI5000\fR\&. .RE .PP \fBmax_topic_aliases\fR: \fI0\&.\&.65535\fR .RS 4 The maximum number of aliases a client is able to associate with the topics\&. The default value is \fI100\fR\&. .RE .PP \fBmax_topic_depth\fR: \fIDepth\fR .RS 4 The maximum topic depth, i\&.e\&. the number of slashes (\fI/\fR) in the topic\&. The default value is \fI8\fR\&. .RE .PP \fBqueue_type\fR: \fIram | file\fR .RS 4 Same as top\-level \fIqueue_type\fR option, but applied to this module only\&. .RE .PP \fBram_db_type\fR: \fImnesia\fR .RS 4 Same as top\-level \fIdefault_ram_db\fR option, but applied to this module only\&. .RE .PP \fBsession_expiry\fR: \fItimeout()\fR .RS 4 The option specifies how long to wait for an MQTT session resumption\&. When \fI0\fR is set, the session gets destroyed when the underlying client connection is closed\&. The default value is \fI5\fR minutes\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_muc" .sp This module provides support for XEP\-0045: Multi\-User Chat\&. Users can discover existing rooms, join or create them\&. Occupants of a room can chat in public or have private chats\&. .sp The MUC service allows any Jabber ID to register a nickname, so nobody else can use that nickname in any room in the MUC service\&. To register a nickname, open the Service Discovery in your XMPP client and register in the MUC service\&. .sp This module supports clustering and load balancing\&. One module can be started per cluster node\&. Rooms are distributed at creation time on all available MUC module instances\&. The multi\-user chat module is clustered but the rooms themselves are not clustered nor fault\-tolerant: if the node managing a set of rooms goes down, the rooms disappear and they will be recreated on an available node on first connection attempt\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 You can specify who is allowed to use the Multi\-User Chat service\&. By default everyone is allowed to use it\&. .RE .PP \fBaccess_admin\fR: \fIAccessName\fR .RS 4 This option specifies who is allowed to administrate the Multi\-User Chat service\&. The default value is \fInone\fR, which means that only the room creator can administer their room\&. The administrators can send a normal message to the service JID, and it will be shown in all active rooms as a service message\&. The administrators can send a groupchat message to the JID of an active room, and the message will be shown in the room as a service message\&. .RE .PP \fBaccess_create\fR: \fIAccessName\fR .RS 4 To configure who is allowed to create new rooms at the Multi\-User Chat service, this option can be used\&. The default value is \fIall\fR, which means everyone is allowed to create rooms\&. .RE .PP \fBaccess_mam\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fImam\fR room option\&. The default value is \fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_persistent\fR: \fIAccessName\fR .RS 4 To configure who is allowed to modify the \fIpersistent\fR room option\&. The default value is \fIall\fR, which means everyone is allowed to modify that option\&. .RE .PP \fBaccess_register\fR: \fIAccessName\fR .RS 4 This option specifies who is allowed to register nickname within the Multi\-User Chat service\&. The default is \fIall\fR for backward compatibility, which means that any user is allowed to register any free nick\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Define the type of persistent storage where the module will store room information\&. The default is the storage defined by the global option \fIdefault_db\fR, or \fImnesia\fR if omitted\&. .RE .PP \fBdefault_room_options\fR: \fIOptions\fR .RS 4 This option allows to define the desired default room options\&. Note that the creator of a room can modify the options of his room at any time using an XMPP client with MUC capability\&. The \fIOptions\fR are: .PP \fBallow_change_subj\fR: \fItrue | false\fR .RS 4 Allow occupants to change the subject\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_private_messages\fR: \fItrue | false\fR .RS 4 Occupants can send private messages to other occupants\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_private_messages_from_visitors\fR: \fIanyone | moderators | nobody\fR .RS 4 Visitors can send private messages to other occupants\&. The default value is \fIanyone\fR which means visitors can send private messages to any occupant\&. .RE .PP \fBallow_query_users\fR: \fItrue | false\fR .RS 4 Occupants can send IQ queries to other occupants\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_subscription\fR: \fItrue | false\fR .RS 4 Allow users to subscribe to room events as described in Multi\-User Chat Subscriptions\&. The default value is \fIfalse\fR\&. .RE .PP \fBallow_user_invites\fR: \fItrue | false\fR .RS 4 Allow occupants to send invitations\&. The default value is \fIfalse\fR\&. .RE .PP \fBallow_visitor_nickchange\fR: \fItrue | false\fR .RS 4 Allow visitors to change nickname\&. The default value is \fItrue\fR\&. .RE .PP \fBallow_visitor_status\fR: \fItrue | false\fR .RS 4 Allow visitors to send status text in presence updates\&. If disallowed, the status text is stripped before broadcasting the presence update to all the room occupants\&. The default value is \fItrue\fR\&. .RE .PP \fBanonymous\fR: \fItrue | false\fR .RS 4 The room is anonymous: occupants don\(cqt see the real JIDs of other occupants\&. Note that the room moderators can always see the real JIDs of the occupants\&. The default value is \fItrue\fR\&. .RE .PP \fBcaptcha_protected\fR: \fItrue | false\fR .RS 4 When a user tries to join a room where they have no affiliation (not owner, admin or member), the room requires them to fill a CAPTCHA challenge (see section CAPTCHA in order to accept their join in the room\&. The default value is \fIfalse\fR\&. .RE .PP \fBlang\fR: \fILanguage\fR .RS 4 Preferred language for the discussions in the room\&. The language format should conform to RFC 5646\&. There is no value by default\&. .RE .PP \fBlogging\fR: \fItrue | false\fR .RS 4 The public messages are logged using \fImod_muc_log\fR\&. The default value is \fIfalse\fR\&. .RE .PP \fBmam\fR: \fItrue | false\fR .RS 4 Enable message archiving\&. Implies mod_mam is enabled\&. The default value is \fIfalse\fR\&. .RE .PP \fBmax_users\fR: \fINumber\fR .RS 4 Maximum number of occupants in the room\&. The default value is \fI200\fR\&. .RE .PP \fBmembers_by_default\fR: \fItrue | false\fR .RS 4 The occupants that enter the room are participants by default, so they have "voice"\&. The default value is \fItrue\fR\&. .RE .PP \fBmembers_only\fR: \fItrue | false\fR .RS 4 Only members of the room can enter\&. The default value is \fIfalse\fR\&. .RE .PP \fBmoderated\fR: \fItrue | false\fR .RS 4 Only occupants with "voice" can send public messages\&. The default value is \fItrue\fR\&. .RE .PP \fBpassword\fR: \fIPassword\fR .RS 4 Password of the room\&. Implies option \fIpassword_protected\fR set to \fItrue\fR\&. There is no default value\&. .RE .PP \fBpassword_protected\fR: \fItrue | false\fR .RS 4 The password is required to enter the room\&. The default value is \fIfalse\fR\&. .RE .PP \fBpersistent\fR: \fItrue | false\fR .RS 4 The room persists even if the last participant leaves\&. The default value is \fIfalse\fR\&. .RE .PP \fBpresence_broadcast\fR: \fI[moderator | participant | visitor, \&.\&.\&.]\fR .RS 4 List of roles for which presence is broadcasted\&. The list can contain one or several of: \fImoderator\fR, \fIparticipant\fR, \fIvisitor\fR\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf presence_broadcast: \- moderator \- participant \- visitor .fi .if n \{\ .RE .\} .RE .PP \fBpublic\fR: \fItrue | false\fR .RS 4 The room is public in the list of the MUC service, so it can be discovered\&. MUC admins and room participants will see private rooms in Service Discovery if their XMPP client supports this feature\&. The default value is \fItrue\fR\&. .RE .PP \fBpublic_list\fR: \fItrue | false\fR .RS 4 The list of participants is public, without requiring to enter the room\&. The default value is \fItrue\fR\&. .RE .PP \fBtitle\fR: \fIRoom Title\fR .RS 4 A human\-readable title of the room\&. There is no default value .RE .RE .PP \fBhibernation_timeout\fR: \fIinfinity | Seconds\fR .RS 4 Timeout before hibernating the room process, expressed in seconds\&. The default value is \fIinfinity\fR\&. .RE .PP \fBhistory_size\fR: \fISize\fR .RS 4 A small history of the current discussion is sent to users when they enter the room\&. With this option you can define the number of history messages to keep and send to users joining the room\&. The value is a non\-negative integer\&. Setting the value to 0 disables the history feature and, as a result, nothing is kept in memory\&. The default value is 20\&. This value affects all rooms on the service\&. NOTE: modern XMPP clients rely on Message Archives (XEP\-0313), so feel free to disable the history feature if you\(cqre only using modern clients and have \fImod_mam\fR module loaded\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "conference\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .sp \fINote\fR about the next option: added in 21\&.01: .PP \fBmax_captcha_whitelist\fR: \fINumber\fR .RS 4 This option defines the maximum number of characters that Captcha Whitelist can have when configuring the room\&. The default value is \fIinfinity\fR\&. .RE .sp \fINote\fR about the next option: added in 21\&.01: .PP \fBmax_password\fR: \fINumber\fR .RS 4 This option defines the maximum number of characters that Password can have when configuring the room\&. The default value is \fIinfinity\fR\&. .RE .PP \fBmax_room_desc\fR: \fINumber\fR .RS 4 This option defines the maximum number of characters that Room Description can have when configuring the room\&. The default value is \fIinfinity\fR\&. .RE .PP \fBmax_room_id\fR: \fINumber\fR .RS 4 This option defines the maximum number of characters that Room ID can have when creating a new room\&. The default value is \fIinfinity\fR\&. .RE .PP \fBmax_room_name\fR: \fINumber\fR .RS 4 This option defines the maximum number of characters that Room Name can have when configuring the room\&. The default value is \fIinfinity\fR\&. .RE .PP \fBmax_rooms_discoitems\fR: \fINumber\fR .RS 4 When there are more rooms than this \fINumber\fR, only the non\-empty ones are returned in a Service Discovery query\&. The default value is \fI100\fR\&. .RE .PP \fBmax_user_conferences\fR: \fINumber\fR .RS 4 This option defines the maximum number of rooms that any given user can join\&. The default value is \fI100\fR\&. This option is used to prevent possible abuses\&. Note that this is a soft limit: some users can sometimes join more conferences in cluster configurations\&. .RE .PP \fBmax_users\fR: \fINumber\fR .RS 4 This option defines at the service level, the maximum number of users allowed per room\&. It can be lowered in each room configuration but cannot be increased in individual room configuration\&. The default value is \fI200\fR\&. .RE .PP \fBmax_users_admin_threshold\fR: \fINumber\fR .RS 4 This option defines the number of service admins or room owners allowed to enter the room when the maximum number of allowed occupants was reached\&. The default limit is \fI5\fR\&. .RE .PP \fBmax_users_presence\fR: \fINumber\fR .RS 4 This option defines after how many users in the room, it is considered overcrowded\&. When a MUC room is considered overcrowed, presence broadcasts are limited to reduce load, traffic and excessive presence "storm" received by participants\&. The default value is \fI1000\fR\&. .RE .PP \fBmin_message_interval\fR: \fINumber\fR .RS 4 This option defines the minimum interval between two messages send by an occupant in seconds\&. This option is global and valid for all rooms\&. A decimal value can be used\&. When this option is not defined, message rate is not limited\&. This feature can be used to protect a MUC service from occupant abuses and limit number of messages that will be broadcasted by the service\&. A good value for this minimum message interval is 0\&.4 second\&. If an occupant tries to send messages faster, an error is send back explaining that the message has been discarded and describing the reason why the message is not acceptable\&. .RE .PP \fBmin_presence_interval\fR: \fINumber\fR .RS 4 This option defines the minimum of time between presence changes coming from a given occupant in seconds\&. This option is global and valid for all rooms\&. A decimal value can be used\&. When this option is not defined, no restriction is applied\&. This option can be used to protect a MUC service for occupants abuses\&. If an occupant tries to change its presence more often than the specified interval, the presence is cached by ejabberd and only the last presence is broadcasted to all occupants in the room after expiration of the interval delay\&. Intermediate presence packets are silently discarded\&. A good value for this option is 4 seconds\&. .RE .PP \fBname\fR: \fIstring()\fR .RS 4 The value of the service name\&. This name is only visible in some clients that support XEP\-0030: Service Discovery\&. The default is \fIChatrooms\fR\&. .RE .PP \fBpreload_rooms\fR: \fItrue | false\fR .RS 4 Whether to load all persistent rooms in memory on startup\&. If disabled, the room is only loaded on first participant join\&. The default is \fItrue\fR\&. It makes sense to disable room preloading when the number of rooms is high: this will improve server startup time and memory consumption\&. .RE .PP \fBqueue_type\fR: \fIram | file\fR .RS 4 Same as top\-level \fIqueue_type\fR option, but applied to this module only\&. .RE .PP \fBram_db_type\fR: \fImnesia | sql\fR .RS 4 Define the type of volatile (in\-memory) storage where the module will store room information (\fImuc_online_room\fR and \fImuc_online_users\fR)\&. .RE .PP \fBregexp_room_id\fR: \fIstring()\fR .RS 4 This option defines the regular expression that a Room ID must satisfy to allow the room creation\&. The default value is the empty string\&. .RE .PP \fBroom_shaper\fR: \fInone | ShaperName\fR .RS 4 This option defines shaper for the MUC rooms\&. The default value is \fInone\fR\&. .RE .PP \fBuser_message_shaper\fR: \fInone | ShaperName\fR .RS 4 This option defines shaper for the users messages\&. The default value is \fInone\fR\&. .RE .PP \fBuser_presence_shaper\fR: \fInone | ShaperName\fR .RS 4 This option defines shaper for the users presences\&. The default value is \fInone\fR\&. .RE .PP \fBvcard\fR: \fIvCard\fR .RS 4 A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp For example, the following XML representation of vCard: .sp .if n \{\ .RS 4 .\} .nf <vCard xmlns=\*(Aqvcard\-temp\*(Aq> <FN>Conferences</FN> <ADR> <WORK/> <STREET>Elm Street</STREET> </ADR> </vCard> .fi .if n \{\ .RE .\} .sp will be translated to: .sp .if n \{\ .RS 4 .\} .nf vcard: fn: Conferences adr: \- work: true street: Elm Street .fi .if n \{\ .RE .\} .RE .RE .SS "mod_muc_admin" .sp This module provides commands to administer local MUC services and their MUC rooms\&. It also provides simple WebAdmin pages to view the existing rooms\&. .sp This module depends on \fImod_muc\fR\&. .sp The module has no options\&. .SS "mod_muc_log" .sp This module enables optional logging of Multi\-User Chat (MUC) public conversations to HTML\&. Once you enable this module, users can join a room using a MUC capable XMPP client, and if they have enough privileges, they can request the configuration form in which they can set the option to enable room logging\&. .sp Features: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Room details are added on top of each page: room title, JID, author, subject and configuration\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} The room JID in the generated HTML is a link to join the room (using XMPP URI)\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Subject and room configuration changes are tracked and displayed\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Joins, leaves, nick changes, kicks, bans and \fI/me\fR are tracked and displayed, including the reason if available\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Generated HTML files are XHTML 1\&.0 Transitional and CSS compliant\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Timestamps are self\-referencing links\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Links on top for quicker navigation: Previous day, Next day, Up\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} CSS is used for style definition, and a custom CSS file can be used\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} URLs on messages and subjects are converted to hyperlinks\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Timezone used on timestamps is shown on the log files\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} A custom link can be added on top of each page\&. .RE .sp The module depends on \fImod_muc\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_log\fR: \fIAccessName\fR .RS 4 This option restricts which occupants are allowed to enable or disable room logging\&. The default value is \fImuc_admin\fR\&. NOTE: for this default setting you need to have an access rule for \fImuc_admin\fR in order to take effect\&. .RE .PP \fBcssfile\fR: \fIPath | URL\fR .RS 4 With this option you can set whether the HTML files should have a custom CSS file or if they need to use the embedded CSS\&. Allowed values are either \fIPath\fR to local file or an \fIURL\fR to a remote file\&. By default a predefined CSS will be embedded into the HTML page\&. .RE .PP \fBdirname\fR: \fIroom_jid | room_name\fR .RS 4 Allows to configure the name of the room directory\&. If set to \fIroom_jid\fR, the room directory name will be the full room JID\&. Otherwise, the room directory name will be only the room name, not including the MUC service name\&. The default value is \fIroom_jid\fR\&. .RE .PP \fBdirtype\fR: \fIsubdirs | plain\fR .RS 4 The type of the created directories can be specified with this option\&. If set to \fIsubdirs\fR, subdirectories are created for each year and month\&. Otherwise, the names of the log files contain the full date, and there are no subdirectories\&. The default value is \fIsubdirs\fR\&. .RE .PP \fBfile_format\fR: \fIhtml | plaintext\fR .RS 4 Define the format of the log files: \fIhtml\fR stores in HTML format, \fIplaintext\fR stores in plain text\&. The default value is \fIhtml\fR\&. .RE .PP \fBfile_permissions\fR: \fI{mode: Mode, group: Group}\fR .RS 4 Define the permissions that must be used when creating the log files: the number of the mode, and the numeric id of the group that will own the files\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf file_permissions: mode: 644 group: 33 .fi .if n \{\ .RE .\} .RE .PP \fBoutdir\fR: \fIPath\fR .RS 4 This option sets the full path to the directory in which the HTML files should be stored\&. Make sure the ejabberd daemon user has write access on that directory\&. The default value is \fIwww/muc\fR\&. .RE .PP \fBspam_prevention\fR: \fItrue | false\fR .RS 4 If set to \fItrue\fR, a special attribute is added to links that prevent their indexation by search engines\&. The default value is \fItrue\fR, which mean that \fInofollow\fR attributes will be added to user submitted links\&. .RE .PP \fBtimezone\fR: \fIlocal | universal\fR .RS 4 The time zone for the logs is configurable with this option\&. If set to \fIlocal\fR, the local time, as reported to Erlang emulator by the operating system, will be used\&. Otherwise, UTC time will be used\&. The default value is \fIlocal\fR\&. .RE .PP \fBtop_link\fR: \fI{URL: Text}\fR .RS 4 With this option you can customize the link on the top right corner of each log file\&. The default value is shown in the example below: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf top_link: /: Home .fi .if n \{\ .RE .\} .RE .PP \fBurl\fR: \fIURL\fR .RS 4 A top level \fIURL\fR where a client can access logs of a particular conference\&. The conference name is appended to the URL if \fIdirname\fR option is set to \fIroom_name\fR or a conference JID is appended to the \fIURL\fR otherwise\&. There is no default value\&. .RE .RE .SS "mod_multicast" .sp This module implements a service for XEP\-0033: Extended Stanza Addressing\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccess\fR .RS 4 The access rule to restrict who can send packets to the multicast service\&. Default value: \fIall\fR\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "multicast\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. The default value is \fImulticast\&.@HOST@\fR\&. .RE .PP \fBlimits\fR: \fISender: Stanza: Number\fR .RS 4 Specify a list of custom limits which override the default ones defined in XEP\-0033\&. Limits are defined per sender type and stanza type, where: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIsender\fR can be: \fIlocal\fR or \fIremote\fR\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIstanza\fR can be: \fImessage\fR or \fIpresence\fR\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fInumber\fR can be a positive integer or \fIinfinite\fR\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf # Default values: local: message: 100 presence: 100 remote: message: 20 presence: 20 .fi .if n \{\ .RE .\} .RE .RE .PP \fBname\fR .RS 4 Service name to provide in the Info query to the Service Discovery\&. Default is \fI"Multicast"\fR\&. .RE .PP \fBvcard\fR .RS 4 vCard element to return when queried\&. Default value is \fIundefined\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf # Only admins can send packets to multicast service access_rules: multicast: \- allow: admin # If you want to allow all your users: access_rules: multicast: \- allow # This allows both admins and remote users to send packets, # but does not allow local users acl: allservers: server_glob: "*" access_rules: multicast: \- allow: admin \- deny: local \- allow: allservers modules: mod_multicast: host: multicast\&.example\&.org access: multicast limits: local: message: 40 presence: infinite remote: message: 150 .fi .if n \{\ .RE .\} .RE .SS "mod_offline" .sp This module implements XEP\-0160: Best Practices for Handling Offline Messages and XEP\-0013: Flexible Offline Message Retrieval\&. This means that all messages sent to an offline user will be stored on the server until that user comes online again\&. Thus it is very similar to how email works\&. A user is considered offline if no session presence priority > 0 are currently open\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp \fIejabberdctl\fR has a command to delete expired messages (see chapter Managing an ejabberd server in online documentation\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_max_user_messages\fR: \fIAccessName\fR .RS 4 This option defines which access rule will be enforced to limit the maximum number of offline messages that a user can have (quota)\&. When a user has too many offline messages, any new messages that they receive are discarded, and a <resource\-constraint/> error is returned to the sender\&. The default value is \fImax_user_offline_messages\fR\&. .RE .PP \fBbounce_groupchat\fR: \fItrue | false\fR .RS 4 This option is use the disable an optimisation that avoids bouncing error messages when groupchat messages could not be stored as offline\&. It will reduce chat room load, without any drawback in standard use cases\&. You may change default value only if you have a custom module which uses offline hook after \fImod_offline\fR\&. This option can be useful for both standard MUC and MucSub, but the bounce is much more likely to happen in the context of MucSub, so it is even more important to have it on large MucSub services\&. The default value is \fIfalse\fR, meaning the optimisation is enabled\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBstore_empty_body\fR: \fItrue | false | unless_chat_state\fR .RS 4 Whether or not to store messages that lack a <body/> element\&. The default value is \fIunless_chat_state\fR, which tells ejabberd to store messages even if they lack the <body/> element, unless they only contain a chat state notification (as defined in XEP\-0085: Chat State Notifications\&. .RE .PP \fBstore_groupchat\fR: \fItrue | false\fR .RS 4 Whether or not to store groupchat messages\&. The default value is \fIfalse\fR\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .PP \fBuse_mam_for_storage\fR: \fItrue | false\fR .RS 4 This is an experimental option\&. Enabling this option will make \fImod_offline\fR not use the former spool table for storing MucSub offline messages, but will use the archive table instead\&. This use of the archive table is cleaner and it makes it possible for clients to slowly drop the former offline use case and rely on message archive instead\&. It also further reduces the storage required when you enabled MucSub\&. Enabling this option has a known drawback for the moment: most of flexible message retrieval queries don\(cqt work (those that allow retrieval/deletion of messages by id), but this specification is not widely used\&. The default value is \fIfalse\fR to keep former behaviour as default and ensure this option is disabled\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp This example allows power users to have as much as 5000 offline messages, administrators up to 2000, and all the other users up to 100: .sp .if n \{\ .RS 4 .\} .nf acl: admin: user: \- admin1@localhost \- admin2@example\&.org poweruser: user: \- bob@example\&.org \- jane@example\&.org shaper_rules: max_user_offline_messages: \- 5000: poweruser \- 2000: admin \- 100 modules: \&.\&.\&. mod_offline: access_max_user_messages: max_user_offline_messages \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_ping" .sp This module implements support for XEP\-0199: XMPP Ping and periodic keepalives\&. When this module is enabled ejabberd responds correctly to ping requests, as defined by the protocol\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBping_ack_timeout\fR: \fItimeout()\fR .RS 4 How long to wait before deeming that a client has not answered a given server ping request\&. The default value is \fIundefined\fR\&. .RE .PP \fBping_interval\fR: \fItimeout()\fR .RS 4 How often to send pings to connected clients, if option \fIsend_pings\fR is set to \fItrue\fR\&. If a client connection does not send or receive any stanza within this interval, a ping request is sent to the client\&. The default value is \fI1\fR minute\&. .RE .PP \fBsend_pings\fR: \fItrue | false\fR .RS 4 If this option is set to \fItrue\fR, the server sends pings to connected clients that are not active in a given interval defined in \fIping_interval\fR option\&. This is useful to keep client connections alive or checking availability\&. The default value is \fIfalse\fR\&. .RE .PP \fBtimeout_action\fR: \fInone | kill\fR .RS 4 What to do when a client does not answer to a server ping request in less than period defined in \fIping_ack_timeout\fR option: \fIkill\fR means destroying the underlying connection, \fInone\fR means to do nothing\&. NOTE: when \fImod_stream_mgmt\fR is loaded and stream management is enabled by a client, killing the client connection doesn\(cqt mean killing the client session \- the session will be kept alive in order to give the client a chance to resume it\&. The default value is \fInone\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_ping: send_pings: true ping_interval: 4 min timeout_action: kill \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_pres_counter" .sp This module detects flood/spam in presence subscriptions traffic\&. If a user sends or receives more of those stanzas in a given time interval, the exceeding stanzas are silently dropped, and a warning is logged\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcount\fR: \fINumber\fR .RS 4 The number of subscription presence stanzas (subscribe, unsubscribe, subscribed, unsubscribed) allowed for any direction (input or output) per time defined in \fIinterval\fR option\&. Please note that two users subscribing to each other usually generate 4 stanzas, so the recommended value is \fI4\fR or more\&. The default value is \fI5\fR\&. .RE .PP \fBinterval\fR: \fItimeout()\fR .RS 4 The time interval\&. The default value is \fI1\fR minute\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_pres_counter: count: 5 interval: 30 secs \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_privacy" .sp This module implements XEP\-0016: Privacy Lists\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp Nowadays modern XMPP clients rely on XEP\-0191: Blocking Command which is implemented by \fImod_blocking\fR module\&. However, you still need \fImod_privacy\fR loaded in order for \fImod_blocking\fR to work\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_private" .sp This module adds support for XEP\-0049: Private XML Storage\&. .sp Using this method, XMPP entities can store private data on the server, retrieve it whenever necessary and share it between multiple connected clients of the same user\&. The data stored might be anything, as long as it is a valid XML\&. One typical usage is storing a bookmark of all user\(cqs conferences (XEP\-0048: Bookmarks)\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_privilege" .sp This module is an implementation of XEP\-0356: Privileged Entity\&. This extension allows components to have privileged access to other entity data (send messages on behalf of the server or on behalf of a user, get/set user roster, access presence information, etc\&.)\&. This may be used to write powerful external components, for example implementing an external PEP or MAM service\&. .sp By default a component does not have any privileged access\&. It is worth noting that the permissions grant access to the component to a specific data type for all users of the virtual host on which \fImod_privilege\fR is loaded\&. .sp Make sure you have a listener configured to connect your component\&. Check the section about listening ports for more information\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp Security issue: Privileged access gives components access to sensitive data, so permission should be granted carefully, only if you trust a component\&. .sp .5v .RE .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp This module is complementary to \fImod_delegation\fR, but can also be used separately\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBmessage\fR: \fIOptions\fR .RS 4 This option defines permissions for messages\&. By default no permissions are given\&. The \fIOptions\fR are: .PP \fBoutgoing\fR: \fIAccessName\fR .RS 4 The option defines an access rule for sending outgoing messages by the component\&. The default value is \fInone\fR\&. .RE .RE .PP \fBpresence\fR: \fIOptions\fR .RS 4 This option defines permissions for presences\&. By default no permissions are given\&. The \fIOptions\fR are: .PP \fBmanaged_entity\fR: \fIAccessName\fR .RS 4 An access rule that gives permissions to the component to receive server presences\&. The default value is \fInone\fR\&. .RE .PP \fBroster\fR: \fIAccessName\fR .RS 4 An access rule that gives permissions to the component to receive the presence of both the users and the contacts in their roster\&. The default value is \fInone\fR\&. .RE .RE .PP \fBroster\fR: \fIOptions\fR .RS 4 This option defines roster permissions\&. By default no permissions are given\&. The \fIOptions\fR are: .PP \fBboth\fR: \fIAccessName\fR .RS 4 Sets read/write access to a user\(cqs roster\&. The default value is \fInone\fR\&. .RE .PP \fBget\fR: \fIAccessName\fR .RS 4 Sets read access to a user\(cqs roster\&. The default value is \fInone\fR\&. .RE .PP \fBset\fR: \fIAccessName\fR .RS 4 Sets write access to a user\(cqs roster\&. The default value is \fInone\fR\&. .RE .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_privilege: roster: get: all presence: managed_entity: all message: outgoing: all \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_proxy65" .sp This module implements XEP\-0065: SOCKS5 Bytestreams\&. It allows ejabberd to act as a file transfer proxy between two XMPP clients\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 Defines an access rule for file transfer initiators\&. The default value is \fIall\fR\&. You may want to restrict access to the users of your server only, in order to avoid abusing your proxy by the users of remote servers\&. .RE .PP \fBauth_type\fR: \fIanonymous | plain\fR .RS 4 SOCKS5 authentication type\&. The default value is \fIanonymous\fR\&. If set to \fIplain\fR, ejabberd will use authentication backend as it would for SASL PLAIN\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhostname\fR: \fIHost\fR .RS 4 Defines a hostname offered by the proxy when establishing a session with clients\&. This is useful when you run the proxy behind a NAT\&. The keyword \fI@HOST@\fR is replaced with the virtual host name\&. The default is to use the value of \fIip\fR option\&. Examples: \fIproxy\&.mydomain\&.org\fR, \fI200\&.150\&.100\&.50\fR\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "proxy\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .PP \fBip\fR: \fIIPAddress\fR .RS 4 This option specifies which network interface to listen for\&. The default value is an IP address of the service\(cqs DNS name, or, if fails, \fI127\&.0\&.0\&.1\fR\&. .RE .PP \fBmax_connections\fR: \fIpos_integer() | infinity\fR .RS 4 Maximum number of active connections per file transfer initiator\&. The default value is \fIinfinity\fR\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 The value of the service name\&. This name is only visible in some clients that support XEP\-0030: Service Discovery\&. The default is "SOCKS5 Bytestreams"\&. .RE .PP \fBport\fR: \fI1\&.\&.65535\fR .RS 4 A port number to listen for incoming connections\&. The default value is \fI7777\fR\&. .RE .PP \fBram_db_type\fR: \fImnesia | redis | sql\fR .RS 4 Define the type of volatile (in\-memory) storage where the module will store room information\&. .RE .PP \fBrecbuf\fR: \fISize\fR .RS 4 A size of the buffer for incoming packets\&. If you define a shaper, set the value of this option to the size of the shaper in order to avoid traffic spikes in file transfers\&. The default value is \fI65536\fR bytes\&. .RE .PP \fBshaper\fR: \fIShaper\fR .RS 4 This option defines a shaper for the file transfer peers\&. A shaper with the maximum bandwidth will be selected\&. The default is \fInone\fR, i\&.e\&. no shaper\&. .RE .PP \fBsndbuf\fR: \fISize\fR .RS 4 A size of the buffer for outgoing packets\&. If you define a shaper, set the value of this option to the size of the shaper in order to avoid traffic spikes in file transfers\&. The default value is \fI65536\fR bytes\&. .RE .PP \fBvcard\fR: \fIvCard\fR .RS 4 A custom vCard of the service that will be displayed by some XMPP clients in Service Discovery\&. The value of \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp For example, the following XML representation of vCard: .sp .if n \{\ .RS 4 .\} .nf <vCard xmlns=\*(Aqvcard\-temp\*(Aq> <FN>Conferences</FN> <ADR> <WORK/> <STREET>Elm Street</STREET> </ADR> </vCard> .fi .if n \{\ .RE .\} .sp will be translated to: .sp .if n \{\ .RS 4 .\} .nf vcard: fn: Conferences adr: \- work: true street: Elm Street .fi .if n \{\ .RE .\} .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf acl: admin: user: admin@example\&.org proxy_users: server: example\&.org access_rules: proxy65_access: allow: proxy_users shaper_rules: proxy65_shaper: none: admin proxyrate: proxy_users shaper: proxyrate: 10240 modules: \&.\&.\&. mod_proxy65: host: proxy1\&.example\&.org name: "File Transfer Proxy" ip: 200\&.150\&.100\&.1 port: 7778 max_connections: 5 access: proxy65_access shaper: proxy65_shaper recbuf: 10240 sndbuf: 10240 \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_pubsub" .sp This module offers a service for XEP\-0060: Publish\-Subscribe\&. The functionality in \fImod_pubsub\fR can be extended using plugins\&. The plugin that implements PEP (XEP\-0163: Personal Eventing via Pubsub) is enabled in the default ejabberd configuration file, and it requires \fImod_caps\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess_createnode\fR: \fIAccessName\fR .RS 4 This option restricts which users are allowed to create pubsub nodes using \fIacl\fR and \fIaccess\fR\&. By default any account in the local ejabberd server is allowed to create pubsub nodes\&. The default value is: \fIall\fR\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBdefault_node_config\fR: \fIList of Key:Value\fR .RS 4 To override default node configuration, regardless of node plugin\&. Value is a list of key\-value definition\&. Node configuration still uses default configuration defined by node plugin, and overrides any items by value defined in this configurable list\&. .RE .PP \fBforce_node_config\fR: \fIList of Node and the list of its Key:Value\fR .RS 4 Define the configuration for given nodes\&. The default value is: \fI[]\fR\&. .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf force_node_config: ## Avoid buggy clients to make their bookmarks public storage:bookmarks: access_model: whitelist .fi .if n \{\ .RE .\} .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "pubsub\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .PP \fBignore_pep_from_offline\fR: \fIfalse | true\fR .RS 4 To specify whether or not we should get last published PEP items from users in our roster which are offline when we connect\&. Value is \fItrue\fR or \fIfalse\fR\&. If not defined, pubsub assumes true so we only get last items of online contacts\&. .RE .PP \fBlast_item_cache\fR: \fIfalse | true\fR .RS 4 To specify whether or not pubsub should cache last items\&. Value is \fItrue\fR or \fIfalse\fR\&. If not defined, pubsub does not cache last items\&. On systems with not so many nodes, caching last items speeds up pubsub and allows to raise user connection rate\&. The cost is memory usage, as every item is stored in memory\&. .RE .PP \fBmax_item_expire_node\fR: \fItimeout() | infinity\fR .RS 4 Specify the maximum item epiry time\&. Default value is: \fIinfinity\fR\&. .RE .PP \fBmax_items_node\fR: \fInon_neg_integer() | infinity\fR .RS 4 Define the maximum number of items that can be stored in a node\&. Default value is: \fI1000\fR\&. .RE .PP \fBmax_nodes_discoitems\fR: \fIpos_integer() | infinity\fR .RS 4 The maximum number of nodes to return in a discoitem response\&. The default value is: \fI100\fR\&. .RE .PP \fBmax_subscriptions_node\fR: \fIMaxSubs\fR .RS 4 Define the maximum number of subscriptions managed by a node\&. Default value is no limitation: \fIundefined\fR\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 The value of the service name\&. This name is only visible in some clients that support XEP\-0030: Service Discovery\&. The default is \fIvCard User Search\fR\&. .RE .PP \fBnodetree\fR: \fINodetree\fR .RS 4 To specify which nodetree to use\&. If not defined, the default pubsub nodetree is used: \fItree\fR\&. Only one nodetree can be used per host, and is shared by all node plugins\&. .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fItree\fR nodetree store node configuration and relations on the database\&. \fIflat\fR nodes are stored without any relationship, and \fIhometree\fR nodes can have child nodes\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIvirtual\fR nodetree does not store nodes on database\&. This saves resources on systems with tons of nodes\&. If using the \fIvirtual\fR nodetree, you can only enable those node plugins: \fI[flat, pep]\fR or \fI[flat]\fR; any other plugins configuration will not work\&. Also, all nodes will have the default configuration, and this can not be changed\&. Using \fIvirtual\fR nodetree requires to start from a clean database, it will not work if you used the default \fItree\fR nodetree before\&. .RE .RE .PP \fBpep_mapping\fR: \fIList of Key:Value\fR .RS 4 This allows to define a list of key\-value to choose defined node plugins on given PEP namespace\&. The following example will use \fInode_tune\fR instead of \fInode_pep\fR for every PEP node with the tune namespace: .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_pubsub: pep_mapping: http://jabber\&.org/protocol/tune: tune \&.\&.\&. .fi .if n \{\ .RE .\} .RE .PP \fBplugins\fR: \fI[Plugin, \&.\&.\&.]\fR .RS 4 To specify which pubsub node plugins to use\&. The first one in the list is used by default\&. If this option is not defined, the default plugins list is: \fI[flat]\fR\&. PubSub clients can define which plugin to use when creating a node: add \fItype=\*(Aqplugin\-name\fR\*(Aq attribute to the \fIcreate\fR stanza element\&. .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIflat\fR plugin handles the default behaviour and follows standard XEP\-0060 implementation\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} \fIpep\fR plugin adds extention to handle Personal Eventing Protocol (XEP\-0163) to the PubSub engine\&. Adding pep allows to handle PEP automatically\&. .RE .RE .PP \fBvcard\fR: \fIvCard\fR .RS 4 A custom vCard of the server that will be displayed by some XMPP clients in Service Discovery\&. The value of \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp The following XML representation of vCard: .sp .if n \{\ .RS 4 .\} .nf <vCard xmlns=\*(Aqvcard\-temp\*(Aq> <FN>PubSub Service</FN> <ADR> <WORK/> <STREET>Elm Street</STREET> </ADR> </vCard> .fi .if n \{\ .RE .\} .sp will be translated to: .sp .if n \{\ .RS 4 .\} .nf vcard: fn: PubSub Service adr: \- work: true street: Elm Street .fi .if n \{\ .RE .\} .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp Example of configuration that uses flat nodes as default, and allows use of flat, hometree and pep nodes: .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_pubsub: access_createnode: pubsub_createnode max_subscriptions_node: 100 default_node_config: notification_type: normal notify_retract: false max_items: 4 plugins: \- flat \- pep \&.\&.\&. .fi .if n \{\ .RE .\} .sp Using relational database requires using mod_pubsub with db_type \fIsql\fR\&. Only flat, hometree and pep plugins supports SQL\&. The following example shows previous configuration with SQL usage: .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_pubsub: db_type: sql access_createnode: pubsub_createnode ignore_pep_from_offline: true last_item_cache: false plugins: \- flat \- pep \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_push" .sp This module implements the XMPP server\(cqs part of the push notification solution specified in XEP\-0357: Push Notifications\&. It does not generate, for example, APNS or FCM notifications directly\&. Instead, it\(cqs designed to work with so\-called "app servers" operated by third\-party vendors of mobile apps\&. Those app servers will usually trigger notification delivery to the user\(cqs mobile device using platform\-dependant backend services such as FCM or APNS\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBinclude_body\fR: \fItrue | false | Text\fR .RS 4 If this option is set to \fItrue\fR, the message text is included with push notifications generated for incoming messages with a body\&. The option can instead be set to a static \fIText\fR, in which case the specified text will be included in place of the actual message body\&. This can be useful to signal the app server whether the notification was triggered by a message with body (as opposed to other types of traffic) without leaking actual message contents\&. The default value is "New message"\&. .RE .PP \fBinclude_sender\fR: \fItrue | false\fR .RS 4 If this option is set to \fItrue\fR, the sender\(cqs JID is included with push notifications generated for incoming messages with a body\&. The default value is \fIfalse\fR\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_push_keepalive" .sp This module tries to keep the stream management session (see \fImod_stream_mgmt\fR) of a disconnected mobile client alive if the client enabled push notifications for that session\&. However, the normal session resumption timeout is restored once a push notification is issued, so the session will be closed if the client doesn\(cqt respond to push notifications\&. .sp The module depends on \fImod_push\fR\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBresume_timeout\fR: \fItimeout()\fR .RS 4 This option specifies the period of time until the session of a disconnected push client times out\&. This timeout is only in effect as long as no push notification is issued\&. Once that happened, the resumption timeout configured for \fImod_stream_mgmt\fR is restored\&. The default value is \fI72\fR hours\&. .RE .PP \fBwake_on_start\fR: \fItrue | false\fR .RS 4 If this option is set to \fItrue\fR, notifications are generated for \fBall\fR registered push clients during server startup\&. This option should not be enabled on servers with many push clients as it can generate significant load on the involved push services and the server itself\&. The default value is \fIfalse\fR\&. .RE .PP \fBwake_on_timeout\fR: \fItrue | false\fR .RS 4 If this option is set to \fItrue\fR, a notification is generated shortly before the session would time out as per the \fIresume_timeout\fR option\&. The default value is \fItrue\fR\&. .RE .RE .SS "mod_register" .sp This module adds support for XEP\-0077: In\-Band Registration\&. This protocol enables end users to use an XMPP client to: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Register a new account on the server\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Change the password from an existing account on the server\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Delete an existing account on the server\&. .RE .sp This module reads also the top\-level \fIregistration_timeout\fR option defined globally for the server, so please check that option documentation too\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 Specify rules to restrict what usernames can be registered\&. If a rule returns \fIdeny\fR on the requested username, registration of that user name is denied\&. There are no restrictions by default\&. .RE .PP \fBaccess_from\fR: \fIAccessName\fR .RS 4 By default, \fIejabberd\fR doesn\(cqt allow to register new accounts from s2s or existing c2s sessions\&. You can change it by defining access rule in this option\&. Use with care: allowing registration from s2s leads to uncontrolled massive accounts creation by rogue users\&. .RE .PP \fBaccess_remove\fR: \fIAccessName\fR .RS 4 Specify rules to restrict access for user unregistration\&. By default any user is able to unregister their account\&. .RE .PP \fBallow_modules\fR: \fIall | [Module, \&.\&.\&.]\fR .RS 4 List of modules that can register accounts, or \fIall\fR\&. The default value is \fIall\fR, which is equivalent to something like \fI[mod_register, mod_register_web]\fR\&. .RE .PP \fBcaptcha_protected\fR: \fItrue | false\fR .RS 4 Protect registrations with CAPTCHA\&. The default is \fIfalse\fR\&. .RE .PP \fBip_access\fR: \fIAccessName\fR .RS 4 Define rules to allow or deny account registration depending on the IP address of the XMPP client\&. The \fIAccessName\fR should be of type \fIip\fR\&. The default value is \fIall\fR\&. .RE .PP \fBpassword_strength\fR: \fIEntropy\fR .RS 4 This option sets the minimum Shannon entropy for passwords\&. The value \fIEntropy\fR is a number of bits of entropy\&. The recommended minimum is 32 bits\&. The default is \fI0\fR, i\&.e\&. no checks are performed\&. .RE .PP \fBredirect_url\fR: \fIURL\fR .RS 4 This option enables registration redirection as described in XEP\-0077: In\-Band Registration: Redirection\&. .RE .PP \fBregistration_watchers\fR: \fI[JID, \&.\&.\&.]\fR .RS 4 This option defines a list of JIDs which will be notified each time a new account is registered\&. .RE .PP \fBwelcome_message\fR: \fI{subject: Subject, body: Body}\fR .RS 4 Set a welcome message that is sent to each newly registered account\&. The message will have subject \fISubject\fR and text \fIBody\fR\&. .RE .RE .SS "mod_register_web" .sp This module provides a web page where users can: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Register a new account on the server\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Change the password from an existing account on the server\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Unregister an existing account on the server\&. .RE .sp This module supports CAPTCHA to register a new account\&. To enable this feature, configure the top\-level \fIcaptcha_cmd\fR and top\-level \fIcaptcha_url\fR options\&. .sp As an example usage, the users of the host \fIlocalhost\fR can visit the page: \fIhttps://localhost:5280/register/\fR It is important to include the last / character in the URL, otherwise the subpages URL will be incorrect\&. .sp This module is enabled in \fIlisten\fR → \fIejabberd_http\fR → request_handlers, no need to enable in \fImodules\fR\&. The module depends on \fImod_register\fR where all the configuration is performed\&. .sp The module has no options\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf listen: \- port: 5280 module: ejabberd_http request_handlers: /register: mod_register_web modules: mod_register: {} .fi .if n \{\ .RE .\} .RE .SS "mod_roster" .sp This module implements roster management as defined in RFC6121 Section 2\&. The module also adds support for XEP\-0237: Roster Versioning\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 This option can be configured to specify rules to restrict roster management\&. If the rule returns \fIdeny\fR on the requested user name, that user cannot modify their personal roster, i\&.e\&. they cannot add/remove/modify contacts or send presence subscriptions\&. The default value is \fIall\fR, i\&.e\&. no restrictions\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBstore_current_id\fR: \fItrue | false\fR .RS 4 If this option is set to \fItrue\fR, the current roster version number is stored on the database\&. If set to \fIfalse\fR, the roster version number is calculated on the fly each time\&. Enabling this option reduces the load for both ejabberd and the database\&. This option does not affect the client in any way\&. This option is only useful if option \fIversioning\fR is set to \fItrue\fR\&. The default value is \fIfalse\fR\&. IMPORTANT: if you use \fImod_shared_roster\fR or \fImod_shared_roster_ldap\fR, you must set the value of the option to \fIfalse\fR\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .PP \fBversioning\fR: \fItrue | false\fR .RS 4 Enables/disables Roster Versioning\&. The default value is \fIfalse\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_roster: versioning: true store_current_id: false \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_s2s_dialback" .sp The module adds support for XEP\-0220: Server Dialback to provide server identity verification based on DNS\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp DNS\-based verification is vulnerable to DNS cache poisoning, so modern servers rely on verification based on PKIX certificates\&. Thus this module is only recommended for backward compatibility with servers running outdated software or non\-TLS servers, or those with invalid certificates (as long as you accept the risks, e\&.g\&. you assume that the remote server has an invalid certificate due to poor administration and not because it\(cqs compromised)\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 An access rule that can be used to restrict dialback for some servers\&. The default value is \fIall\fR\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_s2s_dialback: access: allow: server: legacy\&.domain\&.tld server: invalid\-cert\&.example\&.org deny: all \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_service_log" .sp This module forwards copies of all stanzas to remote XMPP servers or components\&. Every stanza is encapsulated into <forwarded/> element as described in XEP\-0297: Stanza Forwarding\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBloggers\fR: \fI[Domain, \&.\&.\&.]\fR .RS 4 A list of servers or connected components to which stanzas will be forwarded\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_service_log: loggers: \- xmpp\-server\&.tld \- component\&.domain\&.tld \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_shared_roster" .sp This module enables you to create shared roster groups: groups of accounts that can see members from (other) groups in their rosters\&. .sp The big advantages of this feature are that end users do not need to manually add all users to their rosters, and that they cannot permanently delete users from the shared roster groups\&. A shared roster group can have members from any XMPP server, but the presence will only be available from and to members of the same virtual host where the group is created\&. It still allows the users to have / add their own contacts, as it does not replace the standard roster\&. Instead, the shared roster contacts are merged to the relevant users at retrieval time\&. The standard user rosters thus stay unmodified\&. .sp Shared roster groups can be edited via the Web Admin, and some API commands called \fIsrg_*\fR\&. Each group has a unique name and those parameters: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Label: Used in the rosters where this group is displayed\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Description: of the group, which has no effect\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Members: A list of JIDs of group members, entered one per line in the Web Admin\&. The special member directive \fI@all@\fR represents all the registered users in the virtual host; which is only recommended for a small server with just a few hundred users\&. The special member directive \fI@online@\fR represents the online users in the virtual host\&. With those two directives, the actual list of members in those shared rosters is generated dynamically at retrieval time\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Displayed: A list of groups that will be in the rosters of this group\(cqs members\&. A group of other vhost can be identified with \fIgroupid@vhost\fR\&. .RE .sp This module depends on \fImod_roster\fR\&. If not enabled, roster queries will return 503 errors\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql\fR .RS 4 Define the type of storage where the module will create the tables and store user information\&. The default is the storage defined by the top\-level \fIdefault_db\fR option, or \fImnesia\fR if omitted\&. If \fIsql\fR value is defined, make sure you have defined the database\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExamples:\fR .RS 4 .sp Take the case of a computer club that wants all its members seeing each other in their rosters\&. To achieve this, they need to create a shared roster group similar to this one: .sp .if n \{\ .RS 4 .\} .nf Name: club_members Label: Club Members Description: Members from the computer club Members: member1@example\&.org, member2@example\&.org, member3@example\&.org Displayed Groups: club_members .fi .if n \{\ .RE .\} .sp In another case we have a company which has three divisions: Management, Marketing and Sales\&. All group members should see all other members in their rosters\&. Additionally, all managers should have all marketing and sales people in their roster\&. Simultaneously, all marketeers and the whole sales team should see all managers\&. This scenario can be achieved by creating shared roster groups as shown in the following lists: .sp .if n \{\ .RS 4 .\} .nf First list: Name: management Label: Management Description: Management Members: manager1@example\&.org, manager2@example\&.org Displayed: management, marketing, sales Second list: Name: marketing Label: Marketing Description: Marketing Members: marketeer1@example\&.org, marketeer2@example\&.org, marketeer3@example\&.org Displayed: management, marketing Third list: Name: sales Label: Sales Description: Sales Members: salesman1@example\&.org, salesman2@example\&.org, salesman3@example\&.org Displayed: management, sales .fi .if n \{\ .RE .\} .RE .SS "mod_shared_roster_ldap" .sp This module lets the server administrator automatically populate users\*(Aq rosters (contact lists) with entries based on users and groups defined in an LDAP\-based directory\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp \fImod_shared_roster_ldap\fR depends on \fImod_roster\fR being enabled\&. Roster queries will return \fI503\fR errors if \fImod_roster\fR is not enabled\&. .sp .5v .RE .sp The module accepts many configuration options\&. Some of them, if unspecified, default to the values specified for the top level of configuration\&. This lets you avoid specifying, for example, the bind password in multiple places\&. .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Filters: \fIldap_rfilter\fR, \fIldap_ufilter\fR, \fIldap_gfilter\fR, \fIldap_filter\fR\&. These options specify LDAP filters used to query for shared roster information\&. All of them are run against the ldap_base\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Attributes: \fIldap_groupattr\fR, \fIldap_groupdesc\fR, \fIldap_memberattr\fR, \fIldap_userdesc\fR, \fIldap_useruid\fR\&. These options specify the names of the attributes which hold interesting data in the entries returned by running filters specified with the filter options\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Control parameters: \fIldap_auth_check\fR, \fIldap_group_cache_validity\fR, \fIldap_memberattr_format\fR, \fIldap_memberattr_format_re\fR, \fIldap_user_cache_validity\fR\&. These parameters control the behaviour of the module\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Connection parameters: The module also accepts the connection parameters, all of which default to the top\-level parameter of the same name, if unspecified\&. See LDAP Connection section for more information about them\&. .RE .sp Check also the Configuration examples section to get details about retrieving the roster, and configuration examples including Flat DIT and Deep DIT\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBldap_auth_check\fR: \fItrue | false\fR .RS 4 Whether the module should check (via the ejabberd authentication subsystem) for existence of each user in the shared LDAP roster\&. Set to \fIfalse\fR if you want to disable the check\&. Default value is \fItrue\fR\&. .RE .PP \fBldap_backups\fR .RS 4 Same as top\-level \fIldap_backups\fR option, but applied to this module only\&. .RE .PP \fBldap_base\fR .RS 4 Same as top\-level \fIldap_base\fR option, but applied to this module only\&. .RE .PP \fBldap_deref_aliases\fR .RS 4 Same as top\-level \fIldap_deref_aliases\fR option, but applied to this module only\&. .RE .PP \fBldap_encrypt\fR .RS 4 Same as top\-level \fIldap_encrypt\fR option, but applied to this module only\&. .RE .PP \fBldap_filter\fR .RS 4 Additional filter which is AND\-ed together with "User Filter" and "Group Filter"\&. For more information check the LDAP Filters section\&. .RE .PP \fBldap_gfilter\fR .RS 4 "Group Filter", used when retrieving human\-readable name (a\&.k\&.a\&. "Display Name") and the members of a group\&. See also the parameters \fIldap_groupattr\fR, \fIldap_groupdesc\fR and \fIldap_memberattr\fR\&. If unspecified, defaults to the top\-level parameter of the same name\&. If that one also is unspecified, then the filter is constructed exactly like "User Filter"\&. .RE .PP \fBldap_groupattr\fR .RS 4 The name of the attribute that holds the group name, and that is used to differentiate between them\&. Retrieved from results of the "Roster Filter" and "Group Filter"\&. Defaults to \fIcn\fR\&. .RE .PP \fBldap_groupdesc\fR .RS 4 The name of the attribute which holds the human\-readable group name in the objects you use to represent groups\&. Retrieved from results of the "Group Filter"\&. Defaults to whatever \fIldap_groupattr\fR is set\&. .RE .PP \fBldap_memberattr\fR .RS 4 The name of the attribute which holds the IDs of the members of a group\&. Retrieved from results of the "Group Filter"\&. Defaults to \fImemberUid\fR\&. The name of the attribute differs depending on the objectClass you use for your group objects, for example: \fIposixGroup\fR → \fImemberUid\fR; \fIgroupOfNames\fR → \fImember\fR; \fIgroupOfUniqueNames\fR → \fIuniqueMember\fR\&. .RE .PP \fBldap_memberattr_format\fR .RS 4 A globbing format for extracting user ID from the value of the attribute named by \fIldap_memberattr\fR\&. Defaults to \fI%u\fR, which means that the whole value is the member ID\&. If you change it to something different, you may also need to specify the User and Group Filters manually; see section Filters\&. .RE .PP \fBldap_memberattr_format_re\fR .RS 4 A regex for extracting user ID from the value of the attribute named by \fIldap_memberattr\fR\&. Check the LDAP Control Parameters section\&. .RE .PP \fBldap_password\fR .RS 4 Same as top\-level \fIldap_password\fR option, but applied to this module only\&. .RE .PP \fBldap_port\fR .RS 4 Same as top\-level \fIldap_port\fR option, but applied to this module only\&. .RE .PP \fBldap_rfilter\fR .RS 4 So called "Roster Filter"\&. Used to find names of all "shared roster" groups\&. See also the \fIldap_groupattr\fR parameter\&. If unspecified, defaults to the top\-level parameter of the same name\&. You must specify it in some place in the configuration, there is no default\&. .RE .PP \fBldap_rootdn\fR .RS 4 Same as top\-level \fIldap_rootdn\fR option, but applied to this module only\&. .RE .PP \fBldap_servers\fR .RS 4 Same as top\-level \fIldap_servers\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_cacertfile\fR .RS 4 Same as top\-level \fIldap_tls_cacertfile\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_certfile\fR .RS 4 Same as top\-level \fIldap_tls_certfile\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_depth\fR .RS 4 Same as top\-level \fIldap_tls_depth\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_verify\fR .RS 4 Same as top\-level \fIldap_tls_verify\fR option, but applied to this module only\&. .RE .PP \fBldap_ufilter\fR .RS 4 "User Filter", used for retrieving the human\-readable name of roster entries (usually full names of people in the roster)\&. See also the parameters \fIldap_userdesc\fR and \fIldap_useruid\fR\&. For more information check the LDAP Filters section\&. .RE .PP \fBldap_uids\fR .RS 4 Same as top\-level \fIldap_uids\fR option, but applied to this module only\&. .RE .PP \fBldap_userdesc\fR .RS 4 The name of the attribute which holds the human\-readable user name\&. Retrieved from results of the "User Filter"\&. Defaults to \fIcn\fR\&. .RE .PP \fBldap_userjidattr\fR .RS 4 The name of the attribute which is used to map user id to XMPP jid\&. If not specified (and that is default value of this option), user jid will be created from user id and this module host\&. .RE .PP \fBldap_useruid\fR .RS 4 The name of the attribute which holds the ID of a roster item\&. Value of this attribute in the roster item objects needs to match the ID retrieved from the \fIldap_memberattr\fR attribute of a group object\&. Retrieved from results of the "User Filter"\&. Defaults to \fIcn\fR\&. .RE .PP \fBuse_cache\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_sic" .sp This module adds support for XEP\-0279: Server IP Check\&. This protocol enables a client to discover its external IP address\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBWarning\fR .ps -1 .br .sp The protocol extension is deferred and seems like there are no clients supporting it, so using this module is not recommended and, furthermore, the module might be removed in the future\&. .sp .5v .RE .sp The module has no options\&. .SS "mod_sip" .sp This module adds SIP proxy/registrar support for the corresponding virtual host\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp It is not enough to just load this module\&. You should also configure listeners and DNS records properly\&. For details see the section about the ejabberd_sip listen module in the ejabberd Documentation\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBalways_record_route\fR: \fItrue | false\fR .RS 4 Always insert "Record\-Route" header into SIP messages\&. This approach allows to bypass NATs/firewalls a bit more easily\&. The default value is \fItrue\fR\&. .RE .PP \fBflow_timeout_tcp\fR: \fItimeout()\fR .RS 4 The option sets a keep\-alive timer for SIP outbound TCP connections\&. The default value is \fI2\fR minutes\&. .RE .PP \fBflow_timeout_udp\fR: \fItimeout()\fR .RS 4 The options sets a keep\-alive timer for SIP outbound UDP connections\&. The default value is \fI29\fR seconds\&. .RE .PP \fBrecord_route\fR: \fIURI\fR .RS 4 When the option \fIalways_record_route\fR is set to \fItrue\fR or when SIP outbound is utilized, ejabberd inserts "Record\-Route" header field with this \fIURI\fR into a SIP message\&. The default is a SIP URI constructed from the virtual host on which the module is loaded\&. .RE .PP \fBroutes\fR: \fI[URI, \&.\&.\&.]\fR .RS 4 You can set a list of SIP URIs of routes pointing to this SIP proxy server\&. The default is a list containing a single SIP URI constructed from the virtual host on which the module is loaded\&. .RE .PP \fBvia\fR: \fI[URI, \&.\&.\&.]\fR .RS 4 A list to construct "Via" headers for inserting them into outgoing SIP messages\&. This is useful if you\(cqre running your SIP proxy in a non\-standard network topology\&. Every \fIURI\fR element in the list must be in the form of "scheme://host:port", where "transport" must be \fItls\fR, \fItcp\fR, or \fIudp\fR, "host" must be a domain name or an IP address and "port" must be an internet port number\&. Note that all parts of the \fIURI\fR are mandatory (e\&.g\&. you cannot omit "port" or "scheme")\&. .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBExample:\fR .RS 4 .sp .if n \{\ .RS 4 .\} .nf modules: \&.\&.\&. mod_sip: always_record_route: false record_route: "sip:example\&.com;lr" routes: \- "sip:example\&.com;lr" \- "sip:sip\&.example\&.com;lr" flow_timeout_udp: 30 sec flow_timeout_tcp: 1 min via: \- tls://sip\-tls\&.example\&.com:5061 \- tcp://sip\-tcp\&.example\&.com:5060 \- udp://sip\-udp\&.example\&.com:5060 \&.\&.\&. .fi .if n \{\ .RE .\} .RE .SS "mod_stats" .sp This module adds support for XEP\-0039: Statistics Gathering\&. This protocol allows you to retrieve the following statistics from your ejabberd server: .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Total number of registered users on the current virtual host (users/total)\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Total number of registered users on all virtual hosts (users/all\-hosts/total)\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Total number of online users on the current virtual host (users/online)\&. .RE .sp .RS 4 .ie n \{\ \h'-04'\(bu\h'+03'\c .\} .el \{\ .sp -1 .IP \(bu 2.3 .\} Total number of online users on all virtual hosts (users/all\-hosts/online)\&. .RE .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp The protocol extension is deferred and seems like even a few clients that were supporting it are now abandoned\&. So using this module makes very little sense\&. .sp .5v .RE .sp The module has no options\&. .SS "mod_stream_mgmt" .sp This module adds support for XEP\-0198: Stream Management\&. This protocol allows active management of an XML stream between two XMPP entities, including features for stanza acknowledgements and stream resumption\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBack_timeout\fR: \fItimeout()\fR .RS 4 A time to wait for stanza acknowledgements\&. Setting it to \fIinfinity\fR effectively disables the timeout\&. The default value is \fI1\fR minute\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. The default value is \fI48 hours\fR\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBmax_ack_queue\fR: \fISize\fR .RS 4 This option specifies the maximum number of unacknowledged stanzas queued for possible retransmission\&. When the limit is exceeded, the client session is terminated\&. The allowed values are positive integers and \fIinfinity\fR\&. You should be careful when setting this value as it should not be set too low, otherwise, you could kill sessions in a loop, before they get the chance to finish proper session initiation\&. It should definitely be set higher that the size of the offline queue (for example at least 3 times the value of the max offline queue and never lower than \fI1000\fR)\&. The default value is \fI5000\fR\&. .RE .PP \fBmax_resume_timeout\fR: \fItimeout()\fR .RS 4 A client may specify the period of time until a session times out if the connection is lost\&. During this period of time, the client may resume its session\&. This option limits the period of time a client is permitted to request\&. It must be set to a timeout equal to or larger than the default \fIresume_timeout\fR\&. By default, it is set to the same value as the \fIresume_timeout\fR option\&. .RE .PP \fBqueue_type\fR: \fIram | file\fR .RS 4 Same as top\-level \fIqueue_type\fR option, but applied to this module only\&. .RE .PP \fBresend_on_timeout\fR: \fItrue | false | if_offline\fR .RS 4 If this option is set to \fItrue\fR, any message stanzas that weren\(cqt acknowledged by the client will be resent on session timeout\&. This behavior might often be desired, but could have unexpected results under certain circumstances\&. For example, a message that was sent to two resources might get resent to one of them if the other one timed out\&. Therefore, the default value for this option is \fIfalse\fR, which tells ejabberd to generate an error message instead\&. As an alternative, the option may be set to \fIif_offline\fR\&. In this case, unacknowledged messages are resent only if no other resource is online when the session times out\&. Otherwise, error messages are generated\&. .RE .PP \fBresume_timeout\fR: \fItimeout()\fR .RS 4 This option configures the (default) period of time until a session times out if the connection is lost\&. During this period of time, a client may resume its session\&. Note that the client may request a different timeout value, see the \fImax_resume_timeout\fR option\&. Setting it to \fI0\fR effectively disables session resumption\&. The default value is \fI5\fR minutes\&. .RE .RE .SS "mod_stun_disco" .sp This module allows XMPP clients to discover STUN/TURN services and to obtain temporary credentials for using them as per XEP\-0215: External Service Discovery\&. This module is included in ejabberd since version 20\&.04\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBaccess\fR: \fIAccessName\fR .RS 4 This option defines which access rule will be used to control who is allowed to discover STUN/TURN services and to request temporary credentials\&. The default value is \fIlocal\fR\&. .RE .PP \fBcredentials_lifetime\fR: \fItimeout()\fR .RS 4 The lifetime of temporary credentials offered to clients\&. If ejabberd\(cqs built\-in TURN service is used, TURN relays allocated using temporary credentials will be terminated shortly after the credentials expired\&. The default value is \fI12 hours\fR\&. Note that restarting the ejabberd node invalidates any temporary credentials offered before the restart unless a \fIsecret\fR is specified (see below)\&. .RE .PP \fBoffer_local_services\fR: \fItrue | false\fR .RS 4 This option specifies whether local STUN/TURN services configured as ejabberd listeners should be announced automatically\&. Note that this will not include TLS\-enabled services, which must be configured manually using the \fIservices\fR option (see below)\&. For non\-anonymous TURN services, temporary credentials will be offered to the client\&. The default value is \fItrue\fR\&. .RE .PP \fBsecret\fR: \fIText\fR .RS 4 The secret used for generating temporary credentials\&. If this option isn\(cqt specified, a secret will be auto\-generated\&. However, a secret must be specified explicitly if non\-anonymous TURN services running on other ejabberd nodes and/or external TURN \fIservices\fR are configured\&. Also note that auto\-generated secrets are lost when the node is restarted, which invalidates any credentials offered before the restart\&. Therefore, it\(cqs recommended to explicitly specify a secret if clients cache retrieved credentials (for later use) across service restarts\&. .RE .PP \fBservices\fR: \fI[Service, \&.\&.\&.]\fR .RS 4 The list of services offered to clients\&. This list can include STUN/TURN services running on any ejabberd node and/or external services\&. However, if any listed TURN service not running on the local ejabberd node requires authentication, a \fIsecret\fR must be specified explicitly, and must be shared with that service\&. This will only work with ejabberd\(cqs built\-in STUN/TURN server and with external servers that support the same REST API For Access To TURN Services\&. Unless the \fIoffer_local_services\fR is set to \fIfalse\fR, the explicitly listed services will be offered in addition to those announced automatically\&. .PP \fBhost\fR: \fIHost\fR .RS 4 The hostname or IP address the STUN/TURN service is listening on\&. For non\-TLS services, it\(cqs recommended to specify an IP address (to avoid additional DNS lookup latency on the client side)\&. For TLS services, the hostname (or IP address) should match the certificate\&. Specifying the \fIhost\fR option is mandatory\&. .RE .PP \fBport\fR: \fI1\&.\&.65535\fR .RS 4 The port number the STUN/TURN service is listening on\&. The default port number is 3478 for non\-TLS services and 5349 for TLS services\&. .RE .PP \fBrestricted\fR: \fItrue | false\fR .RS 4 This option determines whether temporary credentials for accessing the service are offered\&. The default is \fIfalse\fR for STUN/STUNS services and \fItrue\fR for TURN/TURNS services\&. .RE .PP \fBtransport\fR: \fItcp | udp\fR .RS 4 The transport protocol supported by the service\&. The default is \fIudp\fR for non\-TLS services and \fItcp\fR for TLS services\&. .RE .PP \fBtype\fR: \fIstun | turn | stuns | turns\fR .RS 4 The type of service\&. Must be \fIstun\fR or \fIturn\fR for non\-TLS services, \fIstuns\fR or \fIturns\fR for TLS services\&. The default type is \fIstun\fR\&. .RE .sp \fBExample\fR: .sp .if n \{\ .RS 4 .\} .nf services: \- host: 203\&.0\&.113\&.3 port: 3478 type: stun transport: udp restricted: false \- host: 203\&.0\&.113\&.3 port: 3478 type: turn transport: udp restricted: true \- host: 2001:db8::3 port: 3478 type: stun transport: udp restricted: false \- host: 2001:db8::3 port: 3478 type: turn transport: udp restricted: true \- host: server\&.example\&.com port: 5349 type: turns transport: tcp restricted: true .fi .if n \{\ .RE .\} .RE .RE .SS "mod_time" .sp This module adds support for XEP\-0202: Entity Time\&. In other words, the module reports server\(cqs system time\&. .sp The module has no options\&. .SS "mod_vcard" .sp This module allows end users to store and retrieve their vCard, and to retrieve other users vCards, as defined in XEP\-0054: vcard\-temp\&. The module also implements an uncomplicated Jabber User Directory based on the vCards of these users\&. Moreover, it enables the server to send its vCard when queried\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBallow_return_all\fR: \fItrue | false\fR .RS 4 This option enables you to specify if search operations with empty input fields should return all users who added some information to their vCard\&. The default value is \fIfalse\fR\&. .RE .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBdb_type\fR: \fImnesia | sql | ldap\fR .RS 4 Same as top\-level \fIdefault_db\fR option, but applied to this module only\&. .RE .PP \fBhost\fR .RS 4 Deprecated\&. Use \fIhosts\fR instead\&. .RE .PP \fBhosts\fR: \fI[Host, \&.\&.\&.]\fR .RS 4 This option defines the Jabber IDs of the service\&. If the \fIhosts\fR option is not specified, the only Jabber ID will be the hostname of the virtual host with the prefix "vjud\&."\&. The keyword \fI@HOST@\fR is replaced with the real virtual host name\&. .RE .PP \fBmatches\fR: \fIpos_integer() | infinity\fR .RS 4 With this option, the number of reported search results can be limited\&. If the option\(cqs value is set to \fIinfinity\fR, all search results are reported\&. The default value is \fI30\fR\&. .RE .PP \fBname\fR: \fIName\fR .RS 4 The value of the service name\&. This name is only visible in some clients that support XEP\-0030: Service Discovery\&. The default is \fIvCard User Search\fR\&. .RE .PP \fBsearch\fR: \fItrue | false\fR .RS 4 This option specifies whether the search functionality is enabled or not\&. If disabled, the options \fIhosts\fR, \fIname\fR and \fIvcard\fR will be ignored and the Jabber User Directory service will not appear in the Service Discovery item list\&. The default value is \fIfalse\fR\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .PP \fBvcard\fR: \fIvCard\fR .RS 4 A custom vCard of the server that will be displayed by some XMPP clients in Service Discovery\&. The value of \fIvCard\fR is a YAML map constructed from an XML representation of vCard\&. Since the representation has no attributes, the mapping is straightforward\&. .sp For example, the following XML representation of vCard: .sp .if n \{\ .RS 4 .\} .nf <vCard xmlns=\*(Aqvcard\-temp\*(Aq> <FN>Conferences</FN> <ADR> <WORK/> <STREET>Elm Street</STREET> </ADR> </vCard> .fi .if n \{\ .RE .\} .sp will be translated to: .sp .if n \{\ .RS 4 .\} .nf vcard: fn: Conferences adr: \- work: true street: Elm Street .fi .if n \{\ .RE .\} .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options for ldap backend:\fR .RS 4 .PP \fBldap_backups\fR .RS 4 Same as top\-level \fIldap_backups\fR option, but applied to this module only\&. .RE .PP \fBldap_base\fR .RS 4 Same as top\-level \fIldap_base\fR option, but applied to this module only\&. .RE .PP \fBldap_deref_aliases\fR .RS 4 Same as top\-level \fIldap_deref_aliases\fR option, but applied to this module only\&. .RE .PP \fBldap_encrypt\fR .RS 4 Same as top\-level \fIldap_encrypt\fR option, but applied to this module only\&. .RE .PP \fBldap_filter\fR .RS 4 Same as top\-level \fIldap_filter\fR option, but applied to this module only\&. .RE .PP \fBldap_password\fR .RS 4 Same as top\-level \fIldap_password\fR option, but applied to this module only\&. .RE .PP \fBldap_port\fR .RS 4 Same as top\-level \fIldap_port\fR option, but applied to this module only\&. .RE .PP \fBldap_rootdn\fR .RS 4 Same as top\-level \fIldap_rootdn\fR option, but applied to this module only\&. .RE .PP \fBldap_search_fields\fR: \fI{Name: Attribute, \&.\&.\&.}\fR .RS 4 This option defines the search form and the LDAP attributes to search within\&. \fIName\fR is the name of a search form field which will be automatically translated by using the translation files (see \fImsgs/*\&.msg\fR for available words)\&. \fIAttribute\fR is the LDAP attribute or the pattern \fI%u\fR\&. .sp The default is: .sp .if n \{\ .RS 4 .\} .nf User: "%u" "Full Name": displayName "Given Name": givenName "Middle Name": initials "Family Name": sn Nickname: "%u" Birthday: birthDay Country: c City: l Email: mail "Organization Name": o "Organization Unit": ou .fi .if n \{\ .RE .\} .RE .PP \fBldap_search_reported\fR: \fI{SearchField: VcardField}, \&.\&.\&.}\fR .RS 4 This option defines which search fields should be reported\&. \fISearchField\fR is the name of a search form field which will be automatically translated by using the translation files (see \fImsgs/*\&.msg\fR for available words)\&. \fIVcardField\fR is the vCard field name defined in the \fIldap_vcard_map\fR option\&. .sp The default is: .sp .if n \{\ .RS 4 .\} .nf "Full Name": FN "Given Name": FIRST "Middle Name": MIDDLE "Family Name": LAST "Nickname": NICKNAME "Birthday": BDAY "Country": CTRY "City": LOCALITY "Email": EMAIL "Organization Name": ORGNAME "Organization Unit": ORGUNIT .fi .if n \{\ .RE .\} .RE .PP \fBldap_servers\fR .RS 4 Same as top\-level \fIldap_servers\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_cacertfile\fR .RS 4 Same as top\-level \fIldap_tls_cacertfile\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_certfile\fR .RS 4 Same as top\-level \fIldap_tls_certfile\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_depth\fR .RS 4 Same as top\-level \fIldap_tls_depth\fR option, but applied to this module only\&. .RE .PP \fBldap_tls_verify\fR .RS 4 Same as top\-level \fIldap_tls_verify\fR option, but applied to this module only\&. .RE .PP \fBldap_uids\fR .RS 4 Same as top\-level \fIldap_uids\fR option, but applied to this module only\&. .RE .PP \fBldap_vcard_map\fR: \fI{Name: {Pattern, LDAPattributes}, \&.\&.\&.}\fR .RS 4 With this option you can set the table that maps LDAP attributes to vCard fields\&. \fIName\fR is the type name of the vCard as defined in RFC 2426\&. \fIPattern\fR is a string which contains pattern variables \fI%u\fR, \fI%d\fR or \fI%s\fR\&. \fILDAPattributes\fR is the list containing LDAP attributes\&. The pattern variables \fI%s\fR will be sequentially replaced with the values of LDAP attributes from \fIList_of_LDAP_attributes\fR, \fI%u\fR will be replaced with the user part of a JID, and \fI%d\fR will be replaced with the domain part of a JID\&. .sp The default is: .sp .if n \{\ .RS 4 .\} .nf NICKNAME: {"%u": []} FN: {"%s": [displayName]} LAST: {"%s": [sn]} FIRST: {"%s": [givenName]} MIDDLE: {"%s": [initials]} ORGNAME: {"%s": [o]} ORGUNIT: {"%s": [ou]} CTRY: {"%s": [c]} LOCALITY: {"%s": [l]} STREET: {"%s": [street]} REGION: {"%s": [st]} PCODE: {"%s": [postalCode]} TITLE: {"%s": [title]} URL: {"%s": [labeleduri]} DESC: {"%s": [description]} TEL: {"%s": [telephoneNumber]} EMAIL: {"%s": [mail]} BDAY: {"%s": [birthDay]} ROLE: {"%s": [employeeType]} PHOTO: {"%s": [jpegPhoto]} .fi .if n \{\ .RE .\} .RE .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options for mnesia backend:\fR .RS 4 .PP \fBsearch_all_hosts\fR: \fItrue | false\fR .RS 4 Whether to perform search on all virtual hosts or not\&. The default value is \fItrue\fR\&. .RE .RE .SS "mod_vcard_xupdate" .sp The user\(cqs client can store an avatar in the user vCard\&. The vCard\-Based Avatars protocol (XEP\-0153) provides a method for clients to inform the contacts what is the avatar hash value\&. However, simple or small clients may not implement that protocol\&. .sp If this module is enabled, all the outgoing client presence stanzas get automatically the avatar hash on behalf of the client\&. So, the contacts receive the presence stanzas with the \fIUpdate Data\fR described in XEP\-0153 as if the client would had inserted it itself\&. If the client had already included such element in the presence stanza, it is replaced with the element generated by ejabberd\&. .sp By enabling this module, each vCard modification produces a hash recalculation, and each presence sent by a client produces hash retrieval and a presence stanza rewrite\&. For this reason, enabling this module will introduce a computational overhead in servers with clients that change frequently their presence\&. However, the overhead is significantly reduced by the use of caching, so you probably don\(cqt want to set \fIuse_cache\fR to \fIfalse\fR\&. .sp The module depends on \fImod_vcard\fR\&. .if n \{\ .sp .\} .RS 4 .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBNote\fR .ps -1 .br .sp Nowadays XEP\-0153 is used mostly as "read\-only", i\&.e\&. modern clients don\(cqt publish their avatars inside vCards\&. Thus in the majority of cases the module is only used along with \fImod_avatar\fR for providing backward compatibility\&. .sp .5v .RE .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBcache_life_time\fR: \fItimeout()\fR .RS 4 Same as top\-level \fIcache_life_time\fR option, but applied to this module only\&. .RE .PP \fBcache_missed\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIcache_missed\fR option, but applied to this module only\&. .RE .PP \fBcache_size\fR: \fIpos_integer() | infinity\fR .RS 4 Same as top\-level \fIcache_size\fR option, but applied to this module only\&. .RE .PP \fBuse_cache\fR: \fItrue | false\fR .RS 4 Same as top\-level \fIuse_cache\fR option, but applied to this module only\&. .RE .RE .SS "mod_version" .sp This module implements XEP\-0092: Software Version\&. Consequently, it answers ejabberd\(cqs version when queried\&. .sp .it 1 an-trap .nr an-no-space-flag 1 .nr an-break-flag 1 .br .ps +1 \fBAvailable options:\fR .RS 4 .PP \fBshow_os\fR: \fItrue | false\fR .RS 4 Should the operating system be revealed or not\&. The default value is \fItrue\fR\&. .RE .RE .SH "LISTENERS" .sp This section describes options of all ejabberd listeners\&. .sp TODO .SH "AUTHOR" .sp ProcessOne\&. .SH "VERSION" .sp This document describes the configuration file of ejabberd 21\&.12\&. Configuration options of other ejabberd versions may differ significantly\&. .SH "REPORTING BUGS" .sp Report bugs to https://github\&.com/processone/ejabberd/issues .SH "SEE ALSO" .sp Default configuration file: https://github\&.com/processone/ejabberd/blob/21\&.12/ejabberd\&.yml\&.example .sp Main site: https://ejabberd\&.im .sp Documentation: https://docs\&.ejabberd\&.im .sp Configuration Guide: https://docs\&.ejabberd\&.im/admin/configuration .sp Source code: https://github\&.com/processone/ejabberd .SH "COPYING" .sp Copyright (c) 2002\-2021 ProcessOne\&. ���������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/Makefile.in��������������������������������������������������������������������������0000644�0002322�0002322�00000034026�14154362354�015704� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������REBAR = @ESCRIPT@ @rebar@ MIX = @rebar@ INSTALL = @INSTALL@ SED = @SED@ ERL = @ERL@ prefix = @prefix@ exec_prefix = @exec_prefix@ DESTDIR = # /etc/ejabberd/ ETCDIR = $(DESTDIR)@sysconfdir@/ejabberd # /bin/ BINDIR = $(DESTDIR)@bindir@ # /sbin/ SBINDIR = $(DESTDIR)@sbindir@ # /lib/ LIBDIR = $(DESTDIR)@libdir@ # /lib/ejabberd/ EJABBERDDIR = $(DESTDIR)@libdir@/ejabberd # /share/doc/ejabberd PACKAGE_TARNAME = @PACKAGE_TARNAME@ datarootdir = @datarootdir@ DOCDIR = $(DESTDIR)@docdir@ # /share/doc/man/man5 MANDIR = $(DESTDIR)@mandir@/man5 # /usr/lib/ejabberd/ebin/ BEAMDIR = $(EJABBERDDIR)/ebin # /usr/lib/ejabberd/include/ INCLUDEDIR = $(EJABBERDDIR)/include # /usr/lib/ejabberd/priv/ PRIVDIR = $(EJABBERDDIR)/priv # /usr/lib/ejabberd/priv/bin PBINDIR = $(PRIVDIR)/bin # /usr/lib/ejabberd/priv/lib SODIR = $(PRIVDIR)/lib # /usr/lib/ejabberd/priv/msgs MSGSDIR = $(PRIVDIR)/msgs # /usr/lib/ejabberd/priv/css CSSDIR = $(PRIVDIR)/css # /usr/lib/ejabberd/priv/img IMGDIR = $(PRIVDIR)/img # /usr/lib/ejabberd/priv/js JSDIR = $(PRIVDIR)/js # /usr/lib/ejabberd/priv/sql SQLDIR = $(PRIVDIR)/sql # /usr/lib/ejabberd/priv/lua LUADIR = $(PRIVDIR)/lua # /var/lib/ejabberd/ SPOOLDIR = $(DESTDIR)@localstatedir@/lib/ejabberd # /var/log/ejabberd/ LOGDIR = $(DESTDIR)@localstatedir@/log/ejabberd INSTALLUSER=@INSTALLUSER@ # if no user was enabled, don't set privileges or ownership ifeq ($(INSTALLUSER),) O_USER= G_USER= CHOWN_COMMAND=echo CHOWN_OUTPUT=/dev/null INIT_USER=root else O_USER=-o $(INSTALLUSER) G_USER=-g $(INSTALLUSER) CHOWN_COMMAND=chown CHOWN_OUTPUT=&1 INIT_USER=$(INSTALLUSER) endif # if no group was enabled, don't set privileges or ownership INSTALLGROUP=@INSTALLGROUP@ ifneq ($(INSTALLGROUP),) G_USER=-g $(INSTALLGROUP) endif ifeq "$(MIX)" "mix" REBAR_VER:=6 else REBAR_VER:=$(shell $(REBAR) --version | awk -F '[ .]' '/rebar / {print $$2}') endif ifeq "$(REBAR_VER)" "6" REBAR=$(MIX) SKIPDEPS= LISTDEPS=deps.tree UPDATEDEPS=deps.update DEPSPATTERN="s/.*─ \([a-z0-9_]*\) .*/\1/p;" DEPSBASE=_build DEPSDIR=$(DEPSBASE)/dev/lib GET_DEPS= deps.get CONFIGURE_DEPS= EBINDIR=$(DEPSDIR)/ejabberd/ebin REBARREL=MIX_ENV=prod $(REBAR) release --overwrite REBARDEV=MIX_ENV=dev $(REBAR) release --overwrite else ifeq "$(REBAR_VER)" "3" SKIPDEPS= LISTDEPS=tree UPDATEDEPS=upgrade DEPSPATTERN="s/ (.*//; /^ / s/.* \([a-z0-9_]*\).*/\1/p;" DEPSBASE=_build DEPSDIR=$(DEPSBASE)/default/lib GET_DEPS= get-deps CONFIGURE_DEPS=$(REBAR) configure-deps EBINDIR=$(DEPSDIR)/ejabberd/ebin REBARREL=$(REBAR) as prod tar REBARDEV=REBAR_PROFILE=dev $(REBAR) release else SKIPDEPS=skip_deps=true LISTDEPS=-q list-deps UPDATEDEPS=update-deps DEPSPATTERN="/ TAG / s/ .*// p; / REV / s/ .*// p; / BRANCH / s/ .*// p;" DEPSBASE=deps DEPSDIR=$(DEPSBASE) GET_DEPS= get-deps CONFIGURE_DEPS=$(REBAR) configure-deps EBINDIR=ebin REBARREL=$(REBAR) generate REBARDEV= endif endif all: deps src deps: $(DEPSDIR)/.got $(DEPSDIR)/.got: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built mkdir -p $(DEPSDIR) $(REBAR) $(GET_DEPS) && :> $(DEPSDIR)/.got $(DEPSDIR)/.built: $(DEPSDIR)/.got $(CONFIGURE_DEPS) $(REBAR) compile && :> $(DEPSDIR)/.built src: $(DEPSDIR)/.built $(REBAR) $(SKIPDEPS) compile update: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built $(REBAR) $(UPDATEDEPS) && :> $(DEPSDIR)/.got xref: all $(REBAR) $(SKIPDEPS) xref hooks: all tools/hook_deps.sh $(EBINDIR) options: all tools/opt_types.sh ejabberd_option $(EBINDIR) translations: tools/prepare-tr.sh $(DEPSDIR) edoc: $(ERL) -noinput +B -eval \ 'case edoc:application(ejabberd, ".", []) of ok -> halt(0); error -> halt(1) end.' JOIN_PATHS=$(if $(wordlist 2,1000,$(1)),$(firstword $(1))/$(call JOIN_PATHS,$(wordlist 2,1000,$(1))),$(1)) VERSIONED_DEP=$(if $(DEP_$(1)_VERSION),$(DEP_$(1)_VERSION),$(1)) DEPIX:=$(words $(subst /, ,$(DEPSDIR))) LIBIX:=$(shell expr "$(DEPIX)" + 2) ELIXIR_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 5,1000,$(1)) DEPS_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,$(word 2,$(1))) $(wordlist 3,1000,$(1)) MAIN_TO_DEST=$(LIBDIR) $(call VERSIONED_DEP,ejabberd) $(1) TO_DEST_SINGLE=$(if $(subst X$(DEPSBASE)X,,X$(word 1,$(1))X),$(call MAIN_TO_DEST,$(1)),$(if $(subst XlibX,,X$(word $(LIBIX),$(1))X),$(call DEPS_TO_DEST,$(wordlist $(DEPIX),1000,$(1))),$(call ELIXIR_TO_DEST,$(wordlist $(DEPIX),1000,$(1))))) TO_DEST=$(foreach path,$(1),$(call JOIN_PATHS,$(call TO_DEST_SINGLE,$(subst /, ,$(path))))) FILTER_DIRS=$(foreach path,$(1),$(if $(wildcard $(path)/*),,$(path))) FILES_WILDCARD=$(call FILTER_DIRS,$(foreach w,$(1),$(wildcard $(w)))) ifeq ($(MAKECMDGOALS),copy-files-sub) DEPS:=$(sort $(shell QUIET=1 $(REBAR) $(LISTDEPS) | $(SED) -ne $(DEPSPATTERN) )) DEPS_FILES=$(call FILES_WILDCARD,$(foreach DEP,$(DEPS),$(DEPSDIR)/$(DEP)/ebin/*.beam $(DEPSDIR)/$(DEP)/ebin/*.app $(DEPSDIR)/$(DEP)/priv/* $(DEPSDIR)/$(DEP)/priv/lib/* $(DEPSDIR)/$(DEP)/priv/bin/* $(DEPSDIR)/$(DEP)/include/*.hrl $(DEPSDIR)/$(DEP)/COPY* $(DEPSDIR)/$(DEP)/LICENSE* $(DEPSDIR)/$(DEP)/lib/*/ebin/*.beam $(DEPSDIR)/$(DEP)/lib/*/ebin/*.app)) BINARIES=$(DEPSDIR)/epam/priv/bin/epam $(DEPSDIR)/eimp/priv/bin/eimp $(DEPSDIR)/fs/priv/mac_listener DEPS_FILES_FILTERED=$(filter-out $(BINARIES) $(DEPSDIR)/elixir/ebin/elixir.app,$(DEPS_FILES)) DEPS_DIRS=$(sort $(DEPSDIR)/ $(foreach DEP,$(DEPS),$(DEPSDIR)/$(DEP)/) $(dir $(DEPS_FILES))) MAIN_FILES=$(filter-out %/configure.beam,$(call FILES_WILDCARD,$(EBINDIR)/*.beam $(EBINDIR)/*.app priv/msgs/*.msg priv/css/*.css priv/img/*.png priv/js/*.js priv/lib/* include/*.hrl COPYING)) MAIN_DIRS=$(sort $(dir $(MAIN_FILES)) priv/bin priv/sql priv/lua) define DEP_VERSION_template DEP_$(1)_VERSION:=$(shell $(SED) -e '/vsn/!d;s/.*, *"/$(1)-/;s/".*//' $(2) 2>/dev/null) endef DELETE_TARGET_SO=$(if $(subst X.soX,,X$(suffix $(1))X),,rm -f $(call TO_DEST,$(1));) $(foreach DEP,$(DEPS),$(eval $(call DEP_VERSION_template,$(DEP),$(DEPSDIR)/$(DEP)/ebin/$(DEP).app))) $(eval $(call DEP_VERSION_template,ejabberd,$(EBINDIR)/ejabberd.app)) define COPY_template $(call TO_DEST,$(1)): $(1) $(call TO_DEST,$(dir $(1))) ; $(call DELETE_TARGET_SO, $(1)) $$(INSTALL) -m 644 $(1) $(call TO_DEST,$(1)) endef define COPY_BINARY_template $(call TO_DEST,$(1)): $(1) $(call TO_DEST,$(dir $(1))) ; rm -f $(call TO_DEST,$(1)); $$(INSTALL) -m 755 $$(O_USER) $(1) $(call TO_DEST,$(1)) endef $(foreach file,$(DEPS_FILES_FILTERED) $(MAIN_FILES),$(eval $(call COPY_template,$(file)))) $(foreach file,$(BINARIES),$(eval $(call COPY_BINARY_template,$(file)))) $(sort $(call TO_DEST,$(MAIN_DIRS) $(DEPS_DIRS))): $(INSTALL) -d $@ $(call TO_DEST,priv/sql/lite.sql): sql/lite.sql $(call TO_DEST,priv/sql) $(INSTALL) -m 644 $< $@ $(call TO_DEST,priv/sql/lite.new.sql): sql/lite.new.sql $(call TO_DEST,priv/sql) $(INSTALL) -m 644 $< $@ $(call TO_DEST,priv/bin/captcha.sh): tools/captcha.sh $(call TO_DEST,priv/bin) $(INSTALL) -m 755 $(O_USER) $< $@ $(call TO_DEST,priv/lua/redis_sm.lua): priv/lua/redis_sm.lua $(call TO_DEST,priv/lua) $(INSTALL) -m 644 $< $@ ifeq (@sqlite@,true) SQLITE_FILES = priv/sql/lite.sql priv/sql/lite.new.sql endif ifeq (@redis@,true) REDIS_FILES = priv/lua/redis_sm.lua endif copy-files-sub2: $(call TO_DEST,$(DEPS_FILES) $(MAIN_FILES) priv/bin/captcha.sh $(SQLITE_FILES) $(REDIS_FILES)) .PHONY: $(call TO_DEST,$(DEPS_FILES) $(MAIN_DIRS) $(DEPS_DIRS)) endif copy-files: $(MAKE) copy-files-sub copy-files-sub: copy-files-sub2 install: copy-files # # Configuration files $(INSTALL) -d -m 750 $(G_USER) $(ETCDIR) [ -f $(ETCDIR)/ejabberd.yml ] \ && $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml-new \ || $(INSTALL) -b -m 640 $(G_USER) ejabberd.yml.example $(ETCDIR)/ejabberd.yml $(SED) -e "s*{{rootdir}}*@prefix@*g" \ -e "s*{{installuser}}*@INSTALLUSER@*g" \ -e "s*{{bindir}}*@bindir@*g" \ -e "s*{{libdir}}*@libdir@*g" \ -e "s*{{sysconfdir}}*@sysconfdir@*g" \ -e "s*{{localstatedir}}*@localstatedir@*g" \ -e "s*{{docdir}}*@docdir@*g" \ -e "s*{{erl}}*@ERL@*g" \ -e "s*{{epmd}}*@EPMD@*g" ejabberdctl.template \ > ejabberdctl.example [ -f $(ETCDIR)/ejabberdctl.cfg ] \ && $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg-new \ || $(INSTALL) -b -m 640 $(G_USER) ejabberdctl.cfg.example $(ETCDIR)/ejabberdctl.cfg $(INSTALL) -b -m 644 $(G_USER) inetrc $(ETCDIR)/inetrc # # Administration script [ -d $(SBINDIR) ] || $(INSTALL) -d -m 755 $(SBINDIR) $(INSTALL) -m 550 $(G_USER) ejabberdctl.example $(SBINDIR)/ejabberdctl # Elixir binaries [ -d $(BINDIR) ] || $(INSTALL) -d -m 755 $(BINDIR) [ -f $(DEPSDIR)/elixir/bin/iex ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/iex $(BINDIR)/iex || true [ -f $(DEPSDIR)/elixir/bin/elixir ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/elixir $(BINDIR)/elixir || true [ -f $(DEPSDIR)/elixir/bin/mix ] && $(INSTALL) -m 550 $(G_USER) $(DEPSDIR)/elixir/bin/mix $(BINDIR)/mix || true # # Init script $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ -e "s*@installuser@*$(INIT_USER)*g" ejabberd.init.template \ > ejabberd.init chmod 755 ejabberd.init # # Service script $(SED) -e "s*@ctlscriptpath@*$(SBINDIR)*g" \ -e "s*@installuser@*$(INIT_USER)*g" ejabberd.service.template \ > ejabberd.service chmod 644 ejabberd.service # # Spool directory $(INSTALL) -d -m 750 $(O_USER) $(SPOOLDIR) $(CHOWN_COMMAND) -R @INSTALLUSER@ $(SPOOLDIR) >$(CHOWN_OUTPUT) chmod -R 750 $(SPOOLDIR) # # Log directory $(INSTALL) -d -m 750 $(O_USER) $(LOGDIR) $(CHOWN_COMMAND) -R @INSTALLUSER@ $(LOGDIR) >$(CHOWN_OUTPUT) chmod -R 750 $(LOGDIR) # # Documentation $(INSTALL) -d $(MANDIR) $(INSTALL) -d $(DOCDIR) [ -f man/ejabberd.yml.5 ] \ && $(INSTALL) -m 644 man/ejabberd.yml.5 $(MANDIR) \ || echo "Man page not included in sources" $(INSTALL) -m 644 COPYING $(DOCDIR) uninstall: uninstall-binary uninstall-binary: rm -f $(SBINDIR)/ejabberdctl rm -f $(BINDIR)/iex rm -f $(BINDIR)/elixir rm -f $(BINDIR)/mix rm -fr $(DOCDIR) rm -f $(BEAMDIR)/*.beam rm -f $(BEAMDIR)/*.app rm -fr $(BEAMDIR) rm -f $(INCLUDEDIR)/*.hrl rm -fr $(INCLUDEDIR) rm -fr $(PBINDIR) rm -f $(SODIR)/*.so rm -fr $(SODIR) rm -f $(MSGSDIR)/*.msg rm -fr $(MSGSDIR) rm -f $(CSSDIR)/*.css rm -fr $(CSSDIR) rm -f $(IMGDIR)/*.png rm -fr $(IMGDIR) rm -f $(JSDIR)/*.js rm -fr $(JSDIR) rm -f $(SQLDIR)/*.sql rm -fr $(SQLDIR) rm -fr $(LUADIR)/*.lua rm -fr $(LUADIR) rm -fr $(PRIVDIR) rm -fr $(EJABBERDDIR) uninstall-all: uninstall-binary rm -rf $(ETCDIR) rm -rf $(EJABBERDDIR) rm -rf $(SPOOLDIR) rm -rf $(LOGDIR) clean: rm -rf $(DEPSDIR)/.got rm -rf $(DEPSDIR)/.built rm -rf test/*.beam $(REBAR) clean clean-rel: rm -rf rel/ejabberd distclean: clean clean-rel rm -f config.status rm -f config.log rm -rf autom4te.cache rm -rf $(EBINDIR) rm -rf $(DEPSBASE) rm -f Makefile rm -f vars.config rm -f src/ejabberd.app.src rm -f ejabberdctl.example ejabberd.init ejabberd.service [ ! -f ../ChangeLog ] || rm -f ../ChangeLog rel: $(REBARREL) DEV_CONFIG = _build/dev/rel/ejabberd/etc/ejabberd/ejabberd.yml dev $(DEV_CONFIG): $(REBARDEV) TAGS: etags *.erl Makefile: Makefile.in ifeq "$(REBAR_VER)" "3" dialyzer: $(REBAR) dialyzer else deps := $(wildcard $(DEPSDIR)/*/ebin) dialyzer/erlang.plt: @mkdir -p dialyzer @dialyzer --build_plt --output_plt dialyzer/erlang.plt \ -o dialyzer/erlang.log --apps kernel stdlib sasl crypto \ public_key ssl mnesia inets odbc compiler erts \ os_mon asn1 syntax_tools; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi dialyzer/deps.plt: @mkdir -p dialyzer @dialyzer --build_plt --output_plt dialyzer/deps.plt \ -o dialyzer/deps.log $(deps); \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi dialyzer/ejabberd.plt: @mkdir -p dialyzer @dialyzer --build_plt --output_plt dialyzer/ejabberd.plt \ -o dialyzer/ejabberd.log ebin; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi erlang_plt: dialyzer/erlang.plt @dialyzer --plt dialyzer/erlang.plt --check_plt -o dialyzer/erlang.log; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi deps_plt: dialyzer/deps.plt @dialyzer --plt dialyzer/deps.plt --check_plt -o dialyzer/deps.log; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi ejabberd_plt: dialyzer/ejabberd.plt @dialyzer --plt dialyzer/ejabberd.plt --check_plt -o dialyzer/ejabberd.log; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi dialyzer: erlang_plt deps_plt ejabberd_plt @dialyzer --plts dialyzer/*.plt --no_check_plt \ --get_warnings -o dialyzer/error.log ebin; \ status=$$? ; if [ $$status -ne 2 ]; then exit $$status; else exit 0; fi endif test: @echo "************************** NOTICE ***************************************" @cat test/README @echo "*************************************************************************" @cd priv && ln -sf ../sql $(REBAR) $(SKIPDEPS) ct .PHONY: src edoc dialyzer Makefile TAGS clean clean-rel distclean rel \ install uninstall uninstall-binary uninstall-all translations deps test \ quicktest erlang_plt deps_plt ejabberd_plt xref hooks options help: @echo "" @echo " [all] Compile dependencies and ejabberd" @echo " src Compile ejabberd" @echo " deps Get dependencies" @echo " update Update dependencies' source code" @echo " clean Clean binary files" @echo " distclean Clean completely the development files" @echo "" @echo " install Install ejabberd to /usr/local" @echo " uninstall Uninstall ejabberd (buggy)" @echo " uninstall-all Uninstall also configuration, logs, mnesia... (buggy)" @echo "" @echo " rel Build a production release" @echo " dev Build a development release" @echo "" @echo " edoc Generate edoc documentation (unused)" @echo " options Generate ejabberd_option.erl" @echo " translations Extract translation files (requires --enable-tools)" @echo " tags Generate tags file for text editors" @echo "" @echo " dialyzer Run Dialyzer static analyzer" @echo " hooks Run hooks validator" @echo " test Run Common Tests suite" @echo " xref Run cross reference analysis" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ejabberd-21.12/CONTRIBUTING.md����������������������������������������������������������������������0000644�0002322�0002322�00000014645�14154362354�016075� 0����������������������������������������������������������������������������������������������������ustar �debalance�����������������������debalance��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Contributing to ejabberd We'd love for you to contribute to our source code and to make ejabberd even better than it is today! Here are the guidelines we'd like you to follow: * [Code of Conduct](#coc) * [Questions and Problems](#question) * [Issues and Bugs](#issue) * [Feature Requests](#feature) * [Issue Submission Guidelines](#submit) * [Pull Request Submission Guidelines](#submit-pr) * [Signing the CLA](#cla) ## <a name="coc"></a> Code of Conduct Help us keep ejabberd community open-minded and inclusive. Please read and follow our [Code of Conduct][coc]. ## <a name="requests"></a> Questions, Bugs, Features ### <a name="question"></a> Got a Question or Problem? Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on dedicated support platforms, the best being [Stack Overflow][stackoverflow]. Stack Overflow is a much better place to ask questions since: - there are thousands of people willing to help on Stack Overflow - questions and answers stay available for public viewing so your question / answer might help someone else - Stack Overflow's voting system assures that the best answers are prominently visible. To save your and our time, we will systematically close all issues that are requests for general support and redirect people to the section you are reading right now. Other channels for support are: - [ejabberd Mailing List][list] - [ejabberd XMPP room][muc]: ejabberd@conference.process-one.net - [ejabberd XMPP room logs][logs] ### <a name="issue"></a> Found an Issue or Bug? If you find a bug in the source code, you can help us by submitting an issue to our [GitHub Repository][github]. Even better, you can submit a Pull Request with a fix. ### <a name="feature"></a> Missing a Feature? You can request a new feature by submitting an issue to our [GitHub Repository][github-issues]. If you would like to implement a new feature then consider what kind of change it is: * **Major Changes** that you wish to contribute to the project should be discussed first in an [GitHub issue][github-issues] that clearly outlines the changes and benefits of the feature. * **Small Changes** can directly be crafted and submitted to the [GitHub Repository][github] as a Pull Request. See the section about [Pull Request Submission Guidelines](#submit-pr). ## <a name="submit"></a> Issue Submission Guidelines Before you submit your issue search the archive, maybe your question was already answered. If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. The "[new issue][github-new-issue]" form contains a number of prompts that you should fill out to make it easier to understand and categorize the issue. ## <a name="submit-pr"></a> Pull Request Submission Guidelines By submitting a pull request for a code or doc contribution, you need to have the right to grant your contribution's copyright license to ProcessOne. Please check [ProcessOne CLA][cla] for details. Before you submit your pull request consider the following guidelines: * Search [GitHub][github-pr] for an open or closed Pull Request that relates to your submission. You don't want to duplicate effort. * Create the [development environment][developer-setup] * Make your changes in a new git branch: ```shell git checkout -b my-fix-branch master ``` * Test your changes and, if relevant, expand the automated test suite. * Create your patch commit, including appropriate test cases. * If the changes affect public APIs, change or add relevant [documentation][doc-repo]. * Commit your changes using a descriptive commit message. ```shell git commit -a ``` Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. * Push your branch to GitHub: ```shell git push origin my-fix-branch ``` * In GitHub, send a pull request to `ejabberd:master`. This will trigger the automated testing. We will also notify you if you have not yet signed the [contribution agreement][cla]. * If you find that the tests have failed, look into the logs to find out if your changes caused test failures, the commit message was malformed etc. If you find that the tests failed or times out for unrelated reasons, you can ping a team member so that the build can be restarted. * If we suggest changes, then: * Make the required updates. * Test your changes and test cases. * Commit your changes to your branch (e.g. `my-fix-branch`). * Push the changes to your GitHub repository (this will update your Pull Request). You can also amend the initial commits and force push them to the branch. ```shell git rebase master -i git push origin my-fix-branch -f ``` This is generally easier to follow, but separate commits are useful if the Pull Request contains iterations that might be interesting to see side-by-side. That's it! Thank you for your contribution! ## <a name="cla"></a> Signing the Contributor License Agreement (CLA) Upon submitting a Pull Request, we will ask you to sign our CLA if you haven't done so before. It's a quick process, we promise, and you will be able to do it all online You can read [ProcessOne Contribution License Agreement][cla] in PDF. This is part of the legal framework of the open-source ecosystem that adds some red tape, but protects both the contributor and the company / foundation behind the project. It also gives us the option to relicense the code with a more permissive license in the future. [coc]: https://github.com/processone/ejabberd/blob/master/CODE_OF_CONDUCT.md [stackoverflow]: https://stackoverflow.com/questions/tagged/ejabberd?sort=newest [list]: https://lists.jabber.ru/mailman/listinfo/ejabberd [muc]: xmpp:ejabberd@conference.process-one.net [logs]: https://process-one.net/logs/ejabberd@conference.process-one.net/ [github]: https://github.com/processone/ejabberd [github-issues]: https://github.com/processone/ejabberd/issues [github-new-issue]: https://github.com/processone/ejabberd/issues/new [github-pr]: https://github.com/processone/ejabberd/pulls [doc-repo]: https://github.com/processone/docs.ejabberd.im [developer-setup]: https://docs.ejabberd.im/developer/ [cla]: https://www.process-one.net/resources/ejabberd-cla.pdf [license]: https://github.com/processone/ejabberd/blob/master/COPYING �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������